From bd0a0ee8548ebc950eb7bcc5cdb11e64c9beb03b Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Fri, 16 Sep 2022 11:07:22 -0500 Subject: [PATCH 01/52] start --- .../e2e/commands/sessions/sessions.cy.js | 315 +++++++++--------- .../driver/src/cy/commands/sessions/index.ts | 115 +++++-- packages/driver/src/cypress/command_queue.ts | 5 +- packages/driver/src/cypress/state.ts | 2 +- packages/reporter/src/commands/command.tsx | 183 +++++----- packages/reporter/src/commands/commands.scss | 2 +- packages/reporter/src/errors/errors.scss | 13 +- 7 files changed, 351 insertions(+), 284 deletions(-) diff --git a/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js b/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js index d6699a770563..31cad336ced5 100644 --- a/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js +++ b/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js @@ -319,65 +319,67 @@ describe('cy.session', { retries: 0 }, () => { setupTestContext() cy.log('Creating new session with validation to test against') - cy.once('fail', (err) => { - 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(logs[0].get()).to.deep.contain({ - name: 'session', - id: sessionGroupId, - renderProps: { - status: 'failed', - }, - }) - - expect(logs[1].get()).to.contain({ - name: 'Clear page', - group: sessionGroupId, - }) - - expect(logs[2].get()).to.contain({ - displayName: 'Clear cookies, localStorage and sessionStorage', - group: sessionGroupId, - }) - - const createNewSessionGroup = logs[3].get() - - expect(createNewSessionGroup).to.contain({ - displayName: 'Create new session', - groupStart: true, - group: sessionGroupId, - }) - - expect(logs[4].get()).to.deep.contain({ - alias: ['setupSession'], - group: createNewSessionGroup.id, - }) - - expect(logs[5].get()).to.contain({ - name: 'Clear page', - group: createNewSessionGroup.id, - }) - - const validateSessionGroup = logs[6].get() - - expect(validateSessionGroup).to.contain({ - displayName: 'Validate session', - group: sessionGroupId, - }) - - expect(logs[7].get()).to.deep.contain({ - alias: ['validateSession'], - group: validateSessionGroup.id, - }) - - done() - }) + // cy.once('fail', (err) => { + // 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(logs[0].get()).to.deep.contain({ + // name: 'session', + // id: sessionGroupId, + // renderProps: { + // status: 'failed', + // }, + // }) + + // expect(logs[1].get()).to.contain({ + // name: 'Clear page', + // group: sessionGroupId, + // }) + + // expect(logs[2].get()).to.contain({ + // displayName: 'Clear cookies, localStorage and sessionStorage', + // group: sessionGroupId, + // }) + + // const createNewSessionGroup = logs[3].get() + + // expect(createNewSessionGroup).to.contain({ + // displayName: 'Create new session', + // groupStart: true, + // group: sessionGroupId, + // }) + + // expect(logs[4].get()).to.deep.contain({ + // alias: ['setupSession'], + // group: createNewSessionGroup.id, + // }) + + // expect(logs[5].get()).to.contain({ + // name: 'Clear page', + // group: createNewSessionGroup.id, + // }) + + // const validateSessionGroup = logs[6].get() + + // expect(validateSessionGroup).to.contain({ + // displayName: 'Validate session', + // group: sessionGroupId, + // }) + + // expect(logs[7].get()).to.deep.contain({ + // alias: ['validateSession'], + // group: validateSessionGroup.id, + // }) + + // done() + // }) validate.callsFake(() => false) - cy.session(`session-${Cypress.state('test').id}`, setup, { validate }) + cy.session(`session-${Cypress.state('test').id}`, setup, { validate: () => { + cy.get('does_not_exist') + } }) }) }) @@ -635,114 +637,117 @@ describe('cy.session', { retries: 0 }, () => { }) describe('recreates existing session with failed validation flow', () => { - it('fails to recreate session and logs correctly', function (done) { + it.only('fails to recreate session and logs correctly', function () { setupTestContext() cy.log('Creating new session for test') cy.session(`session-${Cypress.state('test').id}`, setup, { validate }) .then(() => { // reset and only test restored session resetMocks() - validate.callsFake(() => false) - }) - - cy.once('fail', (err) => { - expect(err.message).to.contain('Your `cy.session` **validate** callback returned false') - expect(setup).to.be.calledOnce - expect(validate).to.be.calledTwice - expect(clearPageCount, 'total times session cleared the page').to.eq(3) - - expect(logs[0].get()).to.contain({ - name: 'session', - id: sessionGroupId, - }) - - expect(logs[0].get()).to.deep.contain({ - name: 'session', - id: sessionGroupId, - renderProps: { - status: 'failed', - }, - }) - - expect(logs[1].get()).to.contain({ - name: 'Clear page', - group: sessionGroupId, - }) - - expect(logs[2].get()).to.contain({ - displayName: 'Clear cookies, localStorage and sessionStorage', - group: sessionGroupId, - }) - - const restoreSavedSessionGroup = logs[3].get() - - expect(restoreSavedSessionGroup).to.contain({ - displayName: 'Restore saved session', - group: sessionGroupId, - }) - - const validateSessionGroup = logs[4].get() - - expect(validateSessionGroup).to.contain({ - displayName: 'Validate session', - group: sessionGroupId, - }) - - expect(logs[5].get()).to.deep.contain({ - alias: ['validateSession'], - group: validateSessionGroup.id, - }) - - expect(logs[6].get()).to.deep.contain({ - showError: true, - group: validateSessionGroup.id, - }) - - expect(logs[6].get('error').message).to.eq('Your `cy.session` **validate** callback returned false.') - - expect(logs[7].get()).to.contain({ - name: 'Clear page', - group: sessionGroupId, - }) - - expect(logs[8].get()).to.contain({ - displayName: 'Clear cookies, localStorage and sessionStorage', - group: sessionGroupId, - }) - - const createNewSessionGroup = logs[9].get() - - expect(createNewSessionGroup).to.contain({ - displayName: 'Recreate session', - groupStart: true, - group: sessionGroupId, - }) - - expect(logs[10].get()).to.deep.contain({ - alias: ['setupSession'], - group: createNewSessionGroup.id, - }) - - expect(logs[11].get()).to.contain({ - name: 'Clear page', - group: createNewSessionGroup.id, - }) - - const secondValidateSessionGroup = logs[12].get() - - expect(secondValidateSessionGroup).to.contain({ - displayName: 'Validate session', - group: sessionGroupId, - }) - - expect(logs[13].get()).to.deep.contain({ - alias: ['validateSession'], - group: secondValidateSessionGroup.id, + validate.callsFake(() => { + return false + // cy.get('does not exist') }) - - done() }) + // cy.once('fail', (err) => { + // expect(err.message).to.contain('Your `cy.session` **validate** callback returned false') + // expect(setup).to.be.calledOnce + // expect(validate).to.be.calledTwice + // expect(clearPageCount, 'total times session cleared the page').to.eq(3) + + // expect(logs[0].get()).to.contain({ + // name: 'session', + // id: sessionGroupId, + // }) + + // expect(logs[0].get()).to.deep.contain({ + // name: 'session', + // id: sessionGroupId, + // renderProps: { + // status: 'failed', + // }, + // }) + + // expect(logs[1].get()).to.contain({ + // name: 'Clear page', + // group: sessionGroupId, + // }) + + // expect(logs[2].get()).to.contain({ + // displayName: 'Clear cookies, localStorage and sessionStorage', + // group: sessionGroupId, + // }) + + // const restoreSavedSessionGroup = logs[3].get() + + // expect(restoreSavedSessionGroup).to.contain({ + // displayName: 'Restore saved session', + // group: sessionGroupId, + // }) + + // const validateSessionGroup = logs[4].get() + + // expect(validateSessionGroup).to.contain({ + // displayName: 'Validate session', + // group: sessionGroupId, + // }) + + // expect(logs[5].get()).to.deep.contain({ + // alias: ['validateSession'], + // group: validateSessionGroup.id, + // }) + + // expect(logs[6].get()).to.deep.contain({ + // showError: true, + // group: validateSessionGroup.id, + // }) + + // expect(logs[6].get('error').message).to.eq('Your `cy.session` **validate** callback returned false.') + + // expect(logs[7].get()).to.contain({ + // name: 'Clear page', + // group: sessionGroupId, + // }) + + // expect(logs[8].get()).to.contain({ + // displayName: 'Clear cookies, localStorage and sessionStorage', + // group: sessionGroupId, + // }) + + // const createNewSessionGroup = logs[9].get() + + // expect(createNewSessionGroup).to.contain({ + // displayName: 'Recreate session', + // groupStart: true, + // group: sessionGroupId, + // }) + + // expect(logs[10].get()).to.deep.contain({ + // alias: ['setupSession'], + // group: createNewSessionGroup.id, + // }) + + // expect(logs[11].get()).to.contain({ + // name: 'Clear page', + // group: createNewSessionGroup.id, + // }) + + // const secondValidateSessionGroup = logs[12].get() + + // expect(secondValidateSessionGroup).to.contain({ + // displayName: 'Validate session', + // group: sessionGroupId, + // }) + + // expect(logs[13].get()).to.deep.contain({ + // alias: ['validateSession'], + // group: secondValidateSessionGroup.id, + // }) + + // done() + // }) + cy.log('restore session to test against') cy.session(`session-${Cypress.state('test').id}`, setup, { validate }) }) diff --git a/packages/driver/src/cy/commands/sessions/index.ts b/packages/driver/src/cy/commands/sessions/index.ts index d045eb0b7720..05b5ddc43ece 100644 --- a/packages/driver/src/cy/commands/sessions/index.ts +++ b/packages/driver/src/cy/commands/sessions/index.ts @@ -23,6 +23,10 @@ type SessionData = Cypress.Commands.Session.SessionData export default function (Commands, Cypress, cy) { function throwIfNoSessionSupport () { + if (Cypress.isBrowser('webkit')) { + $errUtils.throwErrByPath('webkit.session') + } + if (!Cypress.config('experimentalSessionAndOrigin')) { $errUtils.throwErrByPath('sessions.experimentNotEnabled', { args: { @@ -52,11 +56,7 @@ export default function (Commands, Cypress, cy) { }) Commands.addAll({ - session (id, setup?: Function, options: { validate?: Function } = {}) { - if (Cypress.isBrowser('webkit')) { - return $errUtils.throwErrByPath('webkit.session') - } - + session (id, setup?: Function, options: Cypress.SessionOptions = { cacheAcrossSpecs: false }) { throwIfNoSessionSupport() if (!id || !_.isString(id) && !_.isObject(id)) { @@ -118,6 +118,19 @@ export default function (Commands, Cypress, cy) { } } + function setSessionLogStatus (status: string) { + _log.set({ + sessionInfo: { + id: existingSession.id, + isGlobalSession: false, + status, + }, + renderProps: { + status, + }, + }) + } + function createSession (existingSession, recreateSession = false) { logGroup(Cypress, { name: 'session', @@ -158,7 +171,7 @@ export default function (Commands, Cypress, cy) { }) } - function validateSession (existingSession, restoreSession = false) { + function validateSession (existingSession, sessionStatus: 'created' | 'restored' | 'recreated' = 'created') { const isValidSession = true if (!existingSession.validate) { @@ -177,10 +190,12 @@ export default function (Commands, Cypress, cy) { } const onFail = (err) => { - validateLog.set({ state: 'failed' }) - _log.set({ renderProps: { status: 'failed' } }) // show validation error and allow sessions workflow to recreate the session - if (restoreSession) { + if (sessionStatus === 'restored') { + $errUtils.modifyErrMsg(err, `\n\nThis error occurred while validating the restored session. Because validation failed, we will try to recreate the session.`, _.add) + + const current = cy.state('current') + Cypress.log({ showError: true, type: 'system', @@ -191,21 +206,36 @@ export default function (Commands, Cypress, cy) { return !isValidSession } - $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) + validateLog.set({ state: 'failed' }) + setSessionLogStatus('failed') + $errUtils.modifyErrMsg(err, `\n\nThis error occurred while validating the ${sessionStatus} session. Because validation failed immediately after creating the session, we failed the test.`, _.add) - return cy.fail(err) + return cy.then(() => { + cy.fail(err) + }) } - return validate(existingSession, onSuccess, onFail) + return validate(existingSession, sessionStatus, onSuccess, onFail) }) }) } // uses Cypress hackery to resolve `false` if validate() resolves/returns false or throws/fails a cypress command. - function validate (existingSession, onSuccess, onFail) { + function validate (existingSession, sessionStatus, onSuccess, onFail) { let returnVal let _validationError = null + console.log('validate!', sessionStatus) + const normalizeError = (err) => { + err.stack = $stackUtils.normalizedStack(err) + + return $errUtils.enhanceStack({ + err, + userInvocationStack: $errUtils.getUserInvocationStack(err, Cypress.state), + projectRoot: Cypress.config('projectRoot'), + }) + } + try { returnVal = existingSession.validate() } catch (e) { @@ -231,7 +261,8 @@ export default function (Commands, Cypress, cy) { } // catch when a cypress command fails in the validate callback to move the queue index - cy.state('onCommandFailed', (err, queue, next) => { + cy.state('onCommandFailed', (err, queue, next, commandRunningFailed) => { + console.log('onCommandFailed', err) const index = _.findIndex(queue.get(), (command: any) => { return ( _commandToRunAfterValidation @@ -239,29 +270,28 @@ export default function (Commands, Cypress, cy) { ) }) + console.log(index) + // 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) } - err.stack = $stackUtils.normalizedStack(err) + // commandRunningFailed(Cypress, queue.state, err) - _validationError = $errUtils.enhanceStack({ - err, - userInvocationStack: $errUtils.getUserInvocationStack(err, Cypress.state), - projectRoot: Cypress.config('projectRoot'), - }) + _validationError = normalizeError(err) // move to _commandToRunAfterValidation's index to ensure failures are handled correctly cy.state('index', index) - cy.state('onCommandFailed', null) + // cy.state('onCommandFailed', null) return next() }) const _commandToRunAfterValidation = cy.then(async () => { + console.log('_commandToRunAfterValidation', _validationError) cy.state('onCommandFailed', null) if (_validationError) { @@ -270,19 +300,29 @@ export default function (Commands, Cypress, cy) { if (returnVal === false) { // set current command to cy.session for more accurate codeframe + const current = cy.state('current') + cy.state('current', sessionCommand) - return onFail($errUtils.errByPath('sessions.validate_callback_false', { reason: 'returned false' })) - } + const err = normalizeError($errUtils.errByPath('sessions.validate_callback_false', { reason: 'returned false' })) - if (returnVal === undefined || Cypress.isCy(returnVal)) { - const val = cy.state('current').get('prev')?.attributes?.subject + cy.state('current', current) - if (val === false) { - return onFail($errUtils.errByPath('sessions.validate_callback_false', { reason: 'resolved false' })) - } + return onFail(err) } + // if (returnVal === undefined || Cypress.isCy(returnVal)) { + // const val = cy.state('current').get('prev')?.attributes?.subject + + // if (val === false) { + // // set current command to cy.session for more accurate codeframe + // console.log(cy.state('current')) + // cy.state('current', sessionCommand) + + // return onFail($errUtils.errByPath('sessions.validate_callback_false', { reason: 'resolved false' })) + // } + // } + return onSuccess() }) @@ -294,21 +334,22 @@ export default function (Commands, Cypress, cy) { * 1. create session * 2. validate session */ - const createSessionWorkflow = (existingSession, recreateSession = false) => { + const createSessionWorkflow = (existingSession, sessionStatus: 'creating' | 'recreating' = 'creating') => { return cy.then(async () => { - _log.set({ renderProps: { status: recreateSession ? 'recreating' : 'creating' } }) + setSessionLogStatus(sessionStatus) + await navigateAboutBlank() await sessions.clearCurrentSessionData() - return createSession(existingSession, recreateSession) + return createSession(existingSession, sessionStatus === 'recreating') }) - .then(() => validateSession(existingSession)) + .then(() => validateSession(existingSession, sessionStatus === 'recreating' ? 'recreated' : 'created')) .then((isValidSession: boolean) => { if (!isValidSession) { return } - _log.set({ renderProps: { status: recreateSession ? 'recreated' : 'created' } }) + setSessionLogStatus(sessionStatus === 'recreating' ? 'recreated' : 'created') }) } @@ -320,19 +361,19 @@ export default function (Commands, Cypress, cy) { */ const restoreSessionWorkflow = (existingSession) => { return cy.then(async () => { - _log.set({ renderProps: { status: 'restoring' } }) + setSessionLogStatus('restoring') await navigateAboutBlank() await sessions.clearCurrentSessionData() return restoreSession(existingSession) }) - .then(() => validateSession(existingSession, true)) + .then(() => validateSession(existingSession, 'restored')) .then((isValidSession: boolean) => { if (!isValidSession) { - return createSessionWorkflow(existingSession, true) + return createSessionWorkflow(existingSession, 'recreating') } - _log.set({ renderProps: { status: 'restored' } }) + setSessionLogStatus('restored') }) } diff --git a/packages/driver/src/cypress/command_queue.ts b/packages/driver/src/cypress/command_queue.ts index cc23dccbb590..45e04ededde0 100644 --- a/packages/driver/src/cypress/command_queue.ts +++ b/packages/driver/src/cypress/command_queue.ts @@ -36,6 +36,7 @@ const commandRunningFailed = (Cypress, state, err) => { end: true, snapshot: true, error: err, + // showError: true, consoleProps () { if (!current) return @@ -374,8 +375,10 @@ export class CommandQueue extends Queue<$Command> { return } + console.log('onError') + if (this.state('onCommandFailed')) { - return this.state('onCommandFailed')(err, this, next) + return this.state('onCommandFailed')(err, this, next, commandRunningFailed) } debugErrors('caught error in promise chain: %o', err) diff --git a/packages/driver/src/cypress/state.ts b/packages/driver/src/cypress/state.ts index 27144a00fd66..853d7c9dc368 100644 --- a/packages/driver/src/cypress/state.ts +++ b/packages/driver/src/cypress/state.ts @@ -53,7 +53,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, next: any) => boolean): (err: any, queue: any, next: any) => boolean + (k: 'onCommandFailed', v?: (err: any, queue: any, next: any) => boolean): (err: Error | string, queue: any, next: any, onCommandFailed: () => void) => boolean (k: 'promise', v?: Bluebird): Bluebird (k: 'reject', v?: (err: any) => any): (err: any) => any (k: 'cancel', v?: () => void): () => void diff --git a/packages/reporter/src/commands/command.tsx b/packages/reporter/src/commands/command.tsx index a38cca762653..734ed31fac83 100644 --- a/packages/reporter/src/commands/command.tsx +++ b/packages/reporter/src/commands/command.tsx @@ -257,6 +257,77 @@ interface Props { runnablesStore: RunnablesStore groupId?: number } +const CommandDetails = ({ model, events, aliasesWithDuplicates }) => { + const commandName = model.name ? nameClassName(model.name) : '' + const displayNumOfElements = model.state !== 'pending' && model.numElements != null && model.numElements !== 1 + const isSystemEvent = model.type === 'system' && model.event + const isSessionCommand = commandName === 'session' + const displayNumOfChildren = !isSystemEvent && !isSessionCommand && model.hasChildren && !model.isOpen + + const _removeStudioCommand = (e: React.MouseEvent) => { + e.preventDefault() + e.stopPropagation() + + events.emit('studio:remove:command', model.number) + } + + return ( + <> + + + + {model.event && model.type !== 'system' ? `(${displayName(model)})` : displayName(model)} + + + {model.referencesAlias ? + + : + } + + + {model.type === 'parent' && model.isStudio && ( + + )} + {isSessionCommand && ( + + )} + {!model.visible && ( + + + + + + )} + {displayNumOfElements && ( + + )} + + + + {displayNumOfChildren && ( + + )} + + + + ) +} @observer class Command extends Component { @@ -276,17 +347,7 @@ class Command extends Component { return null } - if (model.showError) { - // this error is rendered if an error occurs in session validation executed by cy.session - return - } - const commandName = model.name ? nameClassName(model.name) : '' - const displayNumOfElements = model.state !== 'pending' && model.numElements != null && model.numElements !== 1 - const isSystemEvent = model.type === 'system' && model.event - const isSessionCommand = commandName === 'session' - const displayNumOfChildren = !isSystemEvent && !isSessionCommand && model.hasChildren && !model.isOpen - const groupPlaceholder: Array = [] if (model.groupLevel !== undefined) { @@ -313,78 +374,39 @@ class Command extends Component { { 'command-is-event': !!model.event, 'command-is-pinned': this._isPinned(), - 'command-is-interactive': model.hasConsoleProps || model.hasSnapshot, + 'command-is-interactive': !model.showError && (model.hasConsoleProps || model.hasSnapshot), }, ) } > - -
this._snapshot(true)} - onMouseLeave={() => this._snapshot(false)} + {model.showError && ( + +
+ {groupPlaceholder} + +
+
+ )} + {!model.showError && ( + - {groupPlaceholder} - - - - {model.event && model.type !== 'system' ? `(${displayName(model)})` : displayName(model)} - - - {model.referencesAlias ? - - : - } - - - {model.type === 'parent' && model.isStudio && ( - - )} - {isSessionCommand && ( - - )} - {!model.visible && ( - - - - - - )} - {displayNumOfElements && ( - - )} - - - - {displayNumOfChildren && ( - - )} - - -
-
+
this._snapshot(true)} + onMouseLeave={() => this._snapshot(false)} + > + {groupPlaceholder} + +
+ + )} {this._children()} @@ -481,15 +503,6 @@ class Command extends Component { }, 50) } } - - _removeStudioCommand = (e: React.MouseEvent) => { - e.preventDefault() - e.stopPropagation() - - const { model, events } = this.props - - events.emit('studio:remove:command', model.number) - } } export { Aliases, AliasesReferences, Message, Progress } diff --git a/packages/reporter/src/commands/commands.scss b/packages/reporter/src/commands/commands.scss index 1da5f6e9a8d7..4607bdc05769 100644 --- a/packages/reporter/src/commands/commands.scss +++ b/packages/reporter/src/commands/commands.scss @@ -282,7 +282,7 @@ padding: 0; border: 0; margin: 0; - margin-bottom: 5px; + display: inline-grid; } // Custom Styles for Specific Commands diff --git a/packages/reporter/src/errors/errors.scss b/packages/reporter/src/errors/errors.scss index 2184e69d41f6..a02041d9b51c 100644 --- a/packages/reporter/src/errors/errors.scss +++ b/packages/reporter/src/errors/errors.scss @@ -65,6 +65,7 @@ $code-border-radius: 4px; clear: both; color: $err-text; font-family: $monospace; + font-style: normal; margin-bottom: 0; margin-top: 5px; white-space: pre-wrap; @@ -74,7 +75,7 @@ $code-border-radius: 4px; } .runnable-err-header { - background-color: rgba($red-400, 0.05); + background-color: $red-300, 0.05; display: flex; justify-content: space-between; padding: 5px 10px; @@ -87,6 +88,7 @@ $code-border-radius: 4px; color: $err-header-text; svg { + color: $red-400; margin-right: 10px; } } @@ -99,14 +101,16 @@ $code-border-radius: 4px; } .runnable-err-message { - font-family: $monospace; - font-size: 1em; + font-family: $font-sans; + font-size: 14px; + font-weight: 400; padding: 10px; code { background-color: rgba($black, 0.2); border-radius: 4px; color: $err-code-text; + font-family: $font-sans; padding: 2px 5px; } @@ -184,10 +188,11 @@ $code-border-radius: 4px; div { color: $red-300; cursor: pointer; + font-family: $font-sans; font-size: 14px; font-weight: 500; height: 100%; - padding: 14px 10px; + padding: 16px 10px; width: 100%; &:focus { From 159e4adac1e7b0fea6cd56914be9a9cd4306b031 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Thu, 22 Sep 2022 14:40:53 -0500 Subject: [PATCH 02/52] [skip ci] --- .husky/pre-commit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.husky/pre-commit b/.husky/pre-commit index 36af219892fd..9f5c7d244218 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,4 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" -npx lint-staged +# npx lint-staged From fa44f35a24624f360ffda1ca49a8ee6900cb06c9 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Fri, 23 Sep 2022 13:36:28 -0500 Subject: [PATCH 03/52] some progress (branch appears out-of-date) --- npm/cypress-schematic/src/ct.spec.ts | 3 +- npm/cypress-schematic/src/e2e.spec.ts | 3 +- .../app/cypress/e2e/runner/sessions.ui.cy.ts | 68 +++++++++++++++++++ .../e2e/commands/sessions/sessions.cy.js | 9 ++- .../driver/src/cy/commands/sessions/index.ts | 14 +++- packages/driver/src/cypress/command_queue.ts | 6 +- .../cypress/e2e/session/session_errors.cy.js | 17 +++++ 7 files changed, 111 insertions(+), 9 deletions(-) create mode 100644 system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/session_errors.cy.js diff --git a/npm/cypress-schematic/src/ct.spec.ts b/npm/cypress-schematic/src/ct.spec.ts index 9cf4511c3ac9..d321330b61fa 100644 --- a/npm/cypress-schematic/src/ct.spec.ts +++ b/npm/cypress-schematic/src/ct.spec.ts @@ -41,13 +41,14 @@ describe('ng add @cypress/schematic / e2e and ct', function () { this.timeout(1000 * 60 * 4) for (const project of ANGULAR_PROJECTS) { - it('should install ct files with option and no component specs', async () => { + it('should install ct files with option and no component specs', async (done) => { const projectPath = await scaffoldAngularProject(project) await runCommandInProject(`yarn add @cypress/schematic@file:${cypressSchematicPackagePath}`, projectPath) await runCommandInProject('yarn ng add @cypress/schematic --e2e --component', projectPath) await copyAngularMount(projectPath) await runCommandInProject('yarn ng run angular:ct --watch false --spec src/app/app.component.cy.ts', projectPath) + done() }) } }) diff --git a/npm/cypress-schematic/src/e2e.spec.ts b/npm/cypress-schematic/src/e2e.spec.ts index add1d333e549..25378001aa9e 100644 --- a/npm/cypress-schematic/src/e2e.spec.ts +++ b/npm/cypress-schematic/src/e2e.spec.ts @@ -30,12 +30,13 @@ describe('ng add @cypress/schematic / only e2e', function () { this.timeout(1000 * 60 * 4) for (const project of ANGULAR_PROJECTS) { - it('should install e2e files by default', async () => { + it('should install e2e files by default', async (done) => { const projectPath = await scaffoldAngularProject(project) await runCommandInProject(`yarn add @cypress/schematic@file:${cypressSchematicPackagePath}`, projectPath) await runCommandInProject('yarn ng add @cypress/schematic --e2e --component false --add-ct-specs false', projectPath) await runCommandInProject('yarn ng e2e --watch false', projectPath) + done() }) } }) diff --git a/packages/app/cypress/e2e/runner/sessions.ui.cy.ts b/packages/app/cypress/e2e/runner/sessions.ui.cy.ts index d3205b09b061..164109ea23d1 100644 --- a/packages/app/cypress/e2e/runner/sessions.ui.cy.ts +++ b/packages/app/cypress/e2e/runner/sessions.ui.cy.ts @@ -326,6 +326,74 @@ describe('runner/cypress sessions.ui.spec', { validateSessionsInstrumentPanel(['user1', 'user2']) // cy.percySnapshot() // TODO: restore when Percy CSS is fixed. See https://github.com/cypress-io/cypress/issues/23435 }) + + describe('errors', () => { + it('test error when setup has failing Cypress command', () => { + cy.contains('.test', 'fails to create with failing command').as('test') + // test marked as failed + // test is expanded + // cy.get('test') + + // session is marked as 'failed' + // setup group is expanded + // has error + cy.get('@test').within(() => { + cy.get('.command-name-session').should('contain', 'session_1') + .find('.reporter-tag').should('contain', 'failed') + }) + }) + + describe('created session failed validation', () => { + it('has test error when validate returned false', () => { + // test marked as failed + // test is expanded + // session is marked as 'failed' + // validates group is expanded + }) + + it('test error when validate resolved false', () => { + // test marked as failed + // test is expanded + // session is marked as 'failed' + // validates group is expanded + }) + + it('test error when validate rejected with false', () => { + // test marked as failed + // test is expanded + // session is marked as 'failed' + // validates group is expanded + }) + + it('test error when validate threw error', () => { + // test marked as failed + // test is expanded + // session is marked as 'failed' + // validates group is expanded + }) + + it('test error when validate has failing Cypress command', () => { + // test marked as failed + // test is expanded + // session is marked as 'failed' + // validates group is expanded + }) + }) + + describe('restored session fails validation and session is recreated', () => { + it('has inline error where validate returned false', () => { + // test marked as passed + // test is collapsed + // session is marked as 'successful' + // validates group is collapsed + }) + + it('has inline error where validate resolved false') + it('has inline error where validate rejected with false') + it('has inline error where validate threw error') + it('has inline error where validate has failing Cypress command') + }) + }) }) describe('runner/cypress sessions.open_mode.spec', () => { diff --git a/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js b/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js index 31cad336ced5..3a8f2d3b2d38 100644 --- a/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js +++ b/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js @@ -249,11 +249,14 @@ describe('cy.session', { retries: 0 }, () => { setupTestContext() cy.log('Creating new session with validation to test against') - cy.session(`session-${Cypress.state('test').id}`, setup, { validate }) + // cy.session(`session-${Cypress.state('test').id}`, setup, { validate }) + cy.session(`session-${Cypress.state('test').id}`, () => { + cy.get('does_not_exist') + }) }) // test must be first to run before blank page visit between each test - it('does not clear page visit from validate function', () => { + it.only('does not clear page visit from validate function', () => { cy.url().should('contain', '/fixtures/auth/index.html') }) @@ -637,7 +640,7 @@ describe('cy.session', { retries: 0 }, () => { }) describe('recreates existing session with failed validation flow', () => { - it.only('fails to recreate session and logs correctly', function () { + it('fails to recreate session and logs correctly', function () { setupTestContext() cy.log('Creating new session for test') cy.session(`session-${Cypress.state('test').id}`, setup, { validate }) diff --git a/packages/driver/src/cy/commands/sessions/index.ts b/packages/driver/src/cy/commands/sessions/index.ts index 05b5ddc43ece..f644c119baab 100644 --- a/packages/driver/src/cy/commands/sessions/index.ts +++ b/packages/driver/src/cy/commands/sessions/index.ts @@ -76,6 +76,7 @@ export default function (Commands, Cypress, cy) { const validOpts = { 'validate': 'function', + 'cacheAcrossSpecs': 'boolean', } Object.entries(options).forEach(([key, value]) => { @@ -139,6 +140,15 @@ export default function (Commands, Cypress, cy) { type: 'system', }, () => { return cy.then(async () => { + // catch when a cypress command fails in the validate callback to move the queue index + cy.state('onCommandFailed', (err, queue, next) => { + cy.state('onCommandFailed', null) + console.log('onCommandFailed', err) + setSessionLogStatus('failed') + + return false + }) + return existingSession.setup() }) .then(async () => { @@ -210,9 +220,7 @@ export default function (Commands, Cypress, cy) { setSessionLogStatus('failed') $errUtils.modifyErrMsg(err, `\n\nThis error occurred while validating the ${sessionStatus} session. Because validation failed immediately after creating the session, we failed the test.`, _.add) - return cy.then(() => { - cy.fail(err) - }) + return cy.fail(err) } return validate(existingSession, sessionStatus, onSuccess, onFail) diff --git a/packages/driver/src/cypress/command_queue.ts b/packages/driver/src/cypress/command_queue.ts index 45e04ededde0..2d91fa64f8db 100644 --- a/packages/driver/src/cypress/command_queue.ts +++ b/packages/driver/src/cypress/command_queue.ts @@ -378,7 +378,11 @@ export class CommandQueue extends Queue<$Command> { console.log('onError') if (this.state('onCommandFailed')) { - return this.state('onCommandFailed')(err, this, next, commandRunningFailed) + const handledError = this.state('onCommandFailed')(err, this, next, commandRunningFailed) + + if (handledError) { + return + } } debugErrors('caught error in promise chain: %o', err) diff --git a/system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/session_errors.cy.js b/system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/session_errors.cy.js new file mode 100644 index 000000000000..ef34ca440c2d --- /dev/null +++ b/system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/session_errors.cy.js @@ -0,0 +1,17 @@ +it('fails to create with failing command', () => { + cy.once(() => { + if (Cypress.env('SYSTEM_TESTS')) { + cy.get(top.document).within(() => { + cy.contains('.test', 'fails to create with failing command').as('test').click() + cy.get('@test').within(() => { + cy.get('.command-name-session').should('contain', 'session_1') + .find('.reporter-tag').should('contain', 'failed') + }) + }) + } + }) + + cy.session('session_1', () => { + cy.get('does_not_exist') + }) +}) From 1d3584d560a9f6af1f48e34df55672a8cc849acc Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Fri, 23 Sep 2022 15:53:36 -0500 Subject: [PATCH 04/52] Correctly handle errors thrown from cypress commands in session setup function. --- npm/cypress-schematic/src/ct.spec.ts | 3 +- npm/cypress-schematic/src/e2e.spec.ts | 3 +- .../app/cypress/e2e/runner/sessions.ui.cy.ts | 37 +++++++++++++------ .../e2e/commands/sessions/sessions.cy.js | 16 +++++++- .../driver/src/cy/commands/sessions/index.ts | 14 ++++--- packages/driver/src/cypress/command_queue.ts | 2 + packages/driver/src/cypress/state.ts | 2 +- packages/reporter/src/commands/command.tsx | 9 +++-- packages/reporter/src/commands/commands.scss | 6 ++- .../cypress/e2e/session/session_errors.cy.js | 16 +------- 10 files changed, 67 insertions(+), 41 deletions(-) diff --git a/npm/cypress-schematic/src/ct.spec.ts b/npm/cypress-schematic/src/ct.spec.ts index d321330b61fa..9cf4511c3ac9 100644 --- a/npm/cypress-schematic/src/ct.spec.ts +++ b/npm/cypress-schematic/src/ct.spec.ts @@ -41,14 +41,13 @@ describe('ng add @cypress/schematic / e2e and ct', function () { this.timeout(1000 * 60 * 4) for (const project of ANGULAR_PROJECTS) { - it('should install ct files with option and no component specs', async (done) => { + it('should install ct files with option and no component specs', async () => { const projectPath = await scaffoldAngularProject(project) await runCommandInProject(`yarn add @cypress/schematic@file:${cypressSchematicPackagePath}`, projectPath) await runCommandInProject('yarn ng add @cypress/schematic --e2e --component', projectPath) await copyAngularMount(projectPath) await runCommandInProject('yarn ng run angular:ct --watch false --spec src/app/app.component.cy.ts', projectPath) - done() }) } }) diff --git a/npm/cypress-schematic/src/e2e.spec.ts b/npm/cypress-schematic/src/e2e.spec.ts index 25378001aa9e..add1d333e549 100644 --- a/npm/cypress-schematic/src/e2e.spec.ts +++ b/npm/cypress-schematic/src/e2e.spec.ts @@ -30,13 +30,12 @@ describe('ng add @cypress/schematic / only e2e', function () { this.timeout(1000 * 60 * 4) for (const project of ANGULAR_PROJECTS) { - it('should install e2e files by default', async (done) => { + it('should install e2e files by default', async () => { const projectPath = await scaffoldAngularProject(project) await runCommandInProject(`yarn add @cypress/schematic@file:${cypressSchematicPackagePath}`, projectPath) await runCommandInProject('yarn ng add @cypress/schematic --e2e --component false --add-ct-specs false', projectPath) await runCommandInProject('yarn ng e2e --watch false', projectPath) - done() }) } }) diff --git a/packages/app/cypress/e2e/runner/sessions.ui.cy.ts b/packages/app/cypress/e2e/runner/sessions.ui.cy.ts index 164109ea23d1..a73fd44dff48 100644 --- a/packages/app/cypress/e2e/runner/sessions.ui.cy.ts +++ b/packages/app/cypress/e2e/runner/sessions.ui.cy.ts @@ -329,18 +329,33 @@ describe('runner/cypress sessions.ui.spec', { describe('errors', () => { it('test error when setup has failing Cypress command', () => { - cy.contains('.test', 'fails to create with failing command').as('test') - // test marked as failed - // test is expanded - // cy.get('test') - - // session is marked as 'failed' - // setup group is expanded - // has error - cy.get('@test').within(() => { - cy.get('.command-name-session').should('contain', 'session_1') - .find('.reporter-tag').should('contain', 'failed') + loadSpec({ + projectName: 'session-and-origin-e2e-specs', + filePath: 'session/session_errors.cy.js', + failCount: 1, + }) + + 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') + + cy.get('@session_command') + .children('.command-child-container').should('exist') + .within(() => { + cy.get('.command-name-session') + .should('contain', 'Create new session') + .get('.command-child-container').should('exist') + }) }) + + // // has error + cy.get('@setup_failed').contains('This error occurred while creating session. Because the session setup failed, we failed the test.') }) describe('created session failed validation', () => { diff --git a/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js b/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js index fb3e3356db4f..1240d134e61b 100644 --- a/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js +++ b/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js @@ -786,7 +786,7 @@ describe('cy.session', { retries: 0 }, () => { beforeEach(() => { cy.on('log:added', (attrs, log) => { - if (attrs.name === 'session') { + if (attrs.name === 'session' || (attrs.name === 'get' && attrs.message === '#does_not_exist')) { lastLog = log logs.push(log) } @@ -914,6 +914,20 @@ describe('cy.session', { retries: 0 }, () => { cy.session('some-session') }) + it('throws when setup function has a failing cypress command', function (done) { + 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.') + + done() + }) + + cy.session(`session-${Cypress.state('test').id}`, () => { + cy.get('#does_not_exist', { timeout: 500 }) + }) + }) + it('throws when multiple session calls with same sessionId but different options', function (done) { cy.on('fail', async (err) => { expect(lastLog.get('error')).to.eq(err) diff --git a/packages/driver/src/cy/commands/sessions/index.ts b/packages/driver/src/cy/commands/sessions/index.ts index 1ffa5f55c282..61665189fa58 100644 --- a/packages/driver/src/cy/commands/sessions/index.ts +++ b/packages/driver/src/cy/commands/sessions/index.ts @@ -134,14 +134,16 @@ export default function (Commands, Cypress, cy) { displayName: recreateSession ? 'Recreate session' : 'Create new session', message: '', type: 'system', - }, () => { + }, (createLog) => { return cy.then(async () => { - // catch when a cypress command fails in the validate callback to move the queue index + // 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, queue, next) => { - cy.state('onCommandFailed', null) - console.log('onCommandFailed', err) + createLog.set({ state: 'failed' }) setSessionLogStatus('failed') + $errUtils.modifyErrMsg(err, `\n\nThis error occurred while creating session. Because the session setup failed, we failed the test.`, _.add) + return false }) @@ -343,7 +345,7 @@ export default function (Commands, Cypress, cy) { */ const createSessionWorkflow = (existingSession, sessionStatus: 'creating' | 'recreating' = 'creating') => { return cy.then(async () => { - setSessionLogStatus(recreateSession ? 'recreating' : 'creating') + setSessionLogStatus(sessionStatus ? 'recreating' : 'creating') await navigateAboutBlank() await sessions.clearCurrentSessionData() @@ -356,7 +358,7 @@ export default function (Commands, Cypress, cy) { return } - setSessionLogStatus(recreateSession ? 'recreated' : 'created') + setSessionLogStatus(sessionStatus ? 'recreated' : 'created') }) } diff --git a/packages/driver/src/cypress/command_queue.ts b/packages/driver/src/cypress/command_queue.ts index 2d91fa64f8db..9435c996a454 100644 --- a/packages/driver/src/cypress/command_queue.ts +++ b/packages/driver/src/cypress/command_queue.ts @@ -380,6 +380,8 @@ export class CommandQueue extends Queue<$Command> { if (this.state('onCommandFailed')) { const handledError = this.state('onCommandFailed')(err, this, next, commandRunningFailed) + cy.state('onCommandFailed', null) + if (handledError) { return } diff --git a/packages/driver/src/cypress/state.ts b/packages/driver/src/cypress/state.ts index 853d7c9dc368..fd44c8b2d38f 100644 --- a/packages/driver/src/cypress/state.ts +++ b/packages/driver/src/cypress/state.ts @@ -53,7 +53,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, next: any) => boolean): (err: Error | string, queue: any, next: any, onCommandFailed: () => void) => boolean + (k: 'onCommandFailed', v?: (err: any, queue: any, next: any) => boolean): (err: Error | string, queue: any, next: any, onCommandFailed: () => boolean) => boolean (k: 'promise', v?: Bluebird): Bluebird (k: 'reject', v?: (err: any) => any): (err: any) => any (k: 'cancel', v?: () => void): () => void diff --git a/packages/reporter/src/commands/command.tsx b/packages/reporter/src/commands/command.tsx index d551f7ce7829..c73b22450268 100644 --- a/packages/reporter/src/commands/command.tsx +++ b/packages/reporter/src/commands/command.tsx @@ -9,6 +9,7 @@ import Tooltip from '@cypress/react-tooltip' import appState, { AppState } from '../lib/app-state' import events, { Events } from '../lib/events' import FlashOnClick from '../lib/flash-on-click' +import StateIcon from '../lib/state-icon' import Tag from '../lib/tag' import { TimeoutID } from '../lib/types' import runnablesStore, { RunnablesStore } from '../runnables/runnables-store' @@ -257,7 +258,8 @@ interface Props { runnablesStore: RunnablesStore groupId?: number } -const CommandDetails = ({ model, events, aliasesWithDuplicates }) => { + +const CommandDetails = observer(({ model, groupId, events, aliasesWithDuplicates }) => { const commandName = model.name ? nameClassName(model.name) : '' const displayNumOfElements = model.state !== 'pending' && model.numElements != null && model.numElements !== 1 const isSystemEvent = model.type === 'system' && model.event @@ -279,6 +281,7 @@ const CommandDetails = ({ model, events, aliasesWithDuplicates }) => { {model.event && model.type !== 'system' ? `(${displayName(model)})` : displayName(model)} + {!!groupId && model.type === 'system' && model.state === 'failed' && } {model.referencesAlias ? : @@ -327,7 +330,7 @@ const CommandDetails = ({ model, events, aliasesWithDuplicates }) => { ) -} +}) @observer class Command extends Component { @@ -403,7 +406,7 @@ class Command extends Component { onMouseLeave={() => this._snapshot(false)} > {groupPlaceholder} - + )} diff --git a/packages/reporter/src/commands/commands.scss b/packages/reporter/src/commands/commands.scss index 4607bdc05769..2f4f75845df9 100644 --- a/packages/reporter/src/commands/commands.scss +++ b/packages/reporter/src/commands/commands.scss @@ -255,7 +255,7 @@ &:not(.command-type-system) { border-left: 2px solid $fail; background-color: $err-header-background; - + &.command-is-interactive:hover { background: rgba($red-400, 0.3); } @@ -267,6 +267,10 @@ color: $err-header-text; } + .failed-indicator { + vertical-align: middle; + } + .command-group { border-color: $err-header-text; @include nested-command-dashes($err-header-text); diff --git a/system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/session_errors.cy.js b/system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/session_errors.cy.js index ef34ca440c2d..35194187c189 100644 --- a/system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/session_errors.cy.js +++ b/system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/session_errors.cy.js @@ -1,17 +1,5 @@ -it('fails to create with failing command', () => { - cy.once(() => { - if (Cypress.env('SYSTEM_TESTS')) { - cy.get(top.document).within(() => { - cy.contains('.test', 'fails to create with failing command').as('test').click() - cy.get('@test').within(() => { - cy.get('.command-name-session').should('contain', 'session_1') - .find('.reporter-tag').should('contain', 'failed') - }) - }) - } - }) - +it('setup has failing command', () => { cy.session('session_1', () => { - cy.get('does_not_exist') + cy.get('does_not_exist', { timeout: 500 }) }) }) From 4dc9e6b3cd8f88785fdfe44022dbd2c72cbe9d27 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Fri, 23 Sep 2022 16:12:30 -0500 Subject: [PATCH 05/52] more granular error tests --- .../e2e/commands/sessions/sessions.cy.js | 108 +++++++++++------- 1 file changed, 69 insertions(+), 39 deletions(-) diff --git a/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js b/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js index 1240d134e61b..570908ed5ff0 100644 --- a/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js +++ b/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js @@ -782,14 +782,18 @@ describe('cy.session', { retries: 0 }, () => { describe('errors', () => { let lastLog = null - let logs = [] + let lastSessionLog = null + const handleAddLog = (attrs, log) => { + lastLog = log + if (attrs.name === 'session') { + lastSessionLog = log + } + } beforeEach(() => { - cy.on('log:added', (attrs, log) => { - if (attrs.name === 'session' || (attrs.name === 'get' && attrs.message === '#does_not_exist')) { - lastLog = log - logs.push(log) - } + cy.on('log:added', handleAddLog) + cy.on('fail', (err) => { + cy.off('log:added', handleAddLog) }) return null @@ -797,8 +801,9 @@ describe('cy.session', { retries: 0 }, () => { it('throws error when experimentalSessionAndOrigin not enabled', { experimentalSessionAndOrigin: false, experimentalSessionSupport: false }, (done) => { cy.on('fail', (err) => { - expect(lastLog.get('error')).to.eq(err) - expect(lastLog.get('state')).to.eq('failed') + expect(lastSessionLog).to.eq(lastLog) + expect(lastSessionLog.get('error')).to.eq(err) + expect(lastSessionLog.get('state')).to.eq('failed') expect(err.message).to.eq('`cy.session()` requires enabling the `experimentalSessionAndOrigin` flag.') expect(err.docsUrl).to.eq('https://on.cypress.io/session') @@ -810,8 +815,9 @@ describe('cy.session', { retries: 0 }, () => { it('throws error when experimentalSessionSupport is enabled through test config', { experimentalSessionAndOrigin: false, experimentalSessionSupport: true }, (done) => { cy.on('fail', (err) => { - expect(lastLog.get('error')).to.eq(err) - expect(lastLog.get('state')).to.eq('failed') + expect(lastSessionLog).to.eq(lastLog) + expect(lastSessionLog.get('error')).to.eq(err) + expect(lastSessionLog.get('state')).to.eq('failed') expect(err.message).to.eq('\`cy.session()\` requires enabling the \`experimentalSessionAndOrigin\` flag. The \`experimentalSessionSupport\` flag was enabled but was removed in Cypress version 9.6.0.') expect(err.docsUrl).to.eq('https://on.cypress.io/session') @@ -826,6 +832,7 @@ describe('cy.session', { retries: 0 }, () => { cy.on('fail', (err) => { Cypress.config('experimentalSessionSupport', false) + 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()\` requires enabling the \`experimentalSessionAndOrigin\` flag. The \`experimentalSessionSupport\` flag was enabled but was removed in Cypress version 9.6.0.') @@ -838,6 +845,7 @@ describe('cy.session', { retries: 0 }, () => { it('throws when sessionId argument was not provided', function (done) { cy.on('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 first argument `id` must be an string or serializable object.') @@ -851,6 +859,7 @@ describe('cy.session', { retries: 0 }, () => { it('throws when sessionId argument is not an object', function (done) { cy.on('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 first argument `id` must be an string or serializable object.') @@ -864,6 +873,7 @@ describe('cy.session', { retries: 0 }, () => { it('throws when options argument is provided and is not an object', function (done) { cy.on('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.') @@ -877,6 +887,7 @@ describe('cy.session', { retries: 0 }, () => { it('throws when options argument has an invalid option', function (done) { cy.on('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 option: **invalid_key**\nAvailable options are: `validate`') @@ -890,6 +901,7 @@ describe('cy.session', { retries: 0 }, () => { it('throws when options argument has an option with an invalid type', function (done) { cy.on('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 option value. **validate** must be of type **function** but was **number**.') @@ -901,35 +913,9 @@ describe('cy.session', { retries: 0 }, () => { cy.session('some-session', () => {}, { validate: 2 }) }) - it('throws when setup function is not provided and existing session is not found', function (done) { - cy.on('fail', (err) => { - expect(lastLog.get('error')).to.eq(err) - expect(lastLog.get('state')).to.eq('failed') - expect(err.message).to.eq('No session is defined with the name\n **some-session**\nIn order to use `cy.session()`, provide a `setup` as the second argument:\n\n`cy.session(id, setup)`') - expect(err.docsUrl).to.eq('https://on.cypress.io/session') - - done() - }) - - cy.session('some-session') - }) - - it('throws when setup function has a failing cypress command', function (done) { - 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.') - - done() - }) - - cy.session(`session-${Cypress.state('test').id}`, () => { - cy.get('#does_not_exist', { timeout: 500 }) - }) - }) - it('throws when multiple session calls with same sessionId but different options', function (done) { - cy.on('fail', async (err) => { + cy.once('fail', async (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('You may not call `cy.session()` with a previously used name and different options. If you want to specify different options, please use a unique name other than **duplicate-session**.') @@ -953,7 +939,51 @@ describe('cy.session', { retries: 0 }, () => { }) }) - describe('options.validate failures', () => { + describe('setup function failures', () => { + it('throws when setup function is not provided and existing session is not found', function (done) { + cy.on('fail', (err) => { + expect(lastLog.get('error')).to.eq(err) + expect(lastLog.get('state')).to.eq('failed') + expect(err.message).to.eq('No session is defined with the name\n **some-session**\nIn order to use `cy.session()`, provide a `setup` as the second argument:\n\n`cy.session(id, setup)`') + expect(err.docsUrl).to.eq('https://on.cypress.io/session') + + done() + }) + + cy.session('some-session') + }) + + it('throws when setup function has a failing Cypress command', function (done) { + 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(lastSessionLog.get('state')).to.eq('failed') + done() + }) + + cy.session(`session-${Cypress.state('test').id}`, () => { + cy.get('#does_not_exist', { timeout: 500 }) + }) + }) + + it('throws when setup function has a failing assertion', function (done) { + 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(lastSessionLog.get('state')).to.eq('failed') + + done() + }) + + cy.session(`session-${Cypress.state('test').id}`, () => { + expect(true).to.be.false + }) + }) + }) + + describe.skip('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.' it('throws when options.validate has a failing Cypress command', (done) => { From b10cdc48631abeac759899e26055e4bf6c6500da Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Wed, 12 Oct 2022 09:18:23 -0500 Subject: [PATCH 06/52] Fix missed merge conflict & code check-in. --- .../app/cypress/e2e/runner/sessions.ui.cy.ts | 2 +- .../e2e/commands/sessions/sessions.cy.js | 55 ++++++++---- .../driver/src/cy/commands/sessions/index.ts | 5 +- packages/driver/src/cypress/command_queue.ts | 1 - packages/driver/src/cypress/error_utils.ts | 2 +- packages/driver/src/cypress/log.ts | 26 +++--- packages/reporter/cypress/e2e/commands.cy.ts | 2 +- .../reporter/src/attempts/attempt-model.ts | 8 +- packages/reporter/src/attempts/attempts.tsx | 6 +- .../reporter/src/commands/command-model.ts | 5 +- packages/reporter/src/commands/command.tsx | 84 ++++++++++++------- packages/reporter/src/commands/commands.scss | 13 +-- packages/reporter/src/errors/err-model.ts | 8 ++ packages/reporter/src/errors/errors.scss | 42 ++++++---- packages/reporter/src/errors/test-error.tsx | 11 ++- packages/reporter/src/lib/variables.scss | 10 +-- .../cypress/e2e/session/session_errors.cy.js | 8 ++ 17 files changed, 176 insertions(+), 112 deletions(-) diff --git a/packages/app/cypress/e2e/runner/sessions.ui.cy.ts b/packages/app/cypress/e2e/runner/sessions.ui.cy.ts index c7990c5d90b3..c468d9cef71b 100644 --- a/packages/app/cypress/e2e/runner/sessions.ui.cy.ts +++ b/packages/app/cypress/e2e/runner/sessions.ui.cy.ts @@ -332,7 +332,7 @@ describe('runner/cypress sessions.ui.spec', { }) describe('errors', () => { - it('test error when setup has failing Cypress command', () => { + it.only('test error when setup has failing Cypress command', () => { loadSpec({ projectName: 'session-and-origin-e2e-specs', filePath: 'session/errors.cy.js', diff --git a/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js b/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js index 568264ebfd1d..6c23c6972b97 100644 --- a/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js +++ b/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js @@ -253,20 +253,43 @@ describe('cy.session', { retries: 0 }, () => { describe('create session with validation flow', () => { let sessionId - before(() => { - setupTestContext() - cy.log('Creating new session with validation to test against') - - // cy.session(`session-${Cypress.state('test').id}`, setup, { validate }) - cy.session(`session-${Cypress.state('test').id}`, () => { - cy.get('does_not_exist') - }) + // before(() => { + // setupTestContext() + // cy.log('Creating new session with validation to test against') + + // // cy.session(`session-${Cypress.state('test').id}`, setup, { validate }) + // cy.session(`session-${Cypress.state('test').id}`, () => { + // cy.log('do setup') + // }, { validate: () => { + // expect(true).to.be.false + // } }) + // }) + it.only('try', () => { + expect('true').to.be.true }) - // test must be first to run before blank page visit between each test - it.only('does not clear page visit from validate function', () => { - cy.url().should('contain', '/fixtures/auth/index.html') - }) + // it.only('try', () => { + // cy.get('body').within(() => { + // cy.get('div').within(() => { + // expect('true').to.be.true + // }) + // }) + // }) + + // // test must be first to run before blank page visit between each test + // it.only('does not clear page visit from validate function', () => { + // setupTestContext() + // cy.log('Creating new session with validation to test against') + + // // cy.session(`session-${Cypress.state('test').id}`, setup, { validate }) + // cy.session(`session-${Cypress.state('test').id}`, () => { + // cy.log('do setup') + // }, { validate: () => { + // expect(true).to.be.false + // } }) + + // cy.url().should('contain', '/fixtures/auth/index.html') + // }) it('successfully creates new session and validates it', () => { expect(setup).to.be.calledOnce @@ -391,7 +414,7 @@ describe('cy.session', { retries: 0 }, () => { validate.callsFake(() => false) cy.session(`session-${Cypress.state('test').id}`, setup, { validate: () => { - cy.get('does_not_exist') + cy.get('does_not_exist', { timeout: 500 }) } }) }) }) @@ -559,7 +582,7 @@ describe('cy.session', { retries: 0 }, () => { }) // test must be first to run before blank page visit between each test - it('does not clear page visit from validate function', () => { + it.only('does not clear page visit from validate function', () => { cy.url().should('contain', '/fixtures/auth/index.html') }) @@ -615,7 +638,7 @@ describe('cy.session', { retries: 0 }, () => { }) expect(logs[6].get()).to.deep.contain({ - showError: true, + showRecoveredError: true, group: validateSessionGroup.id, }) @@ -728,7 +751,7 @@ describe('cy.session', { retries: 0 }, () => { // }) // expect(logs[6].get()).to.deep.contain({ - // showError: true, + // showRecoveredError: true, // group: validateSessionGroup.id, // }) diff --git a/packages/driver/src/cy/commands/sessions/index.ts b/packages/driver/src/cy/commands/sessions/index.ts index c1d05ea5a1d4..aeef26c4477f 100644 --- a/packages/driver/src/cy/commands/sessions/index.ts +++ b/packages/driver/src/cy/commands/sessions/index.ts @@ -242,12 +242,13 @@ export default function (Commands, Cypress, cy) { if (sessionStatus === 'restored') { $errUtils.modifyErrMsg(err, `\n\nThis error occurred while validating the restored session. Because validation failed, we will try to recreate the session.`, _.add) - const current = cy.state('current') + err.showRecoveredError = true Cypress.log({ - showError: true, type: 'system', name: 'session', + displayName: 'validate result', + message: '', }) .error(err) diff --git a/packages/driver/src/cypress/command_queue.ts b/packages/driver/src/cypress/command_queue.ts index 576c0a4273b9..26a23d8ee02f 100644 --- a/packages/driver/src/cypress/command_queue.ts +++ b/packages/driver/src/cypress/command_queue.ts @@ -36,7 +36,6 @@ const commandRunningFailed = (Cypress, state, err) => { end: true, snapshot: true, error: err, - // showError: true, consoleProps () { if (!current) return diff --git a/packages/driver/src/cypress/error_utils.ts b/packages/driver/src/cypress/error_utils.ts index cbbed199a3a5..ed7609f285b1 100644 --- a/packages/driver/src/cypress/error_utils.ts +++ b/packages/driver/src/cypress/error_utils.ts @@ -9,7 +9,7 @@ import $stackUtils, { StackAndCodeFrameIndex } from './stack_utils' import $utils from './utils' import type { HandlerType } from './runner' -const ERROR_PROPS = 'message type name stack parsedStack fileName lineNumber columnNumber host uncaught actual expected showDiff isPending docsUrl codeFrame'.split(' ') +const ERROR_PROPS = 'message type name stack parsedStack fileName lineNumber columnNumber host uncaught actual expected showDiff isPending docsUrl codeFrame showRecoveredError'.split(' ') const ERR_PREPARED_FOR_SERIALIZATION = Symbol('ERR_PREPARED_FOR_SERIALIZATION') const crossOriginScriptRe = /^script error/i diff --git a/packages/driver/src/cypress/log.ts b/packages/driver/src/cypress/log.ts index 564002f9bb9c..8c52e9775b8e 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' @@ -15,7 +14,7 @@ import type { StateFunc } from './state' const groupsOrTableRe = /^(groups|table)$/ const parentOrChildRe = /parent|child|system/ const SNAPSHOT_PROPS = 'id snapshots $el url coords highlightAttr scrollBy viewportWidth viewportHeight'.split(' ') -const DISPLAY_PROPS = 'id alias aliasType callCount displayName end err event functionName groupLevel hookId instrument isStubbed group message method name numElements showError numResponses referencesAlias renderProps sessionInfo state testId timeout type url visible wallClockStartedAt testCurrentRetry'.split(' ') +const DISPLAY_PROPS = 'id alias aliasType callCount displayName end err event functionName groupLevel hookId instrument isStubbed group message method name numElements showRecoveredError numResponses referencesAlias renderProps sessionInfo state testId timeout type url visible wallClockStartedAt testCurrentRetry'.split(' ') const BLACKLIST_PROPS = 'snapshots'.split(' ') let counter = 0 @@ -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,11 +398,14 @@ 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() } + console.log('err', err) + console.log('err', err.showRecoveredError) + this.set({ ended: true, error: err, @@ -556,9 +556,10 @@ export class Log { } class LogManager { - logs: Record = {} + logs: Record = {} - constructor () { + constructor (isInteractive: boolean) { + this.isInteractive = isInteractive this.fireChangeEvent = this.fireChangeEvent.bind(this) } @@ -575,13 +576,12 @@ class LogManager { const attrs = log.toJSON() + console.log('trigger', attrs) // only trigger this event if our last stored // emitted attrs do not match the current toJSON if (!_.isEqual(log._emittedAttrs, attrs)) { log._emittedAttrs = attrs - log.emit(event, attrs) - return Cypress.action(event, attrs, log) } } @@ -604,6 +604,11 @@ class LogManager { createLogFn (cy, state, config) { return (options: any = {}) => { + // if (!this.isInteractive) { + // // if (_skipCollectingLogs || !this.isInteractive) { + // return + // } + if (!_.isObject(options)) { $errUtils.throwErrByPath('log.invalid_argument', { args: { arg: options } }) } @@ -641,7 +646,6 @@ class LogManager { log.wrapConsoleProps() this.addToLogs(log) - if (options.emitOnly) { return } @@ -660,7 +664,7 @@ class LogManager { export function create (Cypress, cy, state, config) { counter = 0 - const logManager = new LogManager() + const logManager = new LogManager(Cypress.state('isInteractive')) return logManager.createLogFn(cy, state, config) } diff --git a/packages/reporter/cypress/e2e/commands.cy.ts b/packages/reporter/cypress/e2e/commands.cy.ts index 84bdbeb0a4bd..a605a1ea63c0 100644 --- a/packages/reporter/cypress/e2e/commands.cy.ts +++ b/packages/reporter/cypress/e2e/commands.cy.ts @@ -937,7 +937,7 @@ describe('commands', { viewportHeight: 1000 }, () => { name: 'validate', displayMessage: 'mock session validation', state: 'failed', - showError: true, + showRecoveredError: true, err: _commandErr, type: 'parent', }) diff --git a/packages/reporter/src/attempts/attempt-model.ts b/packages/reporter/src/attempts/attempt-model.ts index ca6a910a5373..61f91f30e7b3 100644 --- a/packages/reporter/src/attempts/attempt-model.ts +++ b/packages/reporter/src/attempts/attempt-model.ts @@ -17,7 +17,7 @@ export default class Attempt { @observable agents: Agent[] = [] @observable sessions: Record = {} @observable commands: Command[] = [] - @observable err = new Err({}) + // @observable err = new Err({}) @observable hooks: Hook[] = [] // TODO: make this an enum with states: 'QUEUED, ACTIVE, INACTIVE' @observable isActive: boolean | null = null @@ -49,7 +49,7 @@ export default class Attempt { this.id = props.currentRetry || 0 this.test = test this._state = props.state - this.err.update(props.err) + // this.err.update(props.err) this.invocationDetails = props.invocationDetails @@ -140,7 +140,7 @@ export default class Attempt { commandMatchingErr () { return _(this.hooks) .map((hook) => { - return hook.commandMatchingErr(this.err) + // return hook.commandMatchingErr(this.err) }) .compact() .last() @@ -155,7 +155,7 @@ export default class Attempt { this._state = props.state } - this.err.update(props.err) + // this.err.update(props.err) if (props.failedFromHookId) { const hook = _.find(this.hooks, { hookId: props.failedFromHookId }) diff --git a/packages/reporter/src/attempts/attempts.tsx b/packages/reporter/src/attempts/attempts.tsx index 47237f58d150..0418b51d737f 100644 --- a/packages/reporter/src/attempts/attempts.tsx +++ b/packages/reporter/src/attempts/attempts.tsx @@ -54,9 +54,9 @@ function renderAttemptContent (model: AttemptModel, studioActive: boolean) {
{model.hasCommands ? : } -
-
- + {/*
+
*/} + {/* */} {studioActive && model.state === 'failed' && }
diff --git a/packages/reporter/src/commands/command-model.ts b/packages/reporter/src/commands/command-model.ts index 889a934d1085..42ad01c85d62 100644 --- a/packages/reporter/src/commands/command-model.ts +++ b/packages/reporter/src/commands/command-model.ts @@ -35,7 +35,6 @@ export interface CommandProps extends InstrumentProps { wallClockStartedAt?: string hookId: string isStudio?: boolean - showError?: boolean group?: number groupLevel?: number hasSnapshot?: boolean @@ -56,7 +55,6 @@ export default class Command extends Instrument { @observable children: Array = [] @observable hookId: string @observable isStudio: boolean - @observable showError?: boolean = false @observable group?: number @observable groupLevel?: number @observable hasSnapshot?: boolean @@ -92,6 +90,7 @@ export default class Command extends Instrument { return this._isOpen || (this._isOpen === null && ( + (this.err.hasError || this.err.showRecoveredError) || // command has nested commands (this.name !== 'session' && this.hasChildren && !this.event && this.type !== 'system') || // command has nested commands with children @@ -136,7 +135,6 @@ export default class Command extends Instrument { this.wallClockStartedAt = props.wallClockStartedAt this.hookId = props.hookId this.isStudio = !!props.isStudio - this.showError = !!props.showError this.group = props.group this.hasSnapshot = !!props.hasSnapshot this.hasConsoleProps = !!props.hasConsoleProps @@ -159,7 +157,6 @@ export default class Command extends Instrument { this.timeout = props.timeout this.hasSnapshot = props.hasSnapshot this.hasConsoleProps = props.hasConsoleProps - this.showError = props.showError this._checkLongRunning() } diff --git a/packages/reporter/src/commands/command.tsx b/packages/reporter/src/commands/command.tsx index c73b22450268..f75e0e1239dc 100644 --- a/packages/reporter/src/commands/command.tsx +++ b/packages/reporter/src/commands/command.tsx @@ -363,37 +363,28 @@ class Command extends Component { } return ( -
  • -
    + +
  • +
    - - {model.showError && ( - -
    - {groupPlaceholder} - -
    -
    - )} - {!model.showError && ( + ) + } + > + {
    - )} - - - {this._children()} -
  • + + + {this._children()} + + {model.err && } + ) } @@ -508,6 +500,38 @@ class Command extends Component { } } +class Err extends Component { + static defaultProps = { + appState, + events, + runnablesStore, + } + + render () { + const { model } = this.props + + if (!model.err?.hasError) { + return null + } + + const groupPlaceholder: Array = [] + + if (model.groupLevel !== undefined) { + // cap the group nesting to 5 levels to keep the log text legible + const level = model.groupLevel < 6 ? model.groupLevel : 5 + + for (let i = 1; i < level; i++) { + groupPlaceholder.push() + } + } + + return ( +
  • + +
  • ) + } +} + export { Aliases, AliasesReferences, Message, Progress } export default Command diff --git a/packages/reporter/src/commands/commands.scss b/packages/reporter/src/commands/commands.scss index 8a00f37bb2c3..c76fde47deb8 100644 --- a/packages/reporter/src/commands/commands.scss +++ b/packages/reporter/src/commands/commands.scss @@ -124,7 +124,8 @@ margin-left: 25px; } - .command-pin-target.command-group { + .command-pin-target.command-group, + .runnable-err-wrapper.show-recovered-test-err { @include nested-command-dashes($gray-600); padding-left: 12px; min-height: 28px; @@ -272,7 +273,8 @@ vertical-align: middle; } - .command-group { + .command-group, + .show-recovered-test-err { border-color: $err-header-text; @include nested-command-dashes($err-header-text); @@ -283,13 +285,6 @@ } } - .command .runnable-err-wrapper { - padding: 0; - border: 0; - margin: 0; - display: inline-grid; - } - // Custom Styles for Specific Commands .command-name-assert { .command-method { diff --git a/packages/reporter/src/errors/err-model.ts b/packages/reporter/src/errors/err-model.ts index 29aff35b2948..4e5e7eb839c2 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 + showRecoveredError: boolean stack: string parsedStack: ParsedStackLine[] docsUrl: string | string[] @@ -41,11 +42,16 @@ export default class Err { @observable templateType = '' // @ts-ignore @observable.ref codeFrame: CodeFrame + @observable showRecoveredError: boolean constructor (props?: Partial) { this.update(props) } + @computed get hasError () { + return this.displayMessage + } + @computed get displayMessage () { return _.compact([this.name, this.message]).join(': ') } @@ -55,6 +61,7 @@ export default class Err { } update (props?: Partial) { + console.log('update', props) if (!props) return if (props.name) this.name = props.name @@ -64,5 +71,6 @@ export default class Err { if (props.parsedStack) this.parsedStack = props.parsedStack if (props.templateType) this.templateType = props.templateType if (props.codeFrame) this.codeFrame = props.codeFrame + this.showRecoveredError = !!props.showRecoveredError } } diff --git a/packages/reporter/src/errors/errors.scss b/packages/reporter/src/errors/errors.scss index a02041d9b51c..c066e4744f8e 100644 --- a/packages/reporter/src/errors/errors.scss +++ b/packages/reporter/src/errors/errors.scss @@ -53,6 +53,12 @@ $code-border-radius: 4px; .runnable-err-wrapper { cursor: default; + margin-top: 2px; + + &.show-recovered-test-err { + margin-left: 49px; + // needs to be padding. blowing out of width + } } .studio-err-wrapper { @@ -60,14 +66,13 @@ $code-border-radius: 4px; } .runnable-err { - border-left: 2px solid $fail; background-color: $err-background; clear: both; color: $err-text; font-family: $monospace; font-style: normal; margin-bottom: 0; - margin-top: 5px; + margin-top: 2px; white-space: pre-wrap; word-break: break-word; user-select: initial; @@ -75,22 +80,23 @@ $code-border-radius: 4px; } .runnable-err-header { - background-color: $red-300, 0.05; - display: flex; + background-color: $err-header-background; + // display: flex; justify-content: space-between; - padding: 5px 10px; + padding: 5px 16px; font-weight: bold; + svg { + color: $red-400; + margin-right: 10px; + align-self: center + } .runnable-err-name { - flex-grow: 2; - font-size: 13px; - line-height: 22px; color: $err-header-text; - - svg { - color: $red-400; - margin-right: 10px; - } + font-size: 12px; + font-weight: 600; + line-height: 20px; + margin-left: 16px; } } @@ -104,13 +110,14 @@ $code-border-radius: 4px; font-family: $font-sans; font-size: 14px; font-weight: 400; - padding: 10px; + padding: 10px 10px 10px 16px; code { background-color: rgba($black, 0.2); border-radius: 4px; color: $err-code-text; - font-family: $font-sans; + font-size: 12px; + font-family: $monospace; padding: 2px 5px; } @@ -229,7 +236,7 @@ $code-border-radius: 4px; .test-err-code-frame { background-color: $gray-1000; - border: 1px solid rgba($red-400, 0.25); + border: 1px dashed rgba(245, 154, 169, 0.1); border-radius: $code-border-radius; margin: 0 10px 10px; @@ -239,7 +246,7 @@ $code-border-radius: 4px; border-top-right-radius: $code-border-radius; display: block; font-size: 14px; - line-height: 16px; + line-height: 20px; padding: 8px; word-break: break-all; @@ -257,3 +264,4 @@ $code-border-radius: 4px; } } } + diff --git a/packages/reporter/src/errors/test-error.tsx b/packages/reporter/src/errors/test-error.tsx index 58adae81a397..5241aa1fa6bf 100644 --- a/packages/reporter/src/errors/test-error.tsx +++ b/packages/reporter/src/errors/test-error.tsx @@ -1,5 +1,6 @@ import _ from 'lodash' import React, { MouseEvent } from 'react' +import cs from 'classnames' import { observer } from 'mobx-react' import Markdown from 'markdown-it' @@ -55,13 +56,15 @@ const TestError = observer((props: TestErrorProps) => { onPrint() } - const { err } = props.model - const { codeFrame } = err + const err = props.model + + if (!err || !err.displayMessage) return null - if (!err.displayMessage) return null + const { codeFrame } = err return ( -
    +
    + {props.groupsPlaceholder}
    diff --git a/packages/reporter/src/lib/variables.scss b/packages/reporter/src/lib/variables.scss index 6201b66f5df2..489e1256f6ef 100644 --- a/packages/reporter/src/lib/variables.scss +++ b/packages/reporter/src/lib/variables.scss @@ -104,24 +104,18 @@ $fail: $red-400; $pending: $indigo-400; $pinned: $purple-400; $retried: $orange-400; -$yellow-medium: $orange-800; $link-text: $indigo-600; -$err-background: #2F2434; +$err-background: #2C2036; $err-code-background: rgba($red-400, 0.18); $err-code-text: $red-300; -$err-header-background: #3D2839; +$err-header-background: #3A243B; $err-header-text: $red-300; $err-text: $red-400; $reporter-section-background: #171926; // not a brand color -$warn-background: $red-1000; -$warn-header-background: $orange-1000; -$warn-header-text: $orange-700; -$warn-text: $orange-600; - $header-height: 64px; $reporter-contents-min-width: 170px; diff --git a/system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/session_errors.cy.js b/system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/session_errors.cy.js index 35194187c189..b03e4f47df4b 100644 --- a/system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/session_errors.cy.js +++ b/system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/session_errors.cy.js @@ -3,3 +3,11 @@ it('setup has failing command', () => { cy.get('does_not_exist', { timeout: 500 }) }) }) + +it('validate has failing command', () => { + cy.session('session_1', () => { + cy.log('do setup') + }, () => { + cy.get('does_not_exist', { timeout: 500 }) + }) +}) From c2a1b5c5be91fe472e02578ae082edc86cefb6a3 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Thu, 13 Oct 2022 14:45:03 -0500 Subject: [PATCH 07/52] great time to check some code in! next steps...aligning the stack traces with the line that failed. --- .../app/cypress/e2e/runner/sessions.ui.cy.ts | 110 +++++----- packages/app/src/runner/logger.ts | 20 +- .../driver/src/cy/commands/sessions/index.ts | 207 +++++++++--------- .../src/cy/commands/sessions/manager.ts | 18 +- .../driver/src/cy/commands/sessions/utils.ts | 81 ++++--- packages/driver/src/cypress/error_messages.ts | 2 +- packages/driver/src/cypress/log.ts | 17 +- packages/reporter/src/commands/command.cy.tsx | 26 +++ packages/reporter/src/commands/commands.scss | 2 + packages/reporter/src/errors/err-model.ts | 1 - .../reporter/src/sessions/sessions-model.ts | 1 - packages/reporter/src/sessions/sessions.tsx | 4 +- .../selector-playground.scss.d.ts | 7 - .../cypress/e2e/session/errors.cy.js | 125 +++++++++++ 14 files changed, 406 insertions(+), 215 deletions(-) delete mode 100644 packages/runner/src/selector-playground/selector-playground.scss.d.ts diff --git a/packages/app/cypress/e2e/runner/sessions.ui.cy.ts b/packages/app/cypress/e2e/runner/sessions.ui.cy.ts index c468d9cef71b..bde3ead3452c 100644 --- a/packages/app/cypress/e2e/runner/sessions.ui.cy.ts +++ b/packages/app/cypress/e2e/runner/sessions.ui.cy.ts @@ -332,70 +332,72 @@ describe('runner/cypress sessions.ui.spec', { }) describe('errors', () => { - it.only('test error when setup has failing Cypress command', () => { - loadSpec({ - projectName: 'session-and-origin-e2e-specs', - filePath: 'session/errors.cy.js', - failCount: 1, - }) + describe.only('created session', () => { + it('has test error when setup has failing Cypress command', () => { + loadSpec({ + projectName: 'session-and-origin-e2e-specs', + filePath: 'session/errors.cy.js', + failCount: 2, + }) - 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(() => { + 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') - - cy.get('@session_command') - .children('.command-child-container').should('exist') - .within(() => { - cy.get('.command-name-session') - .should('contain', 'Create new session') - .get('.command-child-container').should('exist') + cy.get('.command-name-session').eq(0).should('contain', 'session_1').as('session_command') + .children('.command-wrapper').find('.reporter-tag').should('contain', 'failed') + + cy.get('@session_command') + .children('.command-child-container').should('exist') + .within(() => { + cy.get('.command-name-session') + .should('contain', 'Create new session') + .get('.command-child-container').should('exist') + }) }) - }) - // has error - cy.get('@setup_failed').contains('This error occurred while creating session. Because the session setup failed, we failed the test.') - }) - - describe('created session failed validation', () => { - it('has test error when validate returned false', () => { - // test marked as failed - // test is expanded - // session is marked as 'failed' - // validates group is expanded + // has error + cy.get('@setup_failed').contains('This error occurred while creating session. Because the session setup failed, we failed the test.') }) - it('test error when validate resolved false', () => { - // test marked as failed - // test is expanded - // session is marked as 'failed' - // validates group is expanded - }) + describe('failed validation', () => { + it('has test error when validate returned false', () => { + // test marked as failed + // test is expanded + // session is marked as 'failed' + // validates group is expanded + }) - it('test error when validate rejected with false', () => { - // test marked as failed - // test is expanded - // session is marked as 'failed' - // validates group is expanded - }) + it('test error when validate resolved false', () => { + // test marked as failed + // test is expanded + // session is marked as 'failed' + // validates group is expanded + }) - it('test error when validate threw error', () => { - // test marked as failed - // test is expanded - // session is marked as 'failed' - // validates group is expanded - }) + it('test error when validate rejected with false', () => { + // test marked as failed + // test is expanded + // session is marked as 'failed' + // validates group is expanded + }) - it('test error when validate has failing Cypress command', () => { - // test marked as failed - // test is expanded - // session is marked as 'failed' - // validates group is expanded + it('test error when validate threw error', () => { + // test marked as failed + // test is expanded + // session is marked as 'failed' + // validates group is expanded + }) + + it('test error when validate has failing Cypress command', () => { + // test marked as failed + // test is expanded + // session is marked as 'failed' + // validates group is expanded + }) }) }) diff --git a/packages/app/src/runner/logger.ts b/packages/app/src/runner/logger.ts index 82c04f943fcd..24240b5b7fcc 100644 --- a/packages/app/src/runner/logger.ts +++ b/packages/app/src/runner/logger.ts @@ -3,14 +3,16 @@ import _ from 'lodash' interface Table { name: string - data: object - columns: any + data?: object + columns?: any } 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/src/cy/commands/sessions/index.ts b/packages/driver/src/cy/commands/sessions/index.ts index aeef26c4477f..2c7188383b92 100644 --- a/packages/driver/src/cy/commands/sessions/index.ts +++ b/packages/driver/src/cy/commands/sessions/index.ts @@ -170,15 +170,23 @@ export default function (Commands, Cypress, cy) { displayName: recreateSession ? 'Recreate session' : 'Create new session', 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' }) + setupLogGroup.set({ + state: 'failed', + consoleProps: { + Step: 'Create New Session', + 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 creating the session. Because the session setup failed, we failed the test.`, _.add) return false }) @@ -193,8 +201,16 @@ export default function (Commands, Cypress, cy) { _.extend(existingSession, data) existingSession.hydrated = true await sessions.saveSessionData(existingSession) - + console.log('SET CREATE NEW SESSION PROPS') _log.set({ consoleProps: () => getConsoleProps(existingSession) }) + setupLogGroup.set({ + consoleProps: () => { + return { + Step: 'Create new session', + ...getConsoleProps(existingSession), + } + }, + }) return }) @@ -208,6 +224,12 @@ export default function (Commands, Cypress, cy) { displayName: 'Restore saved session', message: '', type: 'system', + consoleProps: () => { + return { + Step: 'Restore saved session', + ...getConsoleProps(testSession), + } + }, }) _log.set({ consoleProps: () => getConsoleProps(testSession) }) @@ -228,15 +250,17 @@ export default function (Commands, Cypress, cy) { displayName: 'Validate session', message: '', type: 'system', + consoleProps: { + Step: 'Validate Session', + }, }, (validateLog) => { return cy.then(async () => { const onSuccess = () => { return isValidSession } - const onFail = (err) => { - // validateLog.set({ state: 'failed' }) - // setSessionLogStatus('failed') + const onFail = (err, yielded) => { + Cypress.state('onFail', null) // show validation error and allow sessions workflow to recreate the session if (sessionStatus === 'restored') { @@ -255,11 +279,33 @@ export default function (Commands, Cypress, cy) { return !isValidSession } - validateLog.set({ state: 'failed' }) setSessionLogStatus('failed') - $errUtils.modifyErrMsg(err, `\n\nThis error occurred while validating the ${sessionStatus} session. Because validation failed immediately after creating the session, we failed the test.`, _.add) + validateLog.set({ + state: 'failed', + consoleProps: (args) => { + return { + Error: err.stack, + } + }, + // error: err, + }) + + err.onFail = (err) => { + // console.log('err on FAIL error', err) + validateLog.set({ + // ...(yielded && { consoleProps: { + // Yielded: yielded + // } + // }), + snapshot: true, + error: err, + }) + } + + $errUtils.modifyErrMsg(err, `\n\nThis error occurred while validating the ${sessionStatus} session. Because validation failed immediately after ${sessionStatus === 'created' ? 'creating' : 'recreating'} the session, we failed the test.`, _.add) + // console.log('before throw', err) - return cy.fail(err) + throw err } return validate(existingSession, sessionStatus, onSuccess, onFail) @@ -270,18 +316,29 @@ export default function (Commands, Cypress, cy) { // uses Cypress hackery to resolve `false` if validate() resolves/returns false or throws/fails a cypress command. function validate (existingSession, sessionStatus, onSuccess, onFail) { let returnVal - let _validationError = null - console.log('validate!', sessionStatus) - const normalizeError = (err) => { - err.stack = $stackUtils.normalizedStack(err) + Cypress.state('onFail', (err) => { + // set current command to cy.session for more accurate codeFrame + // err.stack = $stackUtils.normalizedStack(err) - return $errUtils.enhanceStack({ - err, - userInvocationStack: $errUtils.getUserInvocationStack(err, Cypress.state), - projectRoot: Cypress.config('projectRoot'), - }) - } + // err = $errUtils.enhanceStack({ + // err, + // userInvocationStack: $errUtils.getUserInvocationStack(err, this.state), + // projectRoot: this.config('projectRoot'), + // }) + + return onFail(err) + }) + + // const normalizeError = (err) => { + // err.stack = $stackUtils.normalizedStack(err) + + // return $errUtils.enhanceStack({ + // err, + // userInvocationStack: $errUtils.getUserInvocationStack(err, Cypress.state), + // projectRoot: Cypress.config('projectRoot'), + // }) + // } try { returnVal = existingSession.validate() @@ -289,95 +346,45 @@ export default function (Commands, Cypress, cy) { return onFail(e) } - // 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) - - return onFail($errUtils.errByPath('sessions.validate_callback_false', { reason: 'resolved false' })) - } - - return onSuccess() - }) - .catch((err) => { - return onFail(err) - }) - } - - // catch when a cypress command fails in the validate callback to move the queue index -<<<<<<< HEAD - cy.state('onCommandFailed', (err, queue, next, commandRunningFailed) => { - console.log('onCommandFailed', err) -======= - cy.state('onCommandFailed', (err, queue) => { ->>>>>>> develop - const index = _.findIndex(queue.get(), (command: any) => { - return ( - _commandToRunAfterValidation - && command.attributes.chainerId === _commandToRunAfterValidation.chainerId - ) - }) - - console.log(index) - - // 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) - } - - // commandRunningFailed(Cypress, queue.state, err) - - _validationError = normalizeError(err) - - // move to _commandToRunAfterValidation's index to ensure failures are handled correctly - cy.state('index', index) - - // cy.state('onCommandFailed', null) - - return true - }) + return cy.then(async () => { + Cypress.state('onFail', null) + // 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) + + throw $errUtils.errByPath('sessions.validate_callback_false', { reason: 'promise resolved false' }) + } - const _commandToRunAfterValidation = cy.then(async () => { - console.log('_commandToRunAfterValidation', _validationError) - cy.state('onCommandFailed', null) + return onSuccess() + }) + .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)}` }) + } - if (_validationError) { - return onFail(_validationError) + return onFail(err) + }) } - if (returnVal === false) { - // set current command to cy.session for more accurate codeframe - const current = cy.state('current') + if (returnVal === undefined || Cypress.isCy(returnVal)) { + const val = cy.state('current').get('prev')?.attributes?.subject - cy.state('current', sessionCommand) - - const err = normalizeError($errUtils.errByPath('sessions.validate_callback_false', { reason: 'returned false' })) - - cy.state('current', current) + if (val === false) { + // set current command to cy.session for more accurate codeframe + cy.state('current', sessionCommand) - return onFail(err) + return onFail($errUtils.errByPath('sessions.validate_callback_false', { reason: 'callback\'s last command yielded false' }), val) + } } - // if (returnVal === undefined || Cypress.isCy(returnVal)) { - // const val = cy.state('current').get('prev')?.attributes?.subject - - // if (val === false) { - // // set current command to cy.session for more accurate codeframe - // console.log(cy.state('current')) - // cy.state('current', sessionCommand) - - // return onFail($errUtils.errByPath('sessions.validate_callback_false', { reason: 'resolved false' })) - // } - // } - return onSuccess() }) - - return _commandToRunAfterValidation } /** @@ -387,7 +394,7 @@ export default function (Commands, Cypress, cy) { */ const createSessionWorkflow = (existingSession, sessionStatus: 'creating' | 'recreating' = 'creating') => { return cy.then(async () => { - setSessionLogStatus(sessionStatus ? 'recreating' : 'creating') + setSessionLogStatus(sessionStatus) await navigateAboutBlank() await sessions.clearCurrentSessionData() @@ -400,7 +407,7 @@ export default function (Commands, Cypress, cy) { return } - setSessionLogStatus(sessionStatus ? 'recreated' : 'created') + setSessionLogStatus(sessionStatus === 'recreating' ? 'recreated' : 'created') }) } diff --git a/packages/driver/src/cy/commands/sessions/manager.ts b/packages/driver/src/cy/commands/sessions/manager.ts index 7da6f6694247..37d0d9718abc 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], } } @@ -143,7 +153,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 f9153629e5f7..9cf1407cfe74 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,59 @@ 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 getConsoleProps = (session: SessionData) => { + const sessionDetails = getSessionDetailsByDomain(session) + + const groupsByDomain = _.flatMap(sessionDetails, (val, domain) => { + // const info = '⚠️ There are no ${cookies, local storage or session} storage associated to this domain.', + + return { + name: `${domain} data:`, + expand: true, + label: false, + // items: _.compact([ + // // !val.cookies && `There are no cookies associated to ${domain}.`, + // !val.cookies && `🍪 Cookies - (0)`, + // // !val.localStorage && `There are no local storage items associated to ${domain}.`, + // !val.localStorage && `📁 Local Storage - (0)`, + // // !val.sessionStorage && `There are no session storage items associated to ${domain}.`, + // !val.sessionStorage && `📁 Session Storage - (0)`, + // ]), + // items: [] + 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, + }, + ]), } - - const localStorageTable = () => { - return { - name: `📁 Storage - ${domain} (${_.keys(val.localStorage.value).length})`, - data: _.map(val.localStorage.value, (value, key) => { - return { - key, - 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 or session storage associated to this session.', + }), + ...(groupsByDomain.length && { + Domains: `This session capture data from ${Object.keys(sessionDetails).join(', ')}.`, + }), + groups: _.compact(groupsByDomain), } + + return props } const getPostMessageLocalStorage = (specWindow, origins): Promise => { diff --git a/packages/driver/src/cypress/error_messages.ts b/packages/driver/src/cypress/error_messages.ts index 0bf0366a21e0..cddfd39ac8c7 100644 --- a/packages/driver/src/cypress/error_messages.ts +++ b/packages/driver/src/cypress/error_messages.ts @@ -1681,7 +1681,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/log.ts b/packages/driver/src/cypress/log.ts index 8c52e9775b8e..17755055f766 100644 --- a/packages/driver/src/cypress/log.ts +++ b/packages/driver/src/cypress/log.ts @@ -229,7 +229,7 @@ const defaults = function (state: StateFunc, config, obj) { } export class Log { - cy: any + createSnapshot: Function state: StateFunc config: any fireChangeEvent: ((log) => (void | undefined)) @@ -237,8 +237,8 @@ export class Log { private attributes: Record = {} - constructor (cy, state, config, fireChangeEvent, obj) { - this.cy = cy + constructor (createSnapshot, state, config, fireChangeEvent, obj) { + this.createSnapshot = createSnapshot this.state = state this.config = config // only fire the log:state:changed event as fast as every 4ms @@ -388,7 +388,7 @@ export class Log { } } - const snapshot = this.cy.createSnapshot(name, this.get('$el')) + const snapshot = this.createSnapshot(name, this.get('$el')) this.addSnapshot(snapshot, options) @@ -544,7 +544,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 @@ -576,7 +576,6 @@ class LogManager { const attrs = log.toJSON() - console.log('trigger', attrs) // only trigger this event if our last stored // emitted attrs do not match the current toJSON if (!_.isEqual(log._emittedAttrs, attrs)) { @@ -602,7 +601,7 @@ class LogManager { return this.trigger(log, 'command:log:changed') } - createLogFn (cy, state, config) { + createLogFn (createSnapshot, state, config) { return (options: any = {}) => { // if (!this.isInteractive) { // // if (_skipCollectingLogs || !this.isInteractive) { @@ -613,7 +612,7 @@ class LogManager { $errUtils.throwErrByPath('log.invalid_argument', { args: { arg: options } }) } - const log = new Log(cy, state, config, this.fireChangeEvent, options) + const log = new Log(createSnapshot, state, config, this.fireChangeEvent, options) log.set(options) @@ -666,5 +665,5 @@ export function create (Cypress, cy, state, config) { counter = 0 const logManager = new LogManager(Cypress.state('isInteractive')) - return logManager.createLogFn(cy, state, config) + return logManager.createLogFn(cy.createSnapshot, state, config) } diff --git a/packages/reporter/src/commands/command.cy.tsx b/packages/reporter/src/commands/command.cy.tsx index aa8867a54163..ccab7bf0cea1 100644 --- a/packages/reporter/src/commands/command.cy.tsx +++ b/packages/reporter/src/commands/command.cy.tsx @@ -3,6 +3,30 @@ import Command from './command' import CommandModel from './command-model' describe('commands', () => { + it('clamps long messages', () => { + const message = 'really long looooon glong message' + cy.mount( + + ) + + cy.percySnapshot() + }) + describe('sessionPill', () => { const statusList = [ 'creating', @@ -43,5 +67,7 @@ describe('commands', () => { cy.percySnapshot() }) + + }) }) diff --git a/packages/reporter/src/commands/commands.scss b/packages/reporter/src/commands/commands.scss index c76fde47deb8..5abd38eeace6 100644 --- a/packages/reporter/src/commands/commands.scss +++ b/packages/reporter/src/commands/commands.scss @@ -139,6 +139,8 @@ } .command-info { + padding-top: 4px; + padding-bottom: 4px; margin-left: 0; overflow: hidden; width: 100%; diff --git a/packages/reporter/src/errors/err-model.ts b/packages/reporter/src/errors/err-model.ts index 4e5e7eb839c2..84d749ad3794 100644 --- a/packages/reporter/src/errors/err-model.ts +++ b/packages/reporter/src/errors/err-model.ts @@ -61,7 +61,6 @@ export default class Err { } update (props?: Partial) { - console.log('update', props) if (!props) return if (props.name) this.name = props.name diff --git a/packages/reporter/src/sessions/sessions-model.ts b/packages/reporter/src/sessions/sessions-model.ts index 8b1cf2b77f5c..fbfc39e256ae 100644 --- a/packages/reporter/src/sessions/sessions-model.ts +++ b/packages/reporter/src/sessions/sessions-model.ts @@ -30,6 +30,5 @@ export default class Session extends Instrument { const { sessionInfo, state } = 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..a737f0fcaf60 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) } @@ -35,7 +35,7 @@ const SessionRow = ({ name, isGlobalSession, id, state, status, testId }: Sessio
    diff --git a/packages/runner/src/selector-playground/selector-playground.scss.d.ts b/packages/runner/src/selector-playground/selector-playground.scss.d.ts deleted file mode 100644 index 132b232e8959..000000000000 --- a/packages/runner/src/selector-playground/selector-playground.scss.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -// This file is automatically generated. -// Please do not change this file! -interface CssExports { - -} -export const cssExports: CssExports; -export default cssExports; 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..8cfb53083738 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,133 @@ /** * Used in cy-in-cy tests in @packages/app. */ + +before(async () => { + Cypress.state('activeSessions', {}) + await Cypress.session.clearAllSavedSessions() +}) + it('setup has failing command', () => { cy.session('session_1', () => { cy.get('does_not_exist', { timeout: 1 }) }) }) + +it('created command resolves false', () => { + cy.session('session_2', () => { + cy.get('div') + }, { + async validate () { + return new Promise(async (resolve, reject) => { + // Cypress.log('getCurrentSessionData') // throws uncaught exception + // Cypress.log({ + // name: 'getCurrentSessionData', + // type: 'system', + // state: 'passed', + // }) + const { cookies } = await Cypress.session.getCurrentSessionData() + + console.log(cookies) + if (cookies.length === 0) { // this is always zero! we aren't setting any :D + resolve(false) + } + }) + }, + }) +}) + +it('created command resolves false with log between', () => { + cy.session('session_3', () => { + cy.get('div') + cy.window().then((win) => { + // win.localStorage.setItem('hi', 'there') + }) + }, { + async validate () { + Cypress.log({ + name: 'getCurrentSessionData', + message: '', + type: 'system', + state: 'passed', + }) + + return new Promise(async (resolve, reject) => { + // Cypress.log('getCurrentSessionData') // throws uncaught exception + + const { cookies } = await Cypress.session.getCurrentSessionData() + + if (cookies.length === 0) { // this is always zero! we aren't setting any :D + resolve(false) + } + }) + }, + }) +}) + +it('created command reject false with log between', () => { + cy.session('session_4', () => { + cy.get('div') + }, { + async validate () { + return new Promise(async (resolve, reject) => { + // Cypress.log('getCurrentSessionData') // throws uncaught exception + const { cookies } = await Cypress.session.getCurrentSessionData() + + console.log(cookies) + if (cookies.length === 0) { // this is always zero! we aren't setting any :D + reject(false) + } + }) + }, + }) +}) + +it('created command has failing command in validate', () => { + cy.session('session_5', () => { + cy.get('div').as('hello') + }, { + validate () { + cy.get('does_not_exist', { timeout: 1 }) + }, + }) +}) + +it('created command has failing command in validate', () => { + cy.session('session_6', () => { + cy.get('div') + }, { + validate () { + cy.then(() => { + return false + }) + }, + }) +}) + +it('created command has failing command in validate', () => { + cy.session('session_7', () => { + cy.get('div') + }, { + validate () { + return cy.wrap(false) + }, + }) +}) + +it('created command has failing command in validate', () => { + cy.session('session_8', () => { + cy.get('div') + }, { + validate () { + cy.window().then((win) => { + console.log(win) + + return false + }) + }, + }) +}) + +it('for reference', () => { + cy.get('does_not_exist', { timeout: 1 }) +}) From 342f7012aa04c03e59188875e8ca315abe192619 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Fri, 14 Oct 2022 11:50:12 -0500 Subject: [PATCH 08/52] great checkin point -- going to cherry pick the UI changes out into their own PR --- packages/driver/src/cypress/stack_utils.ts | 11 +- packages/reporter/src/commands/command.tsx | 22 +- packages/reporter/src/commands/commands.scss | 7 +- packages/reporter/src/errors/errors.scss | 31 ++- packages/reporter/src/errors/test-error.tsx | 44 +-- packages/reporter/src/lib/events.ts | 2 + packages/reporter/src/lib/tag.scss | 2 +- packages/reporter/src/lib/variables.scss | 2 +- packages/reporter/src/sessions/sessions.tsx | 12 +- .../cypress/e2e/session/errors.cy.js | 250 +++++++++++------- 10 files changed, 229 insertions(+), 154 deletions(-) diff --git a/packages/driver/src/cypress/stack_utils.ts b/packages/driver/src/cypress/stack_utils.ts index af8a25d9df90..459741d88c44 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 windowedStackLines = _.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(windowedStackLines) } export default { diff --git a/packages/reporter/src/commands/command.tsx b/packages/reporter/src/commands/command.tsx index f75e0e1239dc..46fb1d705177 100644 --- a/packages/reporter/src/commands/command.tsx +++ b/packages/reporter/src/commands/command.tsx @@ -404,7 +404,7 @@ class Command extends Component { {this._children()} - {model.err && } + {model.err?.hasError && } ) } @@ -516,19 +516,23 @@ class Err extends Component { const groupPlaceholder: Array = [] - if (model.groupLevel !== undefined) { + console.log(model.groupLevel, 'model.groupLevel') + let groupLevel = 0 + + if (groupLevel !== undefined) { // cap the group nesting to 5 levels to keep the log text legible - const level = model.groupLevel < 6 ? model.groupLevel : 5 + groupLevel = model.groupLevel < 6 ? model.groupLevel : 5 - for (let i = 1; i < level; i++) { - groupPlaceholder.push() - } + // for (let i = 0; i < level; i++) { + // groupPlaceholder.push() + // } } return ( -
  • - -
  • ) +
  • + +
  • + ) } } diff --git a/packages/reporter/src/commands/commands.scss b/packages/reporter/src/commands/commands.scss index 5abd38eeace6..3b67554622d8 100644 --- a/packages/reporter/src/commands/commands.scss +++ b/packages/reporter/src/commands/commands.scss @@ -124,8 +124,9 @@ margin-left: 25px; } - .command-pin-target.command-group, - .runnable-err-wrapper.show-recovered-test-err { + .command-pin-target.command-group { + // .runnable-err.show-recovered-test-err > .runnable-err-header, + // .show-recovered-test-err { @include nested-command-dashes($gray-600); padding-left: 12px; min-height: 28px; @@ -257,7 +258,7 @@ color: $err-header-text; &:not(.command-type-system) { - border-left: 2px solid $fail; + border-left: 2px solid $red-300; background-color: $err-header-background; &.command-is-interactive:hover { diff --git a/packages/reporter/src/errors/errors.scss b/packages/reporter/src/errors/errors.scss index c066e4744f8e..2e5421bab224 100644 --- a/packages/reporter/src/errors/errors.scss +++ b/packages/reporter/src/errors/errors.scss @@ -51,14 +51,17 @@ $code-border-radius: 4px; } } - .runnable-err-wrapper { - cursor: default; - margin-top: 2px; + .show-recovered-test-err > .runnable-err-header, + .show-recovered-test-err > .runnable-err-content { + padding-left: 49px; + display: flex; + } - &.show-recovered-test-err { - margin-left: 49px; - // needs to be padding. blowing out of width - } + .err-group-block { + border-left: 1px dotted $err-header-text; + border-image-slice: 0 0 0 1; + border-image-source: repeating-linear-gradient(0deg, transparent, $err-header-text, $err-header-text 4px); + padding-right: 12px; } .studio-err-wrapper { @@ -67,6 +70,7 @@ $code-border-radius: 4px; .runnable-err { background-color: $err-background; + border-left: 2px solid $red-300; clear: both; color: $err-text; font-family: $monospace; @@ -83,8 +87,8 @@ $code-border-radius: 4px; background-color: $err-header-background; // display: flex; justify-content: space-between; - padding: 5px 16px; font-weight: bold; + padding-left: 16px; svg { color: $red-400; @@ -93,21 +97,22 @@ $code-border-radius: 4px; } .runnable-err-name { color: $err-header-text; + flex: auto; font-size: 12px; font-weight: 600; line-height: 20px; - margin-left: 16px; + padding: 5px 0; } } .runnable-err-docs-url { margin-left: 0.5em; cursor: pointer; - font-family: $font-sans; + font-family: $font-system; } .runnable-err-message { - font-family: $font-sans; + font-family: $font-system; font-size: 14px; font-weight: 400; padding: 10px 10px 10px 16px; @@ -158,7 +163,7 @@ $code-border-radius: 4px; div { cursor: pointer; outline: none; - padding: 14px 10px; + padding: 14px 16px; width: 100%; .collapsible-header-text { @@ -195,7 +200,7 @@ $code-border-radius: 4px; div { color: $red-300; cursor: pointer; - font-family: $font-sans; + font-family: $font-system; font-size: 14px; font-weight: 500; height: 100%; diff --git a/packages/reporter/src/errors/test-error.tsx b/packages/reporter/src/errors/test-error.tsx index 5241aa1fa6bf..8f98a69e8fba 100644 --- a/packages/reporter/src/errors/test-error.tsx +++ b/packages/reporter/src/errors/test-error.tsx @@ -37,7 +37,7 @@ const DocsUrl = ({ url }: DocsUrlProps) => { } interface TestErrorProps { - model: Attempt | Command + model: Command onPrintToConsole?: () => void } @@ -62,22 +62,33 @@ const TestError = observer((props: TestErrorProps) => { const { codeFrame } = err + const groupPlaceholder: Array = [] + + if (err.showRecoveredError) { + // cap the group nesting to 5 levels to keep the log text legible + for (let i = 0; i < props.groupLevel; i++) { + groupPlaceholder.push() + } + } + return ( -
    - {props.groupsPlaceholder} -
    -
    -
    - - {err.name} -
    +
    +
    + {groupPlaceholder} +
    + + {err.name}
    -
    - - -
    - {codeFrame && } - {err.stack && +
    +
    + {groupPlaceholder} +
    +
    + + +
    + {codeFrame && } + {err.stack && { > - } + } +
    ) diff --git a/packages/reporter/src/lib/events.ts b/packages/reporter/src/lib/events.ts index 31ccfc959503..904167f778c2 100644 --- a/packages/reporter/src/lib/events.ts +++ b/packages/reporter/src/lib/events.ts @@ -152,6 +152,8 @@ const events: Events = { }) localBus.on('show:error', (test: TestModel) => { + console.log(test) + // I broken this with uncaught exception errors const command = test.err.isCommandErr ? test.commandMatchingErr() : null runner.emit('runner:console:error', { diff --git a/packages/reporter/src/lib/tag.scss b/packages/reporter/src/lib/tag.scss index 7105b0af79e7..0951e7f94896 100644 --- a/packages/reporter/src/lib/tag.scss +++ b/packages/reporter/src/lib/tag.scss @@ -6,7 +6,7 @@ font-size: 12px; font-style: normal; font-weight: 500; - line-height: initial; + line-height: 18px; height: 18px; max-width: 200px; overflow: hidden; diff --git a/packages/reporter/src/lib/variables.scss b/packages/reporter/src/lib/variables.scss index 489e1256f6ef..b9066a20ec9d 100644 --- a/packages/reporter/src/lib/variables.scss +++ b/packages/reporter/src/lib/variables.scss @@ -121,5 +121,5 @@ $reporter-contents-min-width: 170px; $font-sans: 'Fira Mono', 'Helvetica Neue', 'Arial', sans-serif; $open-sans: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; -$monospace: Consolas, Monaco, 'Andale Mono', monospace; +$monospace: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono", "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro", "Fira Mono", "Droid Sans Mono", "Courier New", monospace; $font-system: ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"; diff --git a/packages/reporter/src/sessions/sessions.tsx b/packages/reporter/src/sessions/sessions.tsx index a737f0fcaf60..86149b5339f2 100644 --- a/packages/reporter/src/sessions/sessions.tsx +++ b/packages/reporter/src/sessions/sessions.tsx @@ -31,13 +31,11 @@ const SessionRow = ({ name, isGlobalSession, id, status, testId }: SessionsModel {isGlobalSession && } {name} - - - +
    ) 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 8cfb53083738..1a2537a62aa4 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 @@ -7,127 +7,177 @@ before(async () => { await Cypress.session.clearAllSavedSessions() }) -it('setup has failing command', () => { - cy.session('session_1', () => { - cy.get('does_not_exist', { timeout: 1 }) - }) -}) - -it('created command resolves false', () => { - cy.session('session_2', () => { - cy.get('div') - }, { - async validate () { - return new Promise(async (resolve, reject) => { - // Cypress.log('getCurrentSessionData') // throws uncaught exception - // Cypress.log({ - // name: 'getCurrentSessionData', - // type: 'system', - // state: 'passed', - // }) - const { cookies } = await Cypress.session.getCurrentSessionData() - - console.log(cookies) - if (cookies.length === 0) { // this is always zero! we aren't setting any :D - resolve(false) - } +describe('create session', () => { + describe('seems correct', () => { + it('setup has failing command', () => { + cy.session('session_1', () => { + cy.get('does_not_exist', { timeout: 1 }) }) - }, - }) -}) + }) -it('created command resolves false with log between', () => { - cy.session('session_3', () => { - cy.get('div') - cy.window().then((win) => { - // win.localStorage.setItem('hi', 'there') + it('created command reject false', () => { + cy.session('session_4', () => { + cy.get('div') + }, { + async validate () { + return new Promise(async (resolve, reject) => { + // Cypress.log('getCurrentSessionData') // throws uncaught exception + const { cookies } = await Cypress.session.getCurrentSessionData() + + console.log(cookies) + if (cookies.length === 0) { // this is always zero! we aren't setting any :D + reject(false) + } + }) + }, + }) }) - }, { - async validate () { - Cypress.log({ - name: 'getCurrentSessionData', - message: '', - type: 'system', - state: 'passed', + + it('created command validate has failing command', function () { + let count = 0 + + cy.session('session_5', function () { + cy.get('div').as('hello') + }, { + validate () { + console.log('count', count) + if (count > 0) { + count++ + cy.get('does_not_exist', { timeout: 1 }) + cy.get('does_not_exist_2', { timeout: 1 }) + } + }, }) - return new Promise(async (resolve, reject) => { - // Cypress.log('getCurrentSessionData') // throws uncaught exception + cy.then(() => { + count = 1 + }) - const { cookies } = await Cypress.session.getCurrentSessionData() + cy.session('session_5', function () { + cy.get('div').as('hello') + }, { + validate () { + console.log('count', count) + if (count > 0) { + count++ + cy.get('does_not_exist', { timeout: 1 }) + cy.get('does_not_exist_2', { timeout: 1 }) + } + }, + }) + }) - if (cookies.length === 0) { // this is always zero! we aren't setting any :D - resolve(false) - } + it('created command validate has .then yield false ', () => { + cy.session('session_6', () => { + cy.get('div') + }, { + validate () { + cy.then(() => { + return false + }) + }, }) - }, - }) -}) + }) -it('created command reject false with log between', () => { - cy.session('session_4', () => { - cy.get('div') - }, { - async validate () { - return new Promise(async (resolve, reject) => { - // Cypress.log('getCurrentSessionData') // throws uncaught exception - const { cookies } = await Cypress.session.getCurrentSessionData() + it('created command validate has .wrap yield false', () => { + cy.session('session_7', () => { + cy.get('div') + }, { + validate () { + return cy.wrap(false) + }, + }) + }) - console.log(cookies) - if (cookies.length === 0) { // this is always zero! we aren't setting any :D - reject(false) - } + it('created command validate has .then with log before yield false ', () => { + cy.session('session_8', () => { + cy.get('div') + }, { + validate () { + cy.window().then((win) => { + console.log(win) + + return false + }) + }, }) - }, + }) }) -}) -it('created command has failing command in validate', () => { - cy.session('session_5', () => { - cy.get('div').as('hello') - }, { - validate () { - cy.get('does_not_exist', { timeout: 1 }) - }, + // unhappy with stack trace + // ISSUE: missing stack trace (no cy commands to reference) + // FIX BY: could (should) fallback to cy.session stack trace but doesn't feel the best + // FIXED BY DROPPING LAST COMMAND: also saying last command yielded false when it resolved false but maybe yielded false is fine + it('created command resolves false', () => { + cy.session('session_2', () => { + cy.get('div') + }, { + async validate () { + return new Promise(async (resolve, reject) => { + // Cypress.log('getCurrentSessionData') // throws uncaught exception + // Cypress.log({ + // name: 'getCurrentSessionData', + // type: 'system', + // state: 'passed', + // }) + const { cookies } = await Cypress.session.getCurrentSessionData() + + console.log(cookies) + if (cookies.length === 0) { // this is always zero! we aren't setting any :D + resolve(false) + } + }) + }, + }) }) -}) -it('created command has failing command in validate', () => { - cy.session('session_6', () => { - cy.get('div') - }, { - validate () { - cy.then(() => { - return false + // missing stack trace (no cy commands) + // unhappy with missing stack trace + // duplicate logs to capture & also fail session + // could just end curr command without failing the rest + it('created command resolves false with log between', () => { + cy.session('session_3', () => { + cy.get('div') + cy.window().then((win) => { + // win.localStorage.setItem('hi', 'there') }) - }, + }, { + async validate () { + // Cypress.log({ + // name: 'getCurrentSessionData', + // message: '', + // type: 'system', + // state: 'passed', + // }) + + return new Promise(async (resolve, reject) => { + Cypress.log('getCurrentSessionData') // throws uncaught exception + + const { cookies } = await Cypress.session.getCurrentSessionData() + + if (cookies.length === 0) { // this is always zero! we aren't setting any :D + resolve(false) + } + }) + }, + }) }) -}) -it('created command has failing command in validate', () => { - cy.session('session_7', () => { - cy.get('div') - }, { - validate () { - return cy.wrap(false) - }, - }) -}) + it.skip('ref', () => { + cy.then(() => { + return new Promise(async (resolve, reject) => { + Cypress.log('getCurrentSessionData') // throws uncaught exception -it('created command has failing command in validate', () => { - cy.session('session_8', () => { - cy.get('div') - }, { - validate () { - cy.window().then((win) => { - console.log(win) + const { cookies } = await Cypress.session.getCurrentSessionData() - return false + if (cookies.length === 0) { // this is always zero! we aren't setting any :D + resolve(false) + } }) - }, + }) }) }) -it('for reference', () => { - cy.get('does_not_exist', { timeout: 1 }) -}) +// it('for reference', () => { +// cy.get('does_not_exist', { timeout: 1 }) +// }) From c8683860a40e18f6fac9094c1734e02b8084ba83 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Fri, 14 Oct 2022 11:52:36 -0500 Subject: [PATCH 09/52] no ci From e25419b164ea296d75dfecc9e1331ce3c5a6e52e Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Fri, 14 Oct 2022 15:57:56 -0500 Subject: [PATCH 10/52] feat: enhance test error UI --- .../e2e/commands/sessions/sessions.cy.js | 4 +- .../driver/src/cy/commands/sessions/index.ts | 2 +- packages/driver/src/cypress/error_utils.ts | 2 +- packages/driver/src/cypress/log.ts | 12 +- packages/reporter/cypress/e2e/commands.cy.ts | 2 +- .../reporter/src/attempts/attempt-model.ts | 27 ++- packages/reporter/src/attempts/attempts.tsx | 10 +- .../reporter/src/commands/command-model.ts | 21 ++- packages/reporter/src/commands/command.tsx | 175 +++++++++--------- packages/reporter/src/commands/commands.scss | 19 +- packages/reporter/src/errors/err-model.ts | 3 + packages/reporter/src/errors/errors.scss | 89 ++++++--- packages/reporter/src/errors/test-error.tsx | 85 +++++---- packages/reporter/src/hooks/hook-model.ts | 4 +- packages/reporter/src/lib/events.ts | 13 +- packages/reporter/src/lib/tag.scss | 2 +- packages/reporter/src/lib/variables.scss | 7 +- packages/server/lib/socket-base.ts | 2 - 18 files changed, 283 insertions(+), 196 deletions(-) diff --git a/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js b/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js index c48f706dea7c..82329e05911f 100644 --- a/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js +++ b/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js @@ -585,7 +585,7 @@ describe('cy.session', { retries: 0 }, () => { }) expect(logs[6].get()).to.deep.contain({ - showError: true, + isRecovered: true, group: validateSessionGroup.id, }) @@ -679,7 +679,7 @@ describe('cy.session', { retries: 0 }, () => { }) expect(logs[6].get()).to.deep.contain({ - showError: true, + isRecovered: true, group: validateSessionGroup.id, }) diff --git a/packages/driver/src/cy/commands/sessions/index.ts b/packages/driver/src/cy/commands/sessions/index.ts index 51629c51746d..6e4c011e7c60 100644 --- a/packages/driver/src/cy/commands/sessions/index.ts +++ b/packages/driver/src/cy/commands/sessions/index.ts @@ -236,8 +236,8 @@ export default function (Commands, Cypress, cy) { // show validation error and allow sessions workflow to recreate the session if (restoreSession) { + err.isRecovered = true Cypress.log({ - showError: true, type: 'system', name: 'session', }) diff --git a/packages/driver/src/cypress/error_utils.ts b/packages/driver/src/cypress/error_utils.ts index d143c1ed337f..d40e497f8669 100644 --- a/packages/driver/src/cypress/error_utils.ts +++ b/packages/driver/src/cypress/error_utils.ts @@ -9,7 +9,7 @@ import $stackUtils, { StackAndCodeFrameIndex } from './stack_utils' import $utils from './utils' import type { HandlerType } from './runner' -const ERROR_PROPS = ['message', 'type', 'name', 'stack', 'parsedStack', 'fileName', 'lineNumber', 'columnNumber', 'host', 'uncaught', 'actual', 'expected', 'showDiff', 'isPending', 'docsUrl', 'codeFrame'] as const +const ERROR_PROPS = ['message', 'type', 'name', 'stack', 'parsedStack', 'fileName', 'lineNumber', 'columnNumber', 'host', 'uncaught', 'actual', 'expected', 'showDiff', 'isPending', 'isRecovered', 'docsUrl', 'codeFrame'] as const const ERR_PREPARED_FOR_SERIALIZATION = Symbol('ERR_PREPARED_FOR_SERIALIZATION') const crossOriginScriptRe = /^script error/i diff --git a/packages/driver/src/cypress/log.ts b/packages/driver/src/cypress/log.ts index 564002f9bb9c..4a66aab3b287 100644 --- a/packages/driver/src/cypress/log.ts +++ b/packages/driver/src/cypress/log.ts @@ -15,7 +15,7 @@ import type { StateFunc } from './state' const groupsOrTableRe = /^(groups|table)$/ const parentOrChildRe = /parent|child|system/ const SNAPSHOT_PROPS = 'id snapshots $el url coords highlightAttr scrollBy viewportWidth viewportHeight'.split(' ') -const DISPLAY_PROPS = 'id alias aliasType callCount displayName end err event functionName groupLevel hookId instrument isStubbed group message method name numElements showError numResponses referencesAlias renderProps sessionInfo state testId timeout type url visible wallClockStartedAt testCurrentRetry'.split(' ') +const DISPLAY_PROPS = 'id alias aliasType callCount displayName end err event functionName groupLevel hookId instrument isStubbed group message method name numElements numResponses referencesAlias renderProps sessionInfo state testId timeout type url visible wallClockStartedAt testCurrentRetry'.split(' ') const BLACKLIST_PROPS = 'snapshots'.split(' ') let counter = 0 @@ -230,7 +230,7 @@ const defaults = function (state: StateFunc, config, obj) { } export class Log { - cy: any + createSnapshot: Function state: StateFunc config: any fireChangeEvent: ((log) => (void | undefined)) @@ -238,8 +238,8 @@ export class Log { private attributes: Record = {} - constructor (cy, state, config, fireChangeEvent, obj) { - this.cy = cy + constructor (createSnapshot, state, config, fireChangeEvent, obj) { + this.createSnapshot = createSnapshot this.state = state this.config = config // only fire the log:state:changed event as fast as every 4ms @@ -391,7 +391,7 @@ export class Log { } } - const snapshot = this.cy.createSnapshot(name, this.get('$el')) + const snapshot = this.createSnapshot(name, this.get('$el')) this.addSnapshot(snapshot, options) @@ -608,7 +608,7 @@ class LogManager { $errUtils.throwErrByPath('log.invalid_argument', { args: { arg: options } }) } - const log = new Log(cy, state, config, this.fireChangeEvent, options) + const log = new Log(cy.createSnapshot, state, config, this.fireChangeEvent, options) log.set(options) diff --git a/packages/reporter/cypress/e2e/commands.cy.ts b/packages/reporter/cypress/e2e/commands.cy.ts index 84bdbeb0a4bd..d94e36626b4e 100644 --- a/packages/reporter/cypress/e2e/commands.cy.ts +++ b/packages/reporter/cypress/e2e/commands.cy.ts @@ -937,7 +937,7 @@ describe('commands', { viewportHeight: 1000 }, () => { name: 'validate', displayMessage: 'mock session validation', state: 'failed', - showError: true, + isRecovered: true, err: _commandErr, type: 'parent', }) diff --git a/packages/reporter/src/attempts/attempt-model.ts b/packages/reporter/src/attempts/attempt-model.ts index ca6a910a5373..39d3f27fb87c 100644 --- a/packages/reporter/src/attempts/attempt-model.ts +++ b/packages/reporter/src/attempts/attempt-model.ts @@ -17,7 +17,7 @@ export default class Attempt { @observable agents: Agent[] = [] @observable sessions: Record = {} @observable commands: Command[] = [] - @observable err = new Err({}) + @observable err: Err = undefined @observable hooks: Hook[] = [] // TODO: make this an enum with states: 'QUEUED, ACTIVE, INACTIVE' @observable isActive: boolean | null = null @@ -49,7 +49,10 @@ export default class Attempt { this.id = props.currentRetry || 0 this.test = test this._state = props.state - this.err.update(props.err) + + if (props.err) { + this.err = new Err(props.err) + } this.invocationDetails = props.invocationDetails @@ -78,6 +81,16 @@ export default class Attempt { return this._state || (this.isActive ? 'active' : 'processing') } + @computed get error () { + const command = this.err.isCommandErr ? this.commandMatchingErr() : undefined + + return { + err: this.err, + testId: command?.testId, + commandId: command?.id, + } + } + @computed get isLast () { return this.id === this.test.lastAttempt.id } @@ -137,7 +150,7 @@ export default class Attempt { } } - commandMatchingErr () { + commandMatchingErr (): Command | undefined { return _(this.hooks) .map((hook) => { return hook.commandMatchingErr(this.err) @@ -155,7 +168,13 @@ export default class Attempt { this._state = props.state } - this.err.update(props.err) + if (props.err) { + if (this.err) { + this.err.update(props.err) + } else { + this.err = new Err(props.err) + } + } if (props.failedFromHookId) { const hook = _.find(this.hooks, { hookId: props.failedFromHookId }) diff --git a/packages/reporter/src/attempts/attempts.tsx b/packages/reporter/src/attempts/attempts.tsx index 2a40627d6c2a..56b338a7bf98 100644 --- a/packages/reporter/src/attempts/attempts.tsx +++ b/packages/reporter/src/attempts/attempts.tsx @@ -55,10 +55,12 @@ function renderAttemptContent (model: AttemptModel, studioActive: boolean) {
    {model.hasCommands ? : }
    -
    - - {studioActive && model.state === 'failed' && } -
    + {model.state === 'failed' && ( +
    + + {studioActive && } +
    + )}
    ) } diff --git a/packages/reporter/src/commands/command-model.ts b/packages/reporter/src/commands/command-model.ts index 889a934d1085..dc4988685702 100644 --- a/packages/reporter/src/commands/command-model.ts +++ b/packages/reporter/src/commands/command-model.ts @@ -35,7 +35,6 @@ export interface CommandProps extends InstrumentProps { wallClockStartedAt?: string hookId: string isStudio?: boolean - showError?: boolean group?: number groupLevel?: number hasSnapshot?: boolean @@ -45,7 +44,7 @@ export interface CommandProps extends InstrumentProps { export default class Command extends Instrument { @observable.struct renderProps: RenderProps = {} @observable.struct sessionInfo?: SessionProps['sessionInfo'] - @observable err = new Err({}) + @observable err?: Err @observable event?: boolean = false @observable isLongRunning = false @observable number?: number @@ -56,7 +55,6 @@ export default class Command extends Instrument { @observable children: Array = [] @observable hookId: string @observable isStudio: boolean - @observable showError?: boolean = false @observable group?: number @observable groupLevel?: number @observable hasSnapshot?: boolean @@ -92,6 +90,7 @@ export default class Command extends Instrument { return this._isOpen || (this._isOpen === null && ( + this.err?.isRecovered || // command has nested commands (this.name !== 'session' && this.hasChildren && !this.event && this.type !== 'system') || // command has nested commands with children @@ -123,7 +122,10 @@ export default class Command extends Instrument { constructor (props: CommandProps) { super(props) - this.err.update(props.err) + if (props.err) { + this.err = new Err(props.err) + } + this.event = props.event this.number = props.number this.numElements = props.numElements @@ -136,7 +138,6 @@ export default class Command extends Instrument { this.wallClockStartedAt = props.wallClockStartedAt this.hookId = props.hookId this.isStudio = !!props.isStudio - this.showError = !!props.showError this.group = props.group this.hasSnapshot = !!props.hasSnapshot this.hasConsoleProps = !!props.hasConsoleProps @@ -148,7 +149,14 @@ export default class Command extends Instrument { update (props: CommandProps) { super.update(props) - this.err.update(props.err) + if (props.err) { + if (!this.err) { + this.err = new Err(props.err) + } else { + this.err.update(props.err) + } + } + this.event = props.event this.numElements = props.numElements this.renderProps = props.renderProps || {} @@ -159,7 +167,6 @@ export default class Command extends Instrument { this.timeout = props.timeout this.hasSnapshot = props.hasSnapshot this.hasConsoleProps = props.hasConsoleProps - this.showError = props.showError this._checkLongRunning() } diff --git a/packages/reporter/src/commands/command.tsx b/packages/reporter/src/commands/command.tsx index 8c18ecca5590..dd0fe2b3a612 100644 --- a/packages/reporter/src/commands/command.tsx +++ b/packages/reporter/src/commands/command.tsx @@ -9,6 +9,7 @@ import Tooltip from '@cypress/react-tooltip' import appState, { AppState } from '../lib/app-state' import events, { Events } from '../lib/events' import FlashOnClick from '../lib/flash-on-click' +import StateIcon from '../lib/state-icon' import Tag from '../lib/tag' import { TimeoutID } from '../lib/types' import runnablesStore, { RunnablesStore } from '../runnables/runnables-store' @@ -258,6 +259,79 @@ interface Props { groupId?: number } +const CommandDetails = observer(({ model, groupId, events, aliasesWithDuplicates }) => { + const commandName = model.name ? nameClassName(model.name) : '' + const displayNumOfElements = model.state !== 'pending' && model.numElements != null && model.numElements !== 1 + const isSystemEvent = model.type === 'system' && model.event + const isSessionCommand = commandName === 'session' + const displayNumOfChildren = !isSystemEvent && !isSessionCommand && model.hasChildren && !model.isOpen + + const _removeStudioCommand = (e: React.MouseEvent) => { + e.preventDefault() + e.stopPropagation() + + events.emit('studio:remove:command', model.number) + } + + return ( + <> + + + + {model.event && model.type !== 'system' ? `(${displayName(model)})` : displayName(model)} + + + {!!groupId && model.type === 'system' && model.state === 'failed' && } + {model.referencesAlias ? + + : + } + + + {model.type === 'parent' && model.isStudio && ( + + )} + {isSessionCommand && ( + + )} + {!model.visible && ( + + + + + + )} + {displayNumOfElements && ( + + )} + + + + {displayNumOfChildren && ( + + )} + + + + ) +}) + @observer class Command extends Component { @observable isOpen: boolean|null = null @@ -276,47 +350,33 @@ class Command extends Component { return null } - if (model.showError) { - // this error is rendered if an error occurs in session validation executed by cy.session - return - } - const commandName = model.name ? nameClassName(model.name) : '' - const displayNumOfElements = model.state !== 'pending' && model.numElements != null && model.numElements !== 1 - const isSystemEvent = model.type === 'system' && model.event - const isSessionCommand = commandName === 'session' - const displayNumOfChildren = !isSystemEvent && !isSessionCommand && model.hasChildren && !model.isOpen - const groupPlaceholder: Array = [] + let groupLevel = 0 + if (model.groupLevel !== undefined) { // cap the group nesting to 5 levels to keep the log text legible - const level = model.groupLevel < 6 ? model.groupLevel : 5 + groupLevel = model.groupLevel < 6 ? model.groupLevel : 5 - for (let i = 1; i < level; i++) { + for (let i = 1; i < groupLevel; i++) { groupPlaceholder.push() } } - return ( -
  • + return (<> +
  • { onMouseLeave={() => this._snapshot(false)} > {groupPlaceholder} - - - - {model.event && model.type !== 'system' ? `(${displayName(model)})` : displayName(model)} - - - {model.referencesAlias ? - - : - } - - - {model.type === 'parent' && model.isStudio && ( - - )} - {isSessionCommand && ( - - )} - {!model.visible && ( - - - - - - )} - {displayNumOfElements && ( - - )} - - - - {displayNumOfChildren && ( - - )} - - +
  • {this._children()} - ) + {model.err?.isRecovered && ( +
  • + )} + ) } _children () { @@ -481,15 +493,6 @@ class Command extends Component { }, 50) } } - - _removeStudioCommand = (e: React.MouseEvent) => { - e.preventDefault() - e.stopPropagation() - - const { model, events } = this.props - - events.emit('studio:remove:command', model.number) - } } export { Aliases, AliasesReferences, Message, Progress } diff --git a/packages/reporter/src/commands/commands.scss b/packages/reporter/src/commands/commands.scss index 0633227df7ed..ed754a4c4d9a 100644 --- a/packages/reporter/src/commands/commands.scss +++ b/packages/reporter/src/commands/commands.scss @@ -138,13 +138,15 @@ } .command-info { + display: -webkit-box; + font-weight: 600; margin-left: 0; overflow: hidden; - width: 100%; - font-weight: 600; + padding-top: 4px; + padding-bottom: 4px; -webkit-line-clamp: 50; -webkit-box-orient: vertical; - display: -webkit-box; + width: 100%; .command-aliases, .command-message { @@ -254,9 +256,9 @@ color: $err-header-text; &:not(.command-type-system) { - border-left: 2px solid $fail; + border-left: $err-border; background-color: $err-header-background; - + &.command-is-interactive:hover { background: rgba($red-400, 0.3); } @@ -279,13 +281,6 @@ } } - .command .runnable-err-wrapper { - padding: 0; - border: 0; - margin: 0; - margin-bottom: 5px; - } - // Custom Styles for Specific Commands .command-name-assert { .command-method { diff --git a/packages/reporter/src/errors/err-model.ts b/packages/reporter/src/errors/err-model.ts index 29aff35b2948..461a05e2c316 100644 --- a/packages/reporter/src/errors/err-model.ts +++ b/packages/reporter/src/errors/err-model.ts @@ -30,6 +30,7 @@ export interface ErrProps { docsUrl: string | string[] templateType: string codeFrame: CodeFrame + isRecovered: boolean } export default class Err { @@ -41,6 +42,7 @@ export default class Err { @observable templateType = '' // @ts-ignore @observable.ref codeFrame: CodeFrame + @observable isRecovered: boolean = false constructor (props?: Partial) { this.update(props) @@ -64,5 +66,6 @@ export default class Err { if (props.parsedStack) this.parsedStack = props.parsedStack if (props.templateType) this.templateType = props.templateType if (props.codeFrame) this.codeFrame = props.codeFrame + this.isRecovered = !!props.isRecovered } } diff --git a/packages/reporter/src/errors/errors.scss b/packages/reporter/src/errors/errors.scss index 2184e69d41f6..394bacf73a41 100644 --- a/packages/reporter/src/errors/errors.scss +++ b/packages/reporter/src/errors/errors.scss @@ -51,8 +51,32 @@ $code-border-radius: 4px; } } - .runnable-err-wrapper { - cursor: default; + .show-recovered-test-err { + .runnable-err-header, + .runnable-err-body { + padding-left: 49px; + display: flex; + + .err-group-block { + border-left: 1px dotted $err-header-text; + border-image-slice: 0 0 0 1; + border-image-source: repeating-linear-gradient(0deg, transparent, $err-header-text, $err-header-text 4px); + padding-right: 12px; + } + } + + .runnable-err-header > .runnable-err-name { + padding: 5px 0; + } + + .runnable-err-body > .err-group-block:last-of-type { + padding-right: 0px; + } + } + + .runnable-err-content { + max-width: 100%; + overflow: scroll; } .studio-err-wrapper { @@ -60,13 +84,14 @@ $code-border-radius: 4px; } .runnable-err { - border-left: 2px solid $fail; background-color: $err-background; + border-left: 2px solid $red-300; clear: both; color: $err-text; font-family: $monospace; + font-style: normal; margin-bottom: 0; - margin-top: 5px; + margin-top: 2px; white-space: pre-wrap; word-break: break-word; user-select: initial; @@ -74,39 +99,46 @@ $code-border-radius: 4px; } .runnable-err-header { - background-color: rgba($red-400, 0.05); + background-color: $err-header-background; display: flex; - justify-content: space-between; - padding: 5px 10px; font-weight: bold; + justify-content: space-between; + padding-left: 16px; + svg { + color: $red-400; + margin-right: 10px; + align-self: center + } .runnable-err-name { - flex-grow: 2; - font-size: 13px; - line-height: 22px; color: $err-header-text; - - svg { - margin-right: 10px; - } + flex: auto; + font-size: 12px; + font-weight: 600; + line-height: 20px; + padding: 5px 0; + padding: 5px 0 5px 12px; } } .runnable-err-docs-url { margin-left: 0.5em; cursor: pointer; - font-family: $font-sans; + font-family: $font-system; } .runnable-err-message { - font-family: $monospace; - font-size: 1em; - padding: 10px; + font-family: $font-system; + font-size: 14px; + font-weight: 400; + padding: 10px 10px 10px 16px; code { background-color: rgba($black, 0.2); border-radius: 4px; color: $err-code-text; + font-size: 12px; + font-family: $monospace; padding: 2px 5px; } @@ -116,11 +148,17 @@ $code-border-radius: 4px; } } + // .runnable-err-content .collapsible { + // padding: 8px; + // border-top: 1px dashed rgba($red-400, 0.1); + // } + .runnable-err-stack-expander { align-items: center; - border-top: 1px solid rgba($red-400, 0.1); + border-top: 1px dashed rgba($red-400, 0.1); display: flex; - + padding: 10px; + flex-wrap: wrap-reverse; .collapsible-header { flex-grow: 1; @@ -147,7 +185,8 @@ $code-border-radius: 4px; div { cursor: pointer; outline: none; - padding: 14px 10px; + // padding: 14px 16px; + padding: 6px 0; width: 100%; .collapsible-header-text { @@ -184,10 +223,12 @@ $code-border-radius: 4px; div { color: $red-300; cursor: pointer; + font-family: $font-system; font-size: 14px; font-weight: 500; height: 100%; - padding: 14px 10px; + // padding: 16px 10px; + // padding: 6px 0; width: 100%; &:focus { @@ -224,7 +265,7 @@ $code-border-radius: 4px; .test-err-code-frame { background-color: $gray-1000; - border: 1px solid rgba($red-400, 0.25); + border: 1px dashed rgba(245, 154, 169, 0.1); border-radius: $code-border-radius; margin: 0 10px 10px; @@ -234,7 +275,7 @@ $code-border-radius: 4px; border-top-right-radius: $code-border-radius; display: block; font-size: 14px; - line-height: 16px; + line-height: 20px; padding: 8px; word-break: break-all; diff --git a/packages/reporter/src/errors/test-error.tsx b/packages/reporter/src/errors/test-error.tsx index 58adae81a397..6c93517f1f19 100644 --- a/packages/reporter/src/errors/test-error.tsx +++ b/packages/reporter/src/errors/test-error.tsx @@ -1,5 +1,6 @@ import _ from 'lodash' import React, { MouseEvent } from 'react' +import cs from 'classnames' import { observer } from 'mobx-react' import Markdown from 'markdown-it' @@ -10,8 +11,7 @@ import ErrorStack from '../errors/error-stack' import events from '../lib/events' import FlashOnClick from '../lib/flash-on-click' import { onEnterOrSpace } from '../lib/util' -import Attempt from '../attempts/attempt-model' -import Command from '../commands/command-model' +import Err from './err-model' import { formattedMessage } from '../commands/command' import WarningIcon from '-!react-svg-loader!@packages/frontend-shared/src/assets/icons/warning_x8.svg' @@ -26,28 +26,33 @@ const DocsUrl = ({ url }: DocsUrlProps) => { const urlArray = _.castArray(url) - return (<> - {_.map(urlArray, (url) => ( - - Learn more - - ))} - ) + return _.map(urlArray, (url) => ( + + Learn more + + )) } interface TestErrorProps { - model: Attempt | Command - onPrintToConsole?: () => void + err: Err + testId?: string + commandId?: number + // the command group level to nest the recovered in-test error + groupLevel: number } -const TestError = observer((props: TestErrorProps) => { +const TestError = (props: TestErrorProps) => { + const { err } = props + + if (!err || !err.displayMessage) return null + const md = new Markdown('zero') md.enable(['backticks', 'emphasis', 'escape']) - const onPrint = props.onPrintToConsole || (() => { - events.emit('show:error', props.model) - }) + const onPrint = () => { + events.emit('show:error', props) + } const _onPrintClick = (e: MouseEvent) => { e.stopPropagation() @@ -55,26 +60,35 @@ const TestError = observer((props: TestErrorProps) => { onPrint() } - const { err } = props.model const { codeFrame } = err - if (!err.displayMessage) return null + const groupPlaceholder: Array = [] + + if (err.isRecovered) { + // cap the group nesting to 5 levels to keep the log text legible + for (let i = 0; i < props.groupLevel; i++) { + groupPlaceholder.push() + } + } return ( -
    -
    -
    -
    - - {err.name} -
    -
    -
    - - +
    +
    + {groupPlaceholder} + +
    + {err.name}
    - {codeFrame && } - {err.stack && +
    +
    + {groupPlaceholder} +
    +
    + + +
    + {codeFrame && } + {err.stack && { > - } + } +
    ) -}) +} + +TestError.defaultProps = { + groupLevel: 0, +} -export default TestError +export default observer(TestError) diff --git a/packages/reporter/src/hooks/hook-model.ts b/packages/reporter/src/hooks/hook-model.ts index 8477d6c1c639..a745c2a1a762 100644 --- a/packages/reporter/src/hooks/hook-model.ts +++ b/packages/reporter/src/hooks/hook-model.ts @@ -117,8 +117,8 @@ export default class Hook implements HookProps { this.commands.splice(commandIndex, 1) } - commandMatchingErr (errToMatch: Err) { - return _(this.commands) + commandMatchingErr (errToMatch: Err): CommandModel | undefined { + return _(this.commands) // @ts-ignore .filter(({ err }) => { return err && err.message === errToMatch.message && err.message !== undefined }) diff --git a/packages/reporter/src/lib/events.ts b/packages/reporter/src/lib/events.ts index 31ccfc959503..9339d036f951 100644 --- a/packages/reporter/src/lib/events.ts +++ b/packages/reporter/src/lib/events.ts @@ -4,7 +4,8 @@ import appState, { AppState } from './app-state' import runnablesStore, { RunnablesStore, RootRunnable, LogProps } from '../runnables/runnables-store' import statsStore, { StatsStore } from '../header/stats-store' import scroller, { Scroller } from './scroller' -import TestModel, { UpdatableTestProps, UpdateTestCallback, TestProps } from '../test/test-model' +import { UpdatableTestProps, UpdateTestCallback, TestProps } from '../test/test-model' +import Err from '../errors/err-model' import type { ReporterStartInfo, ReporterRunState } from '@packages/types' @@ -151,13 +152,11 @@ const events: Events = { runner.emit('runner:console:log', testId, logId) }) - localBus.on('show:error', (test: TestModel) => { - const command = test.err.isCommandErr ? test.commandMatchingErr() : null - + localBus.on('show:error', ({ err, testId, commandId }: { err: Err, testId?: string, commandId?: number }) => { runner.emit('runner:console:error', { - err: test.err, - testId: command?.testId, - logId: command?.id, + err, + testId, + logId: commandId, }) }) diff --git a/packages/reporter/src/lib/tag.scss b/packages/reporter/src/lib/tag.scss index 7105b0af79e7..0951e7f94896 100644 --- a/packages/reporter/src/lib/tag.scss +++ b/packages/reporter/src/lib/tag.scss @@ -6,7 +6,7 @@ font-size: 12px; font-style: normal; font-weight: 500; - line-height: initial; + line-height: 18px; height: 18px; max-width: 200px; overflow: hidden; diff --git a/packages/reporter/src/lib/variables.scss b/packages/reporter/src/lib/variables.scss index 6201b66f5df2..1eba816a1e10 100644 --- a/packages/reporter/src/lib/variables.scss +++ b/packages/reporter/src/lib/variables.scss @@ -108,10 +108,11 @@ $yellow-medium: $orange-800; $link-text: $indigo-600; -$err-background: #2F2434; +$err-background: #2C2036; +$err-border: #2C2036; $err-code-background: rgba($red-400, 0.18); $err-code-text: $red-300; -$err-header-background: #3D2839; +$err-header-background: #3A243B; $err-header-text: $red-300; $err-text: $red-400; @@ -127,5 +128,5 @@ $reporter-contents-min-width: 170px; $font-sans: 'Fira Mono', 'Helvetica Neue', 'Arial', sans-serif; $open-sans: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; -$monospace: Consolas, Monaco, 'Andale Mono', monospace; +$monospace: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono", "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro", "Fira Mono", "Droid Sans Mono", "Courier New", monospace; $font-system: ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"; diff --git a/packages/server/lib/socket-base.ts b/packages/server/lib/socket-base.ts index 590b309cf4e1..a3a647b4ece1 100644 --- a/packages/server/lib/socket-base.ts +++ b/packages/server/lib/socket-base.ts @@ -48,8 +48,6 @@ const reporterEvents = [ // "go:to:file" 'runner:restart', 'runner:abort', - 'runner:console:log', - 'runner:console:error', 'runner:show:snapshot', 'runner:hide:snapshot', 'reporter:restarted', From f09ba08edf88561b2baf608a9815d893eacaa7d1 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Fri, 14 Oct 2022 16:57:48 -0500 Subject: [PATCH 11/52] fix tests --- .../e2e/commands/sessions/sessions.cy.js | 2 - packages/reporter/cypress/e2e/commands.cy.ts | 80 ++++++++++++++++--- .../reporter/cypress/e2e/test_errors.cy.ts | 5 +- .../reporter/cypress/e2e/unit/err_model.cy.ts | 6 ++ .../reporter/cypress/e2e/unit/events.cy.ts | 29 ++----- .../reporter/src/attempts/attempt-model.ts | 9 ++- packages/reporter/src/errors/errors.scss | 2 +- packages/reporter/src/lib/variables.scss | 2 +- 8 files changed, 93 insertions(+), 42 deletions(-) diff --git a/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js b/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js index 82329e05911f..a77ca3303190 100644 --- a/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js +++ b/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js @@ -585,7 +585,6 @@ describe('cy.session', { retries: 0 }, () => { }) expect(logs[6].get()).to.deep.contain({ - isRecovered: true, group: validateSessionGroup.id, }) @@ -679,7 +678,6 @@ describe('cy.session', { retries: 0 }, () => { }) expect(logs[6].get()).to.deep.contain({ - isRecovered: true, group: validateSessionGroup.id, }) diff --git a/packages/reporter/cypress/e2e/commands.cy.ts b/packages/reporter/cypress/e2e/commands.cy.ts index d94e36626b4e..fffa69a6153d 100644 --- a/packages/reporter/cypress/e2e/commands.cy.ts +++ b/packages/reporter/cypress/e2e/commands.cy.ts @@ -926,24 +926,30 @@ describe('commands', { viewportHeight: 1000 }, () => { context('test error', () => { // this is a unique error permutation currently only observed in cy.session() where an error - // message should be presented during the session validation of a saves/restored session and the - // session command will attempt to recreate a valid session. - it('renders error instead of command', () => { - // font-family: $font-system; + // message should be presented if the session validation fails for a restored session because the + // session command recover and attempt to recreate a valid session. + it('renders recovered error for command', () => { cy.fixture('command_error').then((_commandErr) => { + const groupId = addCommand(runner, { + name: 'session', + message: 'mock restore', + state: 'passed', + type: 'system', + }) + addCommand(runner, { - id: 10, - number: 7, + type: 'system', name: 'validate', - displayMessage: 'mock session validation', state: 'failed', - isRecovered: true, - err: _commandErr, - type: 'parent', + err: { + ..._commandErr, + isRecovered: true, + }, + groupLevel: 1, + group: groupId, }) addCommand(runner, { - id: 11, name: 'recreate session', message: 'mock recreate session cmd', state: 'success', @@ -955,6 +961,58 @@ describe('commands', { viewportHeight: 1000 }, () => { cy.contains('recreate session') cy.percySnapshot() }) + + it('renders recovered error for nested group command', () => { + cy.fixture('command_error').then((_commandErr) => { + const groupId = addCommand(runner, { + name: 'session', + message: 'mock restore', + state: 'passed', + type: 'system', + }) + + const nested = addCommand(runner, { + type: 'system', + name: 'validate', + state: 'failed', + group: groupId, + }) + + addCommand(runner, { + number: 8, + name: 'get', + message: 'does_not_exist', + state: 'failed', + err: { + ..._commandErr, + isRecovered: true, + }, + type: 'parent', + groupLevel: 2, + group: nested, + }) + + addCommand(runner, { + id: 12, + name: 'recreate session', + message: 'mock recreate session cmd', + state: 'success', + type: 'parent', + }) + }) + + cy.contains('.command', 'validate').as('validate') + .find('.command-expander') + .should('have.class', 'command-expander-is-open') + + cy.get('@validate').within(() => { + cy.contains('CommandError') + }) + + cy.contains('recreate session') + + cy.percySnapshot() + }) }) context('studio commands', () => { diff --git a/packages/reporter/cypress/e2e/test_errors.cy.ts b/packages/reporter/cypress/e2e/test_errors.cy.ts index 0634480fc91e..d2bd63474341 100644 --- a/packages/reporter/cypress/e2e/test_errors.cy.ts +++ b/packages/reporter/cypress/e2e/test_errors.cy.ts @@ -227,12 +227,13 @@ describe('test errors', () => { // NOTE: still needs to be implemented it.skip('renders and escapes markdown with leading/trailing whitespace', () => { + setError(commandErr) cy.get('.runnable-err-message') // https://github.com/cypress-io/cypress/issues/1360 // renders ** buzz ** as buzz - .contains('code', 'foo') - .and('not.contain', '`foo`') + .contains('strong', 'buzz') + .and('not.contain', '** buzz **') }) }) diff --git a/packages/reporter/cypress/e2e/unit/err_model.cy.ts b/packages/reporter/cypress/e2e/unit/err_model.cy.ts index 85f228f9468b..86d687b3096f 100644 --- a/packages/reporter/cypress/e2e/unit/err_model.cy.ts +++ b/packages/reporter/cypress/e2e/unit/err_model.cy.ts @@ -69,6 +69,12 @@ describe('Err model', () => { expect(err.stack).to.equal('the stack (path/to/file.js 45:203)') }) + it('updates isRecovered if specified', () => { + expect(err.isRecovered).to.be.false + err.update({ isRecovered: true }) + expect(err.isRecovered).to.be.true + }) + it('does nothing if props is undefined', () => { err.update() expect(err.name).to.equal('BadError') diff --git a/packages/reporter/cypress/e2e/unit/events.cy.ts b/packages/reporter/cypress/e2e/unit/events.cy.ts index 9c666dfe34fb..de05377037e6 100644 --- a/packages/reporter/cypress/e2e/unit/events.cy.ts +++ b/packages/reporter/cypress/e2e/unit/events.cy.ts @@ -306,22 +306,19 @@ describe('events', () => { expect(runner.emit).to.have.been.calledWith('runner:console:log', 'command id') }) - it('emits runner:console:error with test id on show:error', () => { - const test = { err: { isCommandErr: false } } + it('emits runner:console:error with error on show:error', () => { + const errorDetails = { err: { stack: 'hi' } } - runnablesStore.testById.returns(test) - events.emit('show:error', test) + events.emit('show:error', errorDetails) expect(runner.emit).to.have.been.calledWith('runner:console:error', { - err: test.err, + err: errorDetails.err, testId: undefined, logId: undefined, }) }) - it('emits runner:console:error with test id and command id on show:error when it is a command error and there is a matching command', () => { - const test = { err: { isCommandErr: true }, commandMatchingErr: () => { - return { id: 'matching command id', testId: 'test' } - } } + it('emits runner:console:error with error, test id and command id on show:error ', () => { + const test = { err: { isCommandErr: true }, commandId: 'matching command id', testId: 'test' } runnablesStore.testById.returns(test) events.emit('show:error', test) @@ -332,20 +329,6 @@ describe('events', () => { }) }) - it('emits runner:console:error with test id on show:error when it is a command error but there not a matching command', () => { - const test = { err: { isCommandErr: true }, commandMatchingErr: () => { - return null - } } - - runnablesStore.testById.returns(test) - events.emit('show:error', test) - expect(runner.emit).to.have.been.calledWith('runner:console:error', { - err: test.err, - testId: undefined, - logId: undefined, - }) - }) - it('emits runner:show:snapshot on show:snapshot', () => { events.emit('show:snapshot', 'command id') expect(runner.emit).to.have.been.calledWith('runner:show:snapshot', 'command id') diff --git a/packages/reporter/src/attempts/attempt-model.ts b/packages/reporter/src/attempts/attempt-model.ts index 39d3f27fb87c..59716cb78084 100644 --- a/packages/reporter/src/attempts/attempt-model.ts +++ b/packages/reporter/src/attempts/attempt-model.ts @@ -17,7 +17,7 @@ export default class Attempt { @observable agents: Agent[] = [] @observable sessions: Record = {} @observable commands: Command[] = [] - @observable err: Err = undefined + @observable err?: Err = undefined @observable hooks: Hook[] = [] // TODO: make this an enum with states: 'QUEUED, ACTIVE, INACTIVE' @observable isActive: boolean | null = null @@ -82,7 +82,7 @@ export default class Attempt { } @computed get error () { - const command = this.err.isCommandErr ? this.commandMatchingErr() : undefined + const command = this.err?.isCommandErr ? this.commandMatchingErr() : undefined return { err: this.err, @@ -151,8 +151,13 @@ export default class Attempt { } commandMatchingErr (): Command | undefined { + if (!this.err) { + return undefined + } + return _(this.hooks) .map((hook) => { + // @ts-ignore return hook.commandMatchingErr(this.err) }) .compact() diff --git a/packages/reporter/src/errors/errors.scss b/packages/reporter/src/errors/errors.scss index 394bacf73a41..bf39ae5ccd73 100644 --- a/packages/reporter/src/errors/errors.scss +++ b/packages/reporter/src/errors/errors.scss @@ -75,7 +75,7 @@ $code-border-radius: 4px; } .runnable-err-content { - max-width: 100%; + width: 100%; overflow: scroll; } diff --git a/packages/reporter/src/lib/variables.scss b/packages/reporter/src/lib/variables.scss index 1eba816a1e10..78f1c2e01f5d 100644 --- a/packages/reporter/src/lib/variables.scss +++ b/packages/reporter/src/lib/variables.scss @@ -109,7 +109,7 @@ $yellow-medium: $orange-800; $link-text: $indigo-600; $err-background: #2C2036; -$err-border: #2C2036; +$err-border: 2px solid #2C2036; $err-code-background: rgba($red-400, 0.18); $err-code-text: $red-300; $err-header-background: #3A243B; From 85a2b076e45d977f996f6d814c2536e351757c0a Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Fri, 14 Oct 2022 17:17:02 -0500 Subject: [PATCH 12/52] Break this up a bit more --- packages/reporter/src/commands/command.tsx | 176 +++++++++++---------- 1 file changed, 90 insertions(+), 86 deletions(-) diff --git a/packages/reporter/src/commands/command.tsx b/packages/reporter/src/commands/command.tsx index dd0fe2b3a612..dc20ca5bff7b 100644 --- a/packages/reporter/src/commands/command.tsx +++ b/packages/reporter/src/commands/command.tsx @@ -259,8 +259,22 @@ interface Props { groupId?: number } -const CommandDetails = observer(({ model, groupId, events, aliasesWithDuplicates }) => { - const commandName = model.name ? nameClassName(model.name) : '' +const CommandDetails = observer(({ model, groupId, aliasesWithDuplicates }) => ( + + + + {model.event && model.type !== 'system' ? `(${displayName(model)})` : displayName(model)} + + + {!!groupId && model.type === 'system' && model.state === 'failed' && } + {model.referencesAlias ? + + : + } + +)) + +const CommandControls = observer(({ model, commandName, events }) => { const displayNumOfElements = model.state !== 'pending' && model.numElements != null && model.numElements !== 1 const isSystemEvent = model.type === 'system' && model.event const isSessionCommand = commandName === 'session' @@ -274,61 +288,48 @@ const CommandDetails = observer(({ model, groupId, events, aliasesWithDuplicates } return ( - <> - - + + + {model.type === 'parent' && model.isStudio && ( + + )} + {isSessionCommand && ( + + )} + {!model.visible && ( + - {model.event && model.type !== 'system' ? `(${displayName(model)})` : displayName(model)} + - - {!!groupId && model.type === 'system' && model.state === 'failed' && } - {model.referencesAlias ? - - : - } - - - {model.type === 'parent' && model.isStudio && ( - - )} - {isSessionCommand && ( - - )} - {!model.visible && ( - - - - - - )} - {displayNumOfElements && ( + + )} + {displayNumOfElements && ( + + )} + + + + {displayNumOfChildren && ( )} - - - - {displayNumOfChildren && ( - - )} - - + ) }) @@ -364,44 +365,47 @@ class Command extends Component { } } - return (<> -
  • -
    - - +
  • +
    -
    this._snapshot(true)} - onMouseLeave={() => this._snapshot(false)} + + - {groupPlaceholder} - -
    - -
    - - {this._children()} -
  • - {model.err?.isRecovered && ( -
  • - )} - ) +
    this._snapshot(true)} + onMouseLeave={() => this._snapshot(false)} + > + {groupPlaceholder} + + +
    + +
    + + {this._children()} + + {model.err?.isRecovered && ( +
  • + )} + + ) } _children () { From 2b593de0d5fb44c2929137e42dbd89a5f17e8d3e Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Fri, 14 Oct 2022 17:35:47 -0500 Subject: [PATCH 13/52] clean up --- packages/reporter/src/errors/errors.scss | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/reporter/src/errors/errors.scss b/packages/reporter/src/errors/errors.scss index bf39ae5ccd73..4cc1a74d1c80 100644 --- a/packages/reporter/src/errors/errors.scss +++ b/packages/reporter/src/errors/errors.scss @@ -148,11 +148,6 @@ $code-border-radius: 4px; } } - // .runnable-err-content .collapsible { - // padding: 8px; - // border-top: 1px dashed rgba($red-400, 0.1); - // } - .runnable-err-stack-expander { align-items: center; border-top: 1px dashed rgba($red-400, 0.1); From db28fd16874c382df15052e2bcc368988cf338ba Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Mon, 17 Oct 2022 09:51:58 -0500 Subject: [PATCH 14/52] minor style fixes in alignment & border color --- packages/reporter/src/errors/errors.scss | 15 +++++++-------- packages/reporter/src/lib/variables.scss | 2 +- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/reporter/src/errors/errors.scss b/packages/reporter/src/errors/errors.scss index 4cc1a74d1c80..8ad3c83520a0 100644 --- a/packages/reporter/src/errors/errors.scss +++ b/packages/reporter/src/errors/errors.scss @@ -85,7 +85,7 @@ $code-border-radius: 4px; .runnable-err { background-color: $err-background; - border-left: 2px solid $red-300; + border-left: $err-border; clear: both; color: $err-text; font-family: $monospace; @@ -103,21 +103,20 @@ $code-border-radius: 4px; display: flex; font-weight: bold; justify-content: space-between; - padding-left: 16px; + padding-left: 18px; svg { color: $red-400; - margin-right: 10px; align-self: center } + .runnable-err-name { color: $err-header-text; flex: auto; font-size: 12px; font-weight: 600; line-height: 20px; - padding: 5px 0; - padding: 5px 0 5px 12px; + padding: 5px 0 5px 24px; } } @@ -131,7 +130,7 @@ $code-border-radius: 4px; font-family: $font-system; font-size: 14px; font-weight: 400; - padding: 10px 10px 10px 16px; + padding: 10px 10px 10px 18px; code { background-color: rgba($black, 0.2); @@ -152,7 +151,7 @@ $code-border-radius: 4px; align-items: center; border-top: 1px dashed rgba($red-400, 0.1); display: flex; - padding: 10px; + padding: 10px 18px; flex-wrap: wrap-reverse; .collapsible-header { flex-grow: 1; @@ -262,7 +261,7 @@ $code-border-radius: 4px; background-color: $gray-1000; border: 1px dashed rgba(245, 154, 169, 0.1); border-radius: $code-border-radius; - margin: 0 10px 10px; + margin: 0 18px 10px; .runnable-err-file-path { background: rgba($gray-900, 0.5); diff --git a/packages/reporter/src/lib/variables.scss b/packages/reporter/src/lib/variables.scss index 78f1c2e01f5d..5dc085420b5d 100644 --- a/packages/reporter/src/lib/variables.scss +++ b/packages/reporter/src/lib/variables.scss @@ -109,7 +109,7 @@ $yellow-medium: $orange-800; $link-text: $indigo-600; $err-background: #2C2036; -$err-border: 2px solid #2C2036; +$err-border: 2px solid $red-300; $err-code-background: rgba($red-400, 0.18); $err-code-text: $red-300; $err-header-background: #3A243B; From fdda301cb58399264fd45a747daa3f1976564dee Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Mon, 17 Oct 2022 11:20:59 -0500 Subject: [PATCH 15/52] minor padding tweaks --- packages/reporter/src/commands/command.tsx | 2 +- packages/reporter/src/commands/commands.scss | 8 ++++++-- packages/reporter/src/errors/errors.scss | 21 ++++++++++---------- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/packages/reporter/src/commands/command.tsx b/packages/reporter/src/commands/command.tsx index dc20ca5bff7b..671f98f9decd 100644 --- a/packages/reporter/src/commands/command.tsx +++ b/packages/reporter/src/commands/command.tsx @@ -266,7 +266,7 @@ const CommandDetails = observer(({ model, groupId, aliasesWithDuplicates }) => ( {model.event && model.type !== 'system' ? `(${displayName(model)})` : displayName(model)} - {!!groupId && model.type === 'system' && model.state === 'failed' && } + {!!groupId && model.type === 'system' && model.state === 'failed' && } {model.referencesAlias ? : diff --git a/packages/reporter/src/commands/commands.scss b/packages/reporter/src/commands/commands.scss index ed754a4c4d9a..ecc29616a416 100644 --- a/packages/reporter/src/commands/commands.scss +++ b/packages/reporter/src/commands/commands.scss @@ -131,9 +131,9 @@ .command-group-block { @include nested-command-dashes($gray-600); - width: 11px; + width: 12px; min-height: 28px; - min-width: 11px; + min-width: 12px; } } @@ -270,6 +270,10 @@ color: $err-header-text; } + .failed-indicator { + vertical-align: middle; + } + .command-group { border-color: $err-header-text; @include nested-command-dashes($err-header-text); diff --git a/packages/reporter/src/errors/errors.scss b/packages/reporter/src/errors/errors.scss index 8ad3c83520a0..369a5ad2cfd1 100644 --- a/packages/reporter/src/errors/errors.scss +++ b/packages/reporter/src/errors/errors.scss @@ -61,22 +61,24 @@ $code-border-radius: 4px; border-left: 1px dotted $err-header-text; border-image-slice: 0 0 0 1; border-image-source: repeating-linear-gradient(0deg, transparent, $err-header-text, $err-header-text 4px); - padding-right: 12px; + width: 13px; + min-width: 13px; } } .runnable-err-header > .runnable-err-name { - padding: 5px 0; + padding: 5px 4px 5px 15px; } - .runnable-err-body > .err-group-block:last-of-type { - padding-right: 0px; + .runnable-err-content { + padding: 0 12px 0 0; } } .runnable-err-content { width: 100%; overflow: scroll; + padding: 0 18px; } .studio-err-wrapper { @@ -116,7 +118,7 @@ $code-border-radius: 4px; font-size: 12px; font-weight: 600; line-height: 20px; - padding: 5px 0 5px 24px; + padding: 5px 4px 5px 24px; } } @@ -130,7 +132,7 @@ $code-border-radius: 4px; font-family: $font-system; font-size: 14px; font-weight: 400; - padding: 10px 10px 10px 18px; + padding: 10px 0; code { background-color: rgba($black, 0.2); @@ -151,7 +153,7 @@ $code-border-radius: 4px; align-items: center; border-top: 1px dashed rgba($red-400, 0.1); display: flex; - padding: 10px 18px; + padding: 10px 0; flex-wrap: wrap-reverse; .collapsible-header { flex-grow: 1; @@ -179,7 +181,6 @@ $code-border-radius: 4px; div { cursor: pointer; outline: none; - // padding: 14px 16px; padding: 6px 0; width: 100%; @@ -221,8 +222,6 @@ $code-border-radius: 4px; font-size: 14px; font-weight: 500; height: 100%; - // padding: 16px 10px; - // padding: 6px 0; width: 100%; &:focus { @@ -261,7 +260,7 @@ $code-border-radius: 4px; background-color: $gray-1000; border: 1px dashed rgba(245, 154, 169, 0.1); border-radius: $code-border-radius; - margin: 0 18px 10px; + margin: 0 0 10px; .runnable-err-file-path { background: rgba($gray-900, 0.5); From 0ab88469591615f979ac9421064f75ea2ebb5808 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Tue, 18 Oct 2022 16:13:40 -0500 Subject: [PATCH 16/52] tests for create session setup / validation errors --- .../app/cypress/e2e/runner/sessions.ui.cy.ts | 136 ++++--- .../driver/src/cy/commands/sessions/index.ts | 96 +++-- packages/driver/src/cypress/command_queue.ts | 42 ++- packages/driver/src/cypress/log.ts | 3 - packages/reporter/src/errors/err-model.ts | 2 +- .../cypress/e2e/session/error-in-progress.js | 342 ++++++++++++++++++ .../cypress/e2e/session/errors.cy.js | 219 ++++------- 7 files changed, 600 insertions(+), 240 deletions(-) create mode 100644 system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/error-in-progress.js diff --git a/packages/app/cypress/e2e/runner/sessions.ui.cy.ts b/packages/app/cypress/e2e/runner/sessions.ui.cy.ts index bde3ead3452c..f076efa0bfd5 100644 --- a/packages/app/cypress/e2e/runner/sessions.ui.cy.ts +++ b/packages/app/cypress/e2e/runner/sessions.ui.cy.ts @@ -332,71 +332,121 @@ describe('runner/cypress sessions.ui.spec', { }) describe('errors', () => { + const assertTestError = (testAlias: string, errMessage: string, errMessagePostfix: string) => { + cy.get(testAlias) + .find('.attempt-error-region') + .contains(errMessage) + .contains(errMessagePostfix) + } + describe.only('created session', () => { - it('has test error when setup has failing Cypress command', () => { + before(() => { loadSpec({ projectName: 'session-and-origin-e2e-specs', filePath: 'session/errors.cy.js', - failCount: 2, + // passCount: 1, + // failCount: 12, + failCount: 7, }) + }) - cy.contains('.test', 'setup has failing command').as('setup_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('@setup_failed').should('have.attr', 'data-model-state', 'failed') + 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).should('contain', 'session_1').as('session_command') + // 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.get('.command-name-session') - .should('contain', 'Create new session') - .get('.command-child-container').should('exist') + cy.contains('.command-wrapper', 'Create new session') + .should('have.class', 'command-state-failed') + .find('.failed-indicator') + .should('exist') }) }) - // has error - cy.get('@setup_failed').contains('This error occurred while creating session. Because the session setup failed, we failed the test.') + const setupError = 'This error occurred while creating the session. Because the session setup failed, we failed the test.' + + assertTestError('@example_test', 'Expected to find element', setupError) }) describe('failed validation', () => { - it('has test error when validate returned false', () => { - // test marked as failed - // test is expanded - // session is marked as 'failed' - // validates group is expanded - }) - - it('test error when validate resolved false', () => { - // test marked as failed - // test is expanded - // session is marked as 'failed' - // validates group is expanded - }) + const validateErr = 'This error occurred while validating the created session. Because validation failed immediately after creating the session, we failed the test.' - it('test error when validate rejected with false', () => { - // test marked as failed - // test is expanded - // session is marked as 'failed' - // validates group is expanded - }) - - it('test error when validate threw error', () => { - // test marked as failed - // test is expanded - // session is marked as 'failed' - // validates group is expanded - }) + const assertCreateSessionValidationFailed = (testAlias: string) => { + cy.get(testAlias) + .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') + .should('have.class', 'command-state-failed') + .find('.failed-indicator') + .should('exist') + }) + }) + } - it('test error when validate has failing Cypress command', () => { - // test marked as failed - // test is expanded - // session is marked as 'failed' - // validates group is expanded + ;[ + { + testCase: 'has failing Cypress command', + systemTestTitle: 'created command - validate - has failing Cypress command', + errMessage: 'Expected to find element', + }, + { + testCase: 'command yields false', + systemTestTitle: 'created command - validate - command yields false', + errMessage: 'callback yielded false.', + }, + { + testCase: 'has multiple commands and yields false', + systemTestTitle: 'created command - validate - has multiple commands and yields false', + errMessage: 'callback yielded false.', + }, + { + testCase: 'rejects with false', + systemTestTitle: 'created command - validate - rejects with false', + errMessage: 'rejected with false.', + }, + { + testCase: 'promise resolved false', + systemTestTitle: 'created command - validate - promise resolves false', + errMessage: 'promise resolved false.', + }, + { + testCase: 'throws an error', + systemTestTitle: 'created command - 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') + assertCreateSessionValidationFailed('@example_test') + assertTestError('@example_test', errMessage, validateErr) + }) }) }) }) diff --git a/packages/driver/src/cy/commands/sessions/index.ts b/packages/driver/src/cy/commands/sessions/index.ts index 29cb3f946100..68424b0053f1 100644 --- a/packages/driver/src/cy/commands/sessions/index.ts +++ b/packages/driver/src/cy/commands/sessions/index.ts @@ -39,6 +39,48 @@ export default function (Commands, Cypress, cy) { const sessionsManager = new SessionsManager(Cypress, cy) const sessions = sessionsManager.sessions + type SESSION_STEPS = 'create' | 'restore' | 'recreate' | 'validate' + 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.`) + } + }, + } + Cypress.on('run:start', () => { // @ts-ignore Object.values(Cypress.state('activeSessions') || {}).forEach((sessionData: ServerSessionData) => { @@ -160,10 +202,10 @@ export default function (Commands, Cypress, cy) { }) } - function createSession (existingSession, 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', @@ -175,7 +217,7 @@ export default function (Commands, Cypress, cy) { setupLogGroup.set({ state: 'failed', consoleProps: { - Step: 'Create New Session', + Step: statusMap.stepName(step), Error: err.stack || err.message, }, }) @@ -197,12 +239,12 @@ export default function (Commands, Cypress, cy) { _.extend(existingSession, data) existingSession.hydrated = true await sessions.saveSessionData(existingSession) - console.log('SET CREATE NEW SESSION PROPS') + _log.set({ consoleProps: () => getConsoleProps(existingSession) }) setupLogGroup.set({ consoleProps: () => { return { - Step: 'Create new session', + Step: statusMap.stepName(step), ...getConsoleProps(existingSession), } }, @@ -234,7 +276,7 @@ export default function (Commands, Cypress, cy) { }) } - function validateSession (existingSession, sessionStatus: 'created' | 'restored' | 'recreated' = 'created') { + function validateSession (existingSession, step: SESSION_STEPS) { const isValidSession = true if (!existingSession.validate) { @@ -259,18 +301,20 @@ export default function (Commands, Cypress, cy) { Cypress.state('onFail', null) // show validation error and allow sessions workflow to recreate the session - if (sessionStatus === 'restored') { - $errUtils.modifyErrMsg(err, `\n\nThis error occurred while validating the restored session. Because validation failed, we will try to recreate the session.`, _.add) + if (step === 'restore') { + $errUtils.modifyErrMsg(err, `\n\nThis error occurred while validating the restoring session. Because validation failed, we will try to recreate the session.`, _.add) err.isRecovered = true - Cypress.log({ - type: 'system', - name: 'session', - displayName: 'validate result', - message: '', + validateLog.set({ + state: 'failed', + consoleProps: (args) => { + return { + Error: err.stack, + } + }, + error: err, }) - .error(err) return !isValidSession } @@ -298,19 +342,19 @@ export default function (Commands, Cypress, cy) { }) } - $errUtils.modifyErrMsg(err, `\n\nThis error occurred while validating the ${sessionStatus} session. Because validation failed immediately after ${sessionStatus === 'created' ? 'creating' : 'recreating'} the session, we failed the test.`, _.add) + $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) // console.log('before throw', err) throw err } - return validate(existingSession, sessionStatus, onSuccess, onFail) + return validate(existingSession, step, onSuccess, onFail) }) }) } // uses Cypress hackery to resolve `false` if validate() resolves/returns false or throws/fails a cypress command. - function validate (existingSession, sessionStatus, onSuccess, onFail) { + function validate (existingSession, step, onSuccess, onFail) { let returnVal Cypress.state('onFail', (err) => { @@ -361,7 +405,7 @@ export default function (Commands, Cypress, cy) { 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)}` }) + err = $errUtils.errByPath('sessions.validate_callback_false', { reason: `promise rejected with ${String(err)}` }) } return onFail(err) @@ -375,7 +419,7 @@ export default function (Commands, Cypress, cy) { // set current command to cy.session for more accurate codeframe cy.state('current', sessionCommand) - return onFail($errUtils.errByPath('sessions.validate_callback_false', { reason: 'callback\'s last command yielded false' }), val) + return onFail($errUtils.errByPath('sessions.validate_callback_false', { reason: 'callback yielded false' }), val) } } @@ -388,22 +432,22 @@ export default function (Commands, Cypress, cy) { * 1. create session * 2. validate session */ - const createSessionWorkflow = (existingSession, sessionStatus: 'creating' | 'recreating' = 'creating') => { + const createSessionWorkflow = (existingSession, step: 'create' | 'recreate' = 'create') => { return cy.then(async () => { - setSessionLogStatus(sessionStatus) + setSessionLogStatus(statusMap.inProgress(step)) await navigateAboutBlank() await sessions.clearCurrentSessionData() - return createSession(existingSession, sessionStatus === 'recreating') + return createSession(existingSession, step) }) - .then(() => validateSession(existingSession, sessionStatus === 'recreating' ? 'recreated' : 'created')) + .then(() => validateSession(existingSession, step)) .then((isValidSession: boolean) => { if (!isValidSession) { return } - setSessionLogStatus(sessionStatus === 'recreating' ? 'recreated' : 'created') + setSessionLogStatus(statusMap.complete(step)) }) } @@ -421,10 +465,10 @@ export default function (Commands, Cypress, cy) { return restoreSession(existingSession) }) - .then(() => validateSession(existingSession, 'restored')) + .then(() => validateSession(existingSession, 'restore')) .then((isValidSession: boolean) => { if (!isValidSession) { - return createSessionWorkflow(existingSession, 'recreating') + return createSessionWorkflow(existingSession, 'recreate') } setSessionLogStatus('restored') diff --git a/packages/driver/src/cypress/command_queue.ts b/packages/driver/src/cypress/command_queue.ts index 26a23d8ee02f..82c51ec37e48 100644 --- a/packages/driver/src/cypress/command_queue.ts +++ b/packages/driver/src/cypress/command_queue.ts @@ -31,28 +31,36 @@ const commandRunningFailed = (Cypress, state, err) => { } const current = state('current') + const lastLog = _.last(current?.get('logs') || []) - 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) { + return lastLog.set({ consoleProps }).error(err) + } - return consoleProps - }, + return Cypress.log({ + end: true, + snapshot: true, + error: err, + consoleProps, }) } diff --git a/packages/driver/src/cypress/log.ts b/packages/driver/src/cypress/log.ts index 4fabb242044d..e2f843cad028 100644 --- a/packages/driver/src/cypress/log.ts +++ b/packages/driver/src/cypress/log.ts @@ -403,9 +403,6 @@ export class Log { this.endGroup() } - console.log('err', err) - console.log('err', err.showRecoveredError) - this.set({ ended: true, error: err, diff --git a/packages/reporter/src/errors/err-model.ts b/packages/reporter/src/errors/err-model.ts index 6f7a8ae970dc..668affb17dfa 100644 --- a/packages/reporter/src/errors/err-model.ts +++ b/packages/reporter/src/errors/err-model.ts @@ -25,7 +25,7 @@ export interface CodeFrame extends FileDetails { export interface ErrProps { name: string message: string - showRecoveredError: boolean + isRecovered: boolean stack: string parsedStack: ParsedStackLine[] docsUrl: string | string[] diff --git a/system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/error-in-progress.js b/system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/error-in-progress.js new file mode 100644 index 000000000000..0b69296166db --- /dev/null +++ b/system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/error-in-progress.js @@ -0,0 +1,342 @@ +/** + * Used in cy-in-cy tests in @packages/app. + */ + +before(async () => { + Cypress.state('activeSessions', {}) + await Cypress.session.clearAllSavedSessions() +}) + +// afterEach(() => { +// console.log(Cypress.state('runnable')) +// expect(Cypress.state('runnable').ctx.currentTest.state).to.eq('failed') +// }) + +describe('create session', () => { + describe('seems correct', () => { + it('setup has failing command', () => { + cy.session('session_1', () => { + cy.get('does_not_exist', { timeout: 1 }) + }) + }) + + it.only('created command validate threw error', () => { + cy.session('session_4', () => { + cy.get('div') + }, { + async validate () { + cy.get('div') + .within(() => { + throw new Error('Something went wrong!') + }) + }, + }) + }) + + it('created command reject false', () => { + cy.session('session_4', () => { + cy.get('div') + }, { + async validate () { + return new Promise(async (resolve, reject) => { + // Cypress.log('getCurrentSessionData') // throws uncaught exception + const { cookies } = await Cypress.session.getCurrentSessionData() + + if (cookies.length === 0) { // this is always zero! we aren't setting any :D + reject(false) + } + }) + }, + }) + }) + + it('recreated reject false', function () { + let count = 0 + const validate = () => { + return new Promise(async (resolve, reject) => { + // Cypress.log('getCurrentSessionData') // throws uncaught exception + const { cookies } = await Cypress.session.getCurrentSessionData() + + if (count === 0) { + resolve(true) + } + + if (count > 0 && cookies.length === 0) { // this is always zero! we aren't setting any :D + reject(false) + } + }) + } + + cy.session('session_44', function () { + cy.get('div').as('hello') + }, { validate }) + + cy.then(() => { + console.log('ABOUT TO CREATE NEW SESSION') + console.log('') + count = 1 + }) + + cy.session('session_44', function () { + cy.get('div').as('hello') + }, { validate }) + }) + + // FIX ME!!!! + it('created command validate has failing command', function () { + cy.log('hi') + cy.session('session_5', function () { + cy.get('div').as('hello') + }, { + validate () { + cy.get('does_not_exist', { timeout: 1 }) + // cy.get('does_not_exist_2', { timeout: 1 }) + }, + }) + }) + + // FIX ME!!!! + it('created command validate has failing command', function () { + let count = 0 + + cy.session('session_55', function () { + cy.get('div').as('hello') + }, { + validate () { + if (count > 0) { + count++ + cy.get('does_not_exist', { timeout: 1 }) + // cy.get('does_not_exist_2', { timeout: 1 }) + } + }, + }) + + cy.then(() => { + console.log('ABOUT TO CREATE NEW SESSION') + console.log('') + count = 1 + }) + + cy.session('session_55', function () { + cy.get('div').as('hello') + }, { + validate () { + if (count > 0) { + count++ + cy.get('does_not_exist', { timeout: 1 }) + // cy.get('does_not_exist_2', { timeout: 1 }) + } + }, + }) + }) + + // correct + it('created command validate has .then yield false', () => { + cy.session('session_6', () => { + cy.get('div') + }, { + validate () { + cy.then(() => { + return false + }) + }, + }) + }) + + // correct + it('recreated command validate has .then yield false ', () => { + let yieldTF = true + + cy.session('session_66', () => { + cy.get('div') + }, { + validate () { + cy.then(() => { + return yieldTF + }) + }, + }) + + cy.then(() => { + yieldTF = false + }) + + cy.session('session_66', () => { + cy.get('div') + }, { + validate () { + cy.then(() => { + return yieldTF + }) + }, + }) + }) + + // correct + it('created command validate has .wrap yield false', () => { + cy.session('session_7', () => { + cy.get('div') + }, { + validate () { + return cy.wrap(false) + }, + }) + }) + + // correct + it('recreated command validate has .wrap yield false ', () => { + let yieldTF = true + + cy.session('session_77', () => { + cy.get('div') + }, { + validate () { + cy.then(() => { + return cy.wrap(yieldTF) + }) + }, + }) + + cy.then(() => { + yieldTF = false + }) + + cy.session('session_77', () => { + cy.get('div') + }, { + validate () { + cy.then(() => { + return cy.wrap(yieldTF) + }) + }, + }) + }) + + it('created command validate has .then with log before yield false ', () => { + cy.session('session_8', () => { + cy.get('div') + }, { + validate () { + cy.window().then((win) => { + console.log(win) + + return false + }) + }, + }) + }) + }) + + // unhappy with stack trace + // ISSUE: missing stack trace (no cy commands to reference) + // FIX BY: could (should) fallback to cy.session stack trace but doesn't feel the best + // FIXED BY DROPPING LAST COMMAND: also saying last command yielded false when it resolved false but maybe yielded false is fine + it('created command resolves false', () => { + cy.session('session_2', () => { + cy.get('div') + }, { + async validate () { + return new Promise(async (resolve, reject) => { + // Cypress.log('getCurrentSessionData') // throws uncaught exception + // Cypress.log({ + // name: 'getCurrentSessionData', + // type: 'system', + // state: 'passed', + // }) + const { cookies } = await Cypress.session.getCurrentSessionData() + + if (cookies.length === 0) { // this is always zero! we aren't setting any :D + resolve(false) + } + }) + }, + }) + }) + + // correct + it('created command resolves false', () => { + let yeildTF = true + + cy.session('session_22', () => { + cy.get('div') + }, { + async validate () { + return new Promise(async (resolve, reject) => { + const { cookies } = await Cypress.session.getCurrentSessionData() + + if (cookies.length === 0) { // this is always zero! we aren't setting any :D + resolve(yeildTF) + } + }) + }, + }) + + cy.then(() => { + yeildTF = false + }) + + cy.session('session_22', () => { + cy.get('div') + }, { + async validate () { + return new Promise(async (resolve, reject) => { + const { cookies } = await Cypress.session.getCurrentSessionData() + + if (cookies.length === 0) { // this is always zero! we aren't setting any :D + resolve(yeildTF) + } + }) + }, + }) + }) + + // missing stack trace (no cy commands) + // unhappy with missing stack trace + // duplicate logs to capture & also fail session + // could just end curr command without failing the rest + it('created command resolves false with log between', () => { + cy.session('session_3', () => { + cy.get('div') + cy.window().then((win) => { + // win.localStorage.setItem('hi', 'there') + }) + }, { + async validate () { + // Cypress.log({ + // name: 'getCurrentSessionData', + // message: '', + // type: 'system', + // state: 'passed', + // }) + + return new Promise(async (resolve, reject) => { + Cypress.log('getCurrentSessionData') // throws uncaught exception + + const { cookies } = await Cypress.session.getCurrentSessionData() + + if (cookies.length === 0) { // this is always zero! we aren't setting any :D + resolve(false) + } + }) + }, + }) + }) + + it.skip('ref', () => { + cy.then(() => { + return new Promise(async (resolve, reject) => { + Cypress.log('getCurrentSessionData') // throws uncaught exception + + const { cookies } = await Cypress.session.getCurrentSessionData() + + if (cookies.length === 0) { // this is always zero! we aren't setting any :D + resolve(false) + } + }) + }) + }) +}) + +// it('for reference', () => { +// cy.get('does_not_exist', { timeout: 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 1a2537a62aa4..7183637e220e 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 @@ -7,177 +7,96 @@ before(async () => { await Cypress.session.clearAllSavedSessions() }) -describe('create session', () => { - describe('seems correct', () => { - it('setup has failing command', () => { - cy.session('session_1', () => { - cy.get('does_not_exist', { timeout: 1 }) - }) - }) - - it('created command reject false', () => { - cy.session('session_4', () => { - cy.get('div') - }, { - async validate () { - return new Promise(async (resolve, reject) => { - // Cypress.log('getCurrentSessionData') // throws uncaught exception - const { cookies } = await Cypress.session.getCurrentSessionData() +let sessionId - console.log(cookies) - if (cookies.length === 0) { // this is always zero! we aren't setting any :D - reject(false) - } - }) - }, - }) - }) +beforeEach(() => { + sessionId = `session_${Cypress._.uniqueId()}` +}) - it('created command validate has failing command', function () { - let count = 0 +it('setup has failing command', () => { + cy.session(sessionId, () => { + cy.get('does_not_exist', { timeout: 1 }) + }) +}) - cy.session('session_5', function () { - cy.get('div').as('hello') - }, { - validate () { - console.log('count', count) - if (count > 0) { - count++ - cy.get('does_not_exist', { timeout: 1 }) - cy.get('does_not_exist_2', { timeout: 1 }) - } - }, - }) +it('created command - validate - has failing Cypress command', function () { + cy.log('hi') + cy.session(sessionId, function () { + cy.get('div').as('hello') + }, { + validate () { + cy.get('does_not_exist', { timeout: 1 }) + // cy.get('does_not_exist_2', { timeout: 1 }) + }, + }) +}) +it('created command - validate - command yields false', () => { + cy.session(sessionId, () => { + cy.get('div') + }, { + validate () { cy.then(() => { - count = 1 - }) - - cy.session('session_5', function () { - cy.get('div').as('hello') - }, { - validate () { - console.log('count', count) - if (count > 0) { - count++ - cy.get('does_not_exist', { timeout: 1 }) - cy.get('does_not_exist_2', { timeout: 1 }) - } - }, - }) - }) - - it('created command validate has .then yield false ', () => { - cy.session('session_6', () => { - cy.get('div') - }, { - validate () { - cy.then(() => { - return false - }) - }, - }) - }) - - it('created command validate has .wrap yield false', () => { - cy.session('session_7', () => { - cy.get('div') - }, { - validate () { - return cy.wrap(false) - }, - }) - }) - - it('created command validate has .then with log before yield false ', () => { - cy.session('session_8', () => { - cy.get('div') - }, { - validate () { - cy.window().then((win) => { - console.log(win) - - return false - }) - }, + return false }) - }) + }, }) +}) - // unhappy with stack trace - // ISSUE: missing stack trace (no cy commands to reference) - // FIX BY: could (should) fallback to cy.session stack trace but doesn't feel the best - // FIXED BY DROPPING LAST COMMAND: also saying last command yielded false when it resolved false but maybe yielded false is fine - it('created command resolves false', () => { - cy.session('session_2', () => { - cy.get('div') - }, { - async validate () { - return new Promise(async (resolve, reject) => { - // Cypress.log('getCurrentSessionData') // throws uncaught exception - // Cypress.log({ - // name: 'getCurrentSessionData', - // type: 'system', - // state: 'passed', - // }) - const { cookies } = await Cypress.session.getCurrentSessionData() +it('created command - validate - has multiple commands and yields false', () => { + cy.session(sessionId, () => { + cy.get('div') + }, { + validate () { + cy.log('filler log') - console.log(cookies) - if (cookies.length === 0) { // this is always zero! we aren't setting any :D - resolve(false) - } - }) - }, - }) + return cy.wrap(false) + }, }) +}) - // missing stack trace (no cy commands) - // unhappy with missing stack trace - // duplicate logs to capture & also fail session - // could just end curr command without failing the rest - it('created command resolves false with log between', () => { - cy.session('session_3', () => { - cy.get('div') - cy.window().then((win) => { - // win.localStorage.setItem('hi', 'there') - }) - }, { - async validate () { - // Cypress.log({ - // name: 'getCurrentSessionData', - // message: '', - // type: 'system', - // state: 'passed', - // }) - - return new Promise(async (resolve, reject) => { - Cypress.log('getCurrentSessionData') // throws uncaught exception - - const { cookies } = await Cypress.session.getCurrentSessionData() +it('created command - validate - rejects with false', () => { + cy.session(sessionId, () => { + cy.get('div') + }, { + async validate () { + return new Promise(async (resolve, reject) => { + // Cypress.log('getCurrentSessionData') // throws uncaught exception + const { cookies } = await Cypress.session.getCurrentSessionData() - if (cookies.length === 0) { // this is always zero! we aren't setting any :D - resolve(false) - } - }) - }, - }) + if (cookies.length === 0) { // this is always zero! we aren't setting any :D + reject(false) + } + }) + }, }) +}) - it.skip('ref', () => { - cy.then(() => { +it('created command - validate - promise resolves false', () => { + cy.session(sessionId, () => { + cy.get('div') + }, { + async validate () { return new Promise(async (resolve, reject) => { - Cypress.log('getCurrentSessionData') // throws uncaught exception - const { cookies } = await Cypress.session.getCurrentSessionData() if (cookies.length === 0) { // this is always zero! we aren't setting any :D resolve(false) } }) - }) + }, }) }) -// it('for reference', () => { -// cy.get('does_not_exist', { timeout: 1 }) -// }) +it('created command - validate - throws an error', () => { + cy.session(sessionId, () => { + cy.get('div') + }, { + async validate () { + cy.get('div') + .within(() => { + throw new Error('Something went wrong!') + }) + }, + }) +}) From d51cc9510b0f92de926128edc22f608992f59d8e Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Thu, 20 Oct 2022 10:55:50 -0500 Subject: [PATCH 17/52] write test coverage for recreated sessions --- .../app/cypress/e2e/runner/sessions.ui.cy.ts | 333 ++++++++++++++---- .../driver/src/cy/commands/sessions/index.ts | 2 +- packages/reporter/src/commands/commands.scss | 2 - packages/reporter/src/errors/errors.scss | 2 +- packages/reporter/src/errors/test-error.tsx | 2 +- .../cypress/e2e/session/errors.cy.js | 262 ++++++++++---- 6 files changed, 471 insertions(+), 132 deletions(-) diff --git a/packages/app/cypress/e2e/runner/sessions.ui.cy.ts b/packages/app/cypress/e2e/runner/sessions.ui.cy.ts index f076efa0bfd5..a00be2faefb1 100644 --- a/packages/app/cypress/e2e/runner/sessions.ui.cy.ts +++ b/packages/app/cypress/e2e/runner/sessions.ui.cy.ts @@ -332,26 +332,17 @@ describe('runner/cypress sessions.ui.spec', { }) describe('errors', () => { - const assertTestError = (testAlias: string, errMessage: string, errMessagePostfix: string) => { - cy.get(testAlias) - .find('.attempt-error-region') - .contains(errMessage) - .contains(errMessagePostfix) - } - - describe.only('created session', () => { + describe('created session', () => { before(() => { loadSpec({ projectName: 'session-and-origin-e2e-specs', filePath: 'session/errors.cy.js', - // passCount: 1, - // failCount: 12, failCount: 7, }) }) it('setup has failing Cypress command', () => { - cy.contains('.test', 'setup has failing command').as('example_test') + 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') @@ -371,98 +362,320 @@ describe('runner/cypress sessions.ui.spec', { }) }) - const setupError = 'This error occurred while creating the session. Because the session setup failed, we failed the test.' + const setupErrorPostFix = 'This error occurred while creating the session. Because the session setup failed, we failed the test.' - assertTestError('@example_test', 'Expected to find element', setupError) + cy.get('@example_test') + .find('.attempt-error-region') + .contains('Expected to find element') + .contains(setupErrorPostFix) }) describe('failed validation', () => { - const validateErr = 'This error occurred while validating the created session. Because validation failed immediately after creating the session, we failed the test.' - - const assertCreateSessionValidationFailed = (testAlias: string) => { - cy.get(testAlias) - .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') + [ + { + testCase: 'has failing Cypress command', + systemTestTitle: 'validate - has failing Cypress command', + errMessage: 'Expected to find 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') + .should('have.attr', 'data-model-state', 'failed') + .children('.collapsible') + .should('have.class', 'is-open') .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') - .should('have.class', 'command-state-failed') - .find('.failed-indicator') + // 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) }) + }) + }) + }) + + describe('recreated session', () => { + const assertRecreatedSession = ({ + testAlias, + validationErrMessage, + commandPassed, + successfullyRecreatedSession, + }) => { + 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(() => { + // 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.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) => { + return win.CYPRESS_TEST_DATA = { + restoreSessionWithValidationFailure: true, + successfullyRecreatedSession: true, + } + }) + }, + }) + }) + ;[ { testCase: 'has failing Cypress command', - systemTestTitle: 'created command - validate - has failing Cypress command', + systemTestTitle: 'validate - has failing Cypress command', errMessage: 'Expected to find element', }, { testCase: 'command yields false', - systemTestTitle: 'created command - validate - command yields false', + systemTestTitle: 'validate - command yields false', errMessage: 'callback yielded false.', }, { testCase: 'has multiple commands and yields false', - systemTestTitle: 'created command - validate - has multiple commands and yields false', + systemTestTitle: 'validate - has multiple commands and yields false', errMessage: 'callback yielded false.', }, { testCase: 'rejects with false', - systemTestTitle: 'created command - validate - rejects with false', + systemTestTitle: 'validate - rejects with false', errMessage: 'rejected with false.', }, { testCase: 'promise resolved false', - systemTestTitle: 'created command - validate - promise resolves false', + systemTestTitle: 'validate - promise resolves false', errMessage: 'promise resolved false.', }, { testCase: 'throws an error', - systemTestTitle: 'created command - validate - throws an error', + systemTestTitle: 'validate - throws an error', errMessage: 'Something went wrong!', }, ].forEach(({ testCase, systemTestTitle, errMessage }, index) => { + if (index === 0 || index === 5) { + return + } + it(`has test error when validate ${testCase}`, () => { cy.contains('.test', systemTestTitle).as('example_test') - assertCreateSessionValidationFailed('@example_test') - assertTestError('@example_test', errMessage, validateErr) + + 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') }) }) }) - }) - describe('restored session fails validation and session is recreated', () => { - it('has inline error where validate returned false', () => { - // test marked as passed - // test is collapsed - // session is marked as 'successful' - // validates group is collapsed - }) + describe('failed to recreated session', () => { + before(() => { + loadSpec({ + projectName: 'session-and-origin-e2e-specs', + filePath: 'session/errors.cy.js', + // passCount: 0, // should be + passCount: 2, + // failCount:7,// should be + failCount: 5, + // failCount: 1, + setup () { + cy.window().then((win) => { + return win.CYPRESS_TEST_DATA = { + restoreSessionWithValidationFailure: true, + successfullyRecreatedSession: false, + } + }) + }, + }) + }) - it('has inline error where validate resolved false') - it('has inline error where validate rejected with false') - it('has inline error where validate threw error') - it('has inline error where validate has failing Cypress command') + 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 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(recreatedErrPostfix) + }) + + describe('failed validation', () => { + [ + { + testCase: 'has failing Cypress command', + systemTestTitle: 'validate - has failing Cypress command', + errMessage: 'Expected to find 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) => { + if (index === 0 || index === 5) { + return + } + + 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/driver/src/cy/commands/sessions/index.ts b/packages/driver/src/cy/commands/sessions/index.ts index 68424b0053f1..3bfadb3468af 100644 --- a/packages/driver/src/cy/commands/sessions/index.ts +++ b/packages/driver/src/cy/commands/sessions/index.ts @@ -302,7 +302,7 @@ export default function (Commands, Cypress, cy) { // show validation error and allow sessions workflow to recreate the session if (step === 'restore') { - $errUtils.modifyErrMsg(err, `\n\nThis error occurred while validating the restoring session. Because validation failed, we will try to recreate the session.`, _.add) + $errUtils.modifyErrMsg(err, `\n\nThis error occurred while validating the restored session. Because validation failed, we will try to recreate the session.`, _.add) err.isRecovered = true diff --git a/packages/reporter/src/commands/commands.scss b/packages/reporter/src/commands/commands.scss index 305c41398dd5..9e95cc04dc50 100644 --- a/packages/reporter/src/commands/commands.scss +++ b/packages/reporter/src/commands/commands.scss @@ -125,8 +125,6 @@ } .command-pin-target.command-group { - // .runnable-err.show-recovered-test-err > .runnable-err-header, - // .show-recovered-test-err { @include nested-command-dashes($gray-600); padding-left: 12px; min-height: 28px; diff --git a/packages/reporter/src/errors/errors.scss b/packages/reporter/src/errors/errors.scss index cce8d22de8cb..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; 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/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 7183637e220e..9d8974f88403 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,102 +1,230 @@ /** * Used in cy-in-cy tests in @packages/app. */ - 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 }) -it('setup has failing command', () => { - cy.session(sessionId, () => { +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('created command - validate - has failing Cypress command', function () { - cy.log('hi') - cy.session(sessionId, function () { - cy.get('div').as('hello') - }, { - validate () { - cy.get('does_not_exist', { timeout: 1 }) - // cy.get('does_not_exist_2', { timeout: 1 }) - }, - }) +// FIX ME (recreated stuck in ''restoring, double error and test marked as passed) +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.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('created command - validate - command yields false', () => { - cy.session(sessionId, () => { - cy.get('div') - }, { - validate () { - cy.then(() => { - return false +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('created command - validate - has multiple commands and yields false', () => { - cy.session(sessionId, () => { - cy.get('div') - }, { - validate () { - cy.log('filler log') +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('created command - validate - rejects with false', () => { - cy.session(sessionId, () => { - cy.get('div') - }, { - async validate () { - return new Promise(async (resolve, reject) => { - // Cypress.log('getCurrentSessionData') // throws uncaught exception - const { cookies } = await Cypress.session.getCurrentSessionData() - - if (cookies.length === 0) { // this is always zero! we aren't setting any :D - reject(false) +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('created command - validate - promise resolves false', () => { - cy.session(sessionId, () => { - cy.get('div') - }, { - async validate () { - return new Promise(async (resolve, reject) => { - const { cookies } = await Cypress.session.getCurrentSessionData() +it('validate - promise resolves false', () => { + function validate () { + count += 1 - if (cookies.length === 0) { // this is always zero! we aren't setting any :D - resolve(false) + 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 resolve(false) + }) + } + + cy.session(sessionId, setup, { validate }) + + if (testData) { + cy.session(sessionId, setup, { validate }) + } }) -it('created command - validate - throws an error', () => { - cy.session(sessionId, () => { +// FIX ME (recreated stuck in ''restoring, double error and test marked as passed) +it('validate - throws an error', () => { + function validate () { + count += 1 + cy.get('div') - }, { - async validate () { - cy.get('div') - .within(() => { - throw new Error('Something went wrong!') - }) - }, - }) + .within(() => { + 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 }) + } }) From cdafd9f470132f38a51777833a2129d228b1f56f Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Thu, 20 Oct 2022 20:39:24 -0500 Subject: [PATCH 18/52] okay !!! I think we have fixed all the recovered states. check in dump --- .../app/cypress/e2e/runner/sessions.ui.cy.ts | 21 +- .../driver/src/cy/commands/sessions/index.ts | 298 +++++++++++++----- packages/driver/src/cypress/command_queue.ts | 85 +++-- .../reporter/src/commands/command-model.ts | 2 +- .../cypress/e2e/session/errors.cy.js | 26 ++ 5 files changed, 312 insertions(+), 120 deletions(-) diff --git a/packages/app/cypress/e2e/runner/sessions.ui.cy.ts b/packages/app/cypress/e2e/runner/sessions.ui.cy.ts index a00be2faefb1..cc525996ebbc 100644 --- a/packages/app/cypress/e2e/runner/sessions.ui.cy.ts +++ b/packages/app/cypress/e2e/runner/sessions.ui.cy.ts @@ -375,7 +375,7 @@ describe('runner/cypress sessions.ui.spec', { { testCase: 'has failing Cypress command', systemTestTitle: 'validate - has failing Cypress command', - errMessage: 'Expected to find element', + errMessage: 'failed because it requires a DOM element', }, { testCase: 'command yields false', @@ -525,7 +525,7 @@ describe('runner/cypress sessions.ui.spec', { { testCase: 'has failing Cypress command', systemTestTitle: 'validate - has failing Cypress command', - errMessage: 'Expected to find element', + errMessage: 'failed because it requires a DOM element', }, { testCase: 'command yields false', @@ -553,10 +553,6 @@ describe('runner/cypress sessions.ui.spec', { errMessage: 'Something went wrong!', }, ].forEach(({ testCase, systemTestTitle, errMessage }, index) => { - if (index === 0 || index === 5) { - return - } - it(`has test error when validate ${testCase}`, () => { cy.contains('.test', systemTestTitle).as('example_test') @@ -581,11 +577,8 @@ describe('runner/cypress sessions.ui.spec', { loadSpec({ projectName: 'session-and-origin-e2e-specs', filePath: 'session/errors.cy.js', - // passCount: 0, // should be - passCount: 2, - // failCount:7,// should be - failCount: 5, - // failCount: 1, + passCount: 0, + failCount: 7, setup () { cy.window().then((win) => { return win.CYPRESS_TEST_DATA = { @@ -622,7 +615,7 @@ describe('runner/cypress sessions.ui.spec', { { testCase: 'has failing Cypress command', systemTestTitle: 'validate - has failing Cypress command', - errMessage: 'Expected to find element', + errMessage: 'failed because it requires a DOM element', }, { testCase: 'command yields false', @@ -650,10 +643,6 @@ describe('runner/cypress sessions.ui.spec', { errMessage: 'Something went wrong!', }, ].forEach(({ testCase, systemTestTitle, errMessage }, index) => { - if (index === 0 || index === 5) { - return - } - it(`has test error when validate ${testCase}`, () => { cy.contains('.test', systemTestTitle).as('example_test') diff --git a/packages/driver/src/cy/commands/sessions/index.ts b/packages/driver/src/cy/commands/sessions/index.ts index 3bfadb3468af..a179d35d5eb8 100644 --- a/packages/driver/src/cy/commands/sessions/index.ts +++ b/packages/driver/src/cy/commands/sessions/index.ts @@ -20,7 +20,6 @@ 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 @@ -112,6 +111,7 @@ export default function (Commands, Cypress, cy) { // 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') // stringify deterministically if we were given an object id = _.isString(id) ? id : stringifyStable(id) @@ -211,6 +211,7 @@ export default function (Commands, Cypress, cy) { }, (setupLogGroup) => { return cy.then(async () => { + Cypress.state('current').set('name', 'setup session') // 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) => { @@ -226,12 +227,15 @@ export default function (Commands, Cypress, cy) { $errUtils.modifyErrMsg(err, `\n\nThis error occurred while creating the session. Because the session setup failed, we failed the test.`, _.add) - return false + return err }) return existingSession.setup() + + return existingSession.setup.call(cy.state('ctx')) }) .then(async () => { + Cypress.state('current').set('name', 'save session') cy.state('onCommandFailed', null) await navigateAboutBlank() const data = await sessions.getCurrentSessionData() @@ -277,9 +281,12 @@ export default function (Commands, Cypress, cy) { } function validateSession (existingSession, step: SESSION_STEPS) { + console.log('validateSession') const isValidSession = true if (!existingSession.validate) { + console.log('nothing to validate') + return isValidSession } @@ -293,17 +300,31 @@ export default function (Commands, Cypress, cy) { }, }, (validateLog) => { return cy.then(async () => { - const onSuccess = () => { - return isValidSession - } + Cypress.state('current').set('name', 'create validate session commands') - const onFail = (err, yielded) => { + // validate(existingSession, step) + // }) + // }) + // } + + // // uses Cypress hackery to resolve `false` if validate() resolves/returns false or throws/fails a cypress command. + // function validate (existingSession, step) { + const isValidSession = true + let returnVal + let _commandToRunAfterValidation + let _validationError + + const onFail = (err) => { Cypress.state('onFail', null) + Cypress.state('onCommandFailed', null) + console.log('[onFail] step', step, err) // show validation error and allow sessions workflow to recreate the session 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) + const alreadyRecovered = !!err.isRecovered + err.isRecovered = true validateLog.set({ @@ -313,7 +334,8 @@ export default function (Commands, Cypress, cy) { Error: err.stack, } }, - error: err, + // explicitly set via .set() so we don't end the log group early + ...(!alreadyRecovered && { error: err }), }) return !isValidSession @@ -333,12 +355,12 @@ export default function (Commands, Cypress, cy) { err.onFail = (err) => { // console.log('err on FAIL error', err) validateLog.set({ - // ...(yielded && { consoleProps: { - // Yielded: yielded - // } - // }), + // ...(yielded && { consoleProps: { + // Yielded: yielded + // } + // }), snapshot: true, - error: err, + // error: err, }) } @@ -348,92 +370,209 @@ export default function (Commands, Cypress, cy) { throw err } - return validate(existingSession, step, onSuccess, onFail) - }) - }) - } + Cypress.state('onFail', onFail) - // uses Cypress hackery to resolve `false` if validate() resolves/returns false or throws/fails a cypress command. - function validate (existingSession, step, onSuccess, onFail) { - let returnVal + cy.state('onCommandFailed', (err: Error | string, queue): boolean => { + console.log('onCommandFailed', step, err) - Cypress.state('onFail', (err) => { - // set current command to cy.session for more accurate codeFrame - // err.stack = $stackUtils.normalizedStack(err) + if (step === 'restore') { + console.log(_.clone(Cypress.state())) + console.log(queue.get()) + const commands = queue.get() + const index = _.findIndex(commands, (command: any) => { + return ( + _commandToRunAfterValidation + && command.attributes.chainerId === _commandToRunAfterValidation.chainerId + ) + }) - // err = $errUtils.enhanceStack({ - // err, - // userInvocationStack: $errUtils.getUserInvocationStack(err, this.state), - // projectRoot: this.config('projectRoot'), - // }) + console.log(Cypress.state('index')) + console.log(index) + for (let i = Cypress.state('index'); i < index; i++) { + const cmd = commands[i] - return onFail(err) - }) + if (!cmd.get('restore-within')) { + commands[i].skip() + } + } - // const normalizeError = (err) => { - // err.stack = $stackUtils.normalizedStack(err) + // restore within subject back to the original subject used when + // the session command kicked off + Cypress.state('withinSubject', withinSubject) - // return $errUtils.enhanceStack({ - // err, - // userInvocationStack: $errUtils.getUserInvocationStack(err, Cypress.state), - // projectRoot: Cypress.config('projectRoot'), - // }) - // } + // move to _commandToRunAfterValidation's index to ensure failures are handled correctly + queue.state('index', index) - try { - returnVal = existingSession.validate() - } catch (e) { - return onFail(e) - } + console.log('sessionStatus', step, typeof err) - return cy.then(async () => { - Cypress.state('onFail', null) - // 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) - - throw $errUtils.errByPath('sessions.validate_callback_false', { reason: 'promise resolved false' }) + // // 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) + // } + + if (!(err instanceof Error) && err === false) { + console.log('ITS FALSE') + // 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)}` }) + } + + // err.stack = $stackUtils.normalizedStack(err) + + // _validationError = $errUtils.enhanceStack({ + // err, + // userInvocationStack: $errUtils.getUserInvocationStack(err, Cypress.state), + // projectRoot: Cypress.config('projectRoot'), + // }) + + console.log('CATCH error!') + // cy.state('onFail', null) + // $errUtils.modifyErrMsg(err, `\n\nThis error occurred while validating the restored session. Because validation failed, we will try to recreate the session.`, _.add) + + err.isRecovered = true + + onFail(err) + + returnVal = false + + return err } - return onSuccess() + // cy.state('onFail', null) + + // EMILY DID YOU JUST BREAK THIS??? + // if (err === false) { + // console.log('here here') + // setSessionLogStatus('failed') + // validateLog.set({ + // state: 'failed', + // snapshot: true, + // error: err, + // }) + + // // can't remember but think + // // explicitly fail so we don't get the awkward .then err log associated + // // the cy.then used internally within cy.sessions + // onFail(err) + + // return err + // } + + console.log('here return false') + + return err }) - .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)}` }) + + // 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 + // ) + // }) + + // // 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) + // } + + // err.stack = $stackUtils.normalizedStack(err) + + // _validationError = $errUtils.enhanceStack({ + // err, + // userInvocationStack: $errUtils.getUserInvocationStack(err, Cypress.state), + // projectRoot: Cypress.config('projectRoot'), + // }) + + // // move to _commandToRunAfterValidation's index to ensure failures are handled correctly + // cy.state('index', index) + + // cy.state('onCommandFailed', null) + + // return true + // }) + + console.log('create validate', _.clone(cy.state())) + + // try { + returnVal = existingSession.validate.call(cy.state('ctx')) + // } catch (e) { + // console.log('caught err when executing') + + // return onFail(e) + // } + console.log('after execute validate', returnVal) + + _commandToRunAfterValidation = cy.then(async () => { + Cypress.state('current').set('name', '_commandToRunAfterValidation') + console.log('RUNNING _commandToRunAfterValidation', _validationError) + console.log('RUNNING _commandToRunAfterValidation', returnVal) + Cypress.state('onFail', null) + Cypress.state('onCommandFailed', null) + + if (_validationError) { + return onFail(_validationError) } - return onFail(err) - }) - } + if (returnVal === false) { + console.log('returning false') - if (returnVal === undefined || Cypress.isCy(returnVal)) { - const val = cy.state('current').get('prev')?.attributes?.subject + return !isValidSession + } - if (val === false) { - // set current command to cy.session for more accurate codeframe - cy.state('current', sessionCommand) + // 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) - return onFail($errUtils.errByPath('sessions.validate_callback_false', { reason: 'callback yielded false' }), val) - } - } + throw $errUtils.errByPath('sessions.validate_callback_false', { reason: 'promise resolved false' }) + } - return onSuccess() + 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 onFail(err) + }) + } + + if (returnVal === undefined || Cypress.isCy(returnVal)) { + const val = cy.state('current').get('prev')?.attributes?.subject + + if (val === false) { + // set current command to cy.session for more accurate codeframe + cy.state('current', sessionCommand) + + return onFail($errUtils.errByPath('sessions.validate_callback_false', { reason: 'callback yielded false' }), val) + } + } + + return isValidSession + }) + + return _commandToRunAfterValidation + }) }) } - /** * Creates session flow: * 1. create session * 2. validate session */ - const createSessionWorkflow = (existingSession, step: 'create' | 'recreate' = 'create') => { - return cy.then(async () => { + const createSessionWorkflow = (existingSession, step: 'create' | 'recreate') => { + cy.then(async () => { + Cypress.state('current').set('name', step) setSessionLogStatus(statusMap.inProgress(step)) await navigateAboutBlank() @@ -443,6 +582,9 @@ export default function (Commands, Cypress, cy) { }) .then(() => validateSession(existingSession, step)) .then((isValidSession: boolean) => { + Cypress.state('current').set('name', step) + console.log('after create validation....', isValidSession) + Cypress.state('current').set('name', `after ${step} validation`) if (!isValidSession) { return } @@ -458,7 +600,8 @@ export default function (Commands, Cypress, cy) { * 3. if validation fails, catch error and recreate session */ const restoreSessionWorkflow = (existingSession) => { - return cy.then(async () => { + cy.then(async () => { + Cypress.state('current').set('name', 'restore') setSessionLogStatus('restoring') await navigateAboutBlank() await sessions.clearCurrentSessionData() @@ -467,6 +610,7 @@ export default function (Commands, Cypress, cy) { }) .then(() => validateSession(existingSession, 'restore')) .then((isValidSession: boolean) => { + Cypress.state('current').set('name', 'after restore validation') if (!isValidSession) { return createSessionWorkflow(existingSession, 'recreate') } @@ -492,6 +636,7 @@ export default function (Commands, Cypress, cy) { return logGroup(Cypress, groupDetails, (log) => { return cy.then(async () => { + Cypress.state('current').set('name', 'start session workflow') _log = log if (!session.hydrated) { @@ -502,12 +647,13 @@ export default function (Commands, Cypress, cy) { _.extend(session, _.omit(serverStoredSession, 'setup', 'validate')) session.hydrated = true } else { - return createSessionWorkflow(session) + return createSessionWorkflow(session, 'create') } } return restoreSessionWorkflow(session) }).then(() => { + Cypress.state('current').set('name', 'after session workflow') _log.set({ state: 'passed' }) }) }) diff --git a/packages/driver/src/cypress/command_queue.ts b/packages/driver/src/cypress/command_queue.ts index 82c51ec37e48..a92f704837db 100644 --- a/packages/driver/src/cypress/command_queue.ts +++ b/packages/driver/src/cypress/command_queue.ts @@ -31,7 +31,7 @@ const commandRunningFailed = (Cypress, state, err) => { } const current = state('current') - const lastLog = _.last(current?.get('logs') || []) + const lastLog = current?.getLastLog() const consoleProps = () => { if (!current) return @@ -52,7 +52,7 @@ const commandRunningFailed = (Cypress, state, err) => { } // ensure the last log on the command ends correctly - if (lastLog) { + if (lastLog && !lastLog.get('ended')) { return lastLog.set({ consoleProps }).error(err) } @@ -235,9 +235,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 @@ -259,18 +258,50 @@ export class CommandQueue extends Queue<$Command> { command.set({ subject }) - // 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) return subject + }).catch((err) => { + console.log('on command err') + if (this.state('onCommandFailed')) { + err = this.state('onCommandFailed')(err, this) + + this.state('onCommandFailed', null) + } + + debugErrors('cypress command had an error: %o', err) + + // since this failed this means that a specific command failed + // and we should highlight it in red or insert a new command + // @ts-ignore + if (_.isObject(err) && !err.name) { + // @ts-ignore + err.name = 'CypressError' + } + + console.log('cypress command had an error: %o', err.isRecovered) + + commandRunningFailed(Cypress, this.state, err) + + if (err.isRecovered) { + console.log('let the queue move on to the next command') + + return // let the queue move on to the next command + } + + throw err + }).finally(() => { + this.state('commandIntermediateValue', undefined) + // we're finished with the current command so set it back to null + this.state('current', null) + // end / snapshot our logs if they need it + command.finishLogs() + + // reset the nestedIndex back to null + this.state('nestedIndex', null) }) } @@ -382,27 +413,27 @@ export class CommandQueue extends Queue<$Command> { return } - if (this.state('onCommandFailed')) { - const handledError = this.state('onCommandFailed')(err, this) + // if (this.state('onCommandFailed')) { + // const handledError = this.state('onCommandFailed')(err, this) - cy.state('onCommandFailed', null) + // // cy.state('onCommandFailed', null) - if (handledError) { - return next() - } - } + // if (handledError) { + // return next() + // } + // } - debugErrors('caught error in promise chain: %o', err) + // debugErrors('caught error in promise chain: %o', err) - // since this failed this means that a specific command failed - // and we should highlight it in red or insert a new command - // @ts-ignore - if (_.isObject(err) && !err.name) { - // @ts-ignore - err.name = 'CypressError' - } + // // since this failed this means that a specific command failed + // // and we should highlight it in red or insert a new command + // // @ts-ignore + // if (_.isObject(err) && !err.name) { + // // @ts-ignore + // err.name = 'CypressError' + // } - commandRunningFailed(Cypress, this.state, err) + // commandRunningFailed(Cypress, this.state, err) return this.fail(err) } diff --git a/packages/reporter/src/commands/command-model.ts b/packages/reporter/src/commands/command-model.ts index dc4988685702..944957451de9 100644 --- a/packages/reporter/src/commands/command-model.ts +++ b/packages/reporter/src/commands/command-model.ts @@ -96,7 +96,7 @@ export default class Command extends Instrument { // 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 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 9d8974f88403..b0d12478a3f1 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 @@ -17,6 +17,23 @@ 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 () { @@ -76,6 +93,8 @@ it('validate - has failing Cypress command', function () { 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 }) } @@ -207,6 +226,13 @@ it('validate - throws an error', () => { 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 From 27ce5e44638350d275c58799ad85ff12285139e4 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Thu, 20 Oct 2022 20:43:11 -0500 Subject: [PATCH 19/52] no only to run in CI --- packages/driver/cypress/e2e/commands/sessions/sessions.cy.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js b/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js index 8e1fe270c992..63cf5112ddd0 100644 --- a/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js +++ b/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js @@ -277,9 +277,6 @@ describe('cy.session', { retries: 0 }, () => { // expect(true).to.be.false // } }) // }) - it.only('try', () => { - expect('true').to.be.true - }) // it.only('try', () => { // cy.get('body').within(() => { @@ -563,7 +560,7 @@ describe('cy.session', { retries: 0 }, () => { }) // test must be first to run before blank page visit between each test - it.only('does not clear page visit from validate function', () => { + it('does not clear page visit from validate function', () => { cy.url().should('contain', '/fixtures/auth/index.html') }) From 338d3b8287936af08aa63774476da23318021d7b Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Thu, 20 Oct 2022 20:46:57 -0500 Subject: [PATCH 20/52] CI doesnt even like .only in comments :( --- .../cypress/e2e/commands/sessions/sessions.cy.js | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js b/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js index 63cf5112ddd0..c99cd247b4f6 100644 --- a/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js +++ b/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js @@ -278,16 +278,8 @@ describe('cy.session', { retries: 0 }, () => { // } }) // }) - // it.only('try', () => { - // cy.get('body').within(() => { - // cy.get('div').within(() => { - // expect('true').to.be.true - // }) - // }) - // }) - // // test must be first to run before blank page visit between each test - // it.only('does not clear page visit from validate function', () => { + // it('does not clear page visit from validate function', () => { // setupTestContext() // cy.log('Creating new session with validation to test against') From b90434424f87ec91ce368233f2c7a6f975fd0f12 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Fri, 21 Oct 2022 15:10:37 -0500 Subject: [PATCH 21/52] okay check in point. Still not there yet --- .../commands/actions/type_special_chars.cy.js | 19 ++- .../cypress/e2e/commands/assertions.cy.js | 4 +- .../cypress/e2e/commands/navigation.cy.js | 4 +- .../e2e/commands/sessions/sessions.cy.js | 31 +--- .../driver/src/cy/commands/sessions/index.ts | 160 ++++-------------- packages/driver/src/cypress/command_queue.ts | 36 +--- packages/driver/src/cypress/error_utils.ts | 4 +- packages/reporter/src/commands/command.cy.tsx | 26 --- .../cypress/e2e/session/errors.cy.js | 2 +- 9 files changed, 63 insertions(+), 223 deletions(-) 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 cb4ac868bb62..e4ec2eb972f5 100644 --- a/packages/driver/cypress/e2e/commands/assertions.cy.js +++ b/packages/driver/cypress/e2e/commands/assertions.cy.js @@ -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 }, diff --git a/packages/driver/cypress/e2e/commands/navigation.cy.js b/packages/driver/cypress/e2e/commands/navigation.cy.js index cccfc66cda22..59d4a3a6bc7e 100644 --- a/packages/driver/cypress/e2e/commands/navigation.cy.js +++ b/packages/driver/cypress/e2e/commands/navigation.cy.js @@ -2102,13 +2102,13 @@ describe('src/cy/commands/navigation', () => { cy.on('log:added', (_attrs, log) => { this.lastLog = log - this.logs.push(log) + this.logs?.push(log) }) return null }) - describe('can time out', () => { + describe('can time out', { retries: 1 }, () => { let pageLoadTimeout before(() => { diff --git a/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js b/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js index c99cd247b4f6..44ac1ea608e5 100644 --- a/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js +++ b/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js @@ -979,7 +979,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() }) @@ -993,7 +993,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() @@ -1006,7 +1006,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) => { @@ -1071,30 +1071,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')) { @@ -1120,7 +1099,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/src/cy/commands/sessions/index.ts b/packages/driver/src/cy/commands/sessions/index.ts index a179d35d5eb8..eb1b2dbedb19 100644 --- a/packages/driver/src/cy/commands/sessions/index.ts +++ b/packages/driver/src/cy/commands/sessions/index.ts @@ -281,12 +281,9 @@ export default function (Commands, Cypress, cy) { } function validateSession (existingSession, step: SESSION_STEPS) { - console.log('validateSession') const isValidSession = true if (!existingSession.validate) { - console.log('nothing to validate') - return isValidSession } @@ -302,20 +299,13 @@ export default function (Commands, Cypress, cy) { return cy.then(async () => { Cypress.state('current').set('name', 'create validate session commands') - // validate(existingSession, step) - // }) - // }) - // } - // // uses Cypress hackery to resolve `false` if validate() resolves/returns false or throws/fails a cypress command. // function validate (existingSession, step) { const isValidSession = true - let returnVal + let caughtCommandErr = false let _commandToRunAfterValidation - let _validationError - const onFail = (err) => { - Cypress.state('onFail', null) + const enhanceErr = (err) => { Cypress.state('onCommandFailed', null) console.log('[onFail] step', step, err) @@ -323,8 +313,6 @@ export default function (Commands, Cypress, cy) { 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) - const alreadyRecovered = !!err.isRecovered - err.isRecovered = true validateLog.set({ @@ -335,10 +323,10 @@ export default function (Commands, Cypress, cy) { } }, // explicitly set via .set() so we don't end the log group early - ...(!alreadyRecovered && { error: err }), + ...(!caughtCommandErr && { error: err }), }) - return !isValidSession + return err } setSessionLogStatus('failed') @@ -349,45 +337,31 @@ export default function (Commands, Cypress, cy) { Error: err.stack, } }, - // error: err, + snapshot: true, + error: err, }) - err.onFail = (err) => { - // console.log('err on FAIL error', err) - validateLog.set({ - // ...(yielded && { consoleProps: { - // Yielded: yielded - // } - // }), - snapshot: true, - // error: err, - }) - } - $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) - // console.log('before throw', err) - throw err - } + console.log('return err', err) - Cypress.state('onFail', onFail) + return err + } cy.state('onCommandFailed', (err: Error | string, queue): boolean => { console.log('onCommandFailed', step, err) if (step === 'restore') { - console.log(_.clone(Cypress.state())) - console.log(queue.get()) const commands = queue.get() + // determine command queue index of _commandToRunAfterValidation's index const index = _.findIndex(commands, (command: any) => { return ( _commandToRunAfterValidation - && command.attributes.chainerId === _commandToRunAfterValidation.chainerId + && command.attributes.chainerId === _commandToRunAfterValidation.chainerId ) }) - console.log(Cypress.state('index')) - console.log(index) + // skip all commands between this command and _commandToRunAfterValidation for (let i = Cypress.state('index'); i < index; i++) { const cmd = commands[i] @@ -414,88 +388,17 @@ export default function (Commands, Cypress, cy) { if (!(err instanceof Error) && err === false) { console.log('ITS FALSE') // 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)}` }) } - // err.stack = $stackUtils.normalizedStack(err) - - // _validationError = $errUtils.enhanceStack({ - // err, - // userInvocationStack: $errUtils.getUserInvocationStack(err, Cypress.state), - // projectRoot: Cypress.config('projectRoot'), - // }) - - console.log('CATCH error!') - // cy.state('onFail', null) - // $errUtils.modifyErrMsg(err, `\n\nThis error occurred while validating the restored session. Because validation failed, we will try to recreate the session.`, _.add) - err.isRecovered = true - onFail(err) - - returnVal = false - - return err + caughtCommandErr = true } - // cy.state('onFail', null) - - // EMILY DID YOU JUST BREAK THIS??? - // if (err === false) { - // console.log('here here') - // setSessionLogStatus('failed') - // validateLog.set({ - // state: 'failed', - // snapshot: true, - // error: err, - // }) - - // // can't remember but think - // // explicitly fail so we don't get the awkward .then err log associated - // // the cy.then used internally within cy.sessions - // onFail(err) - - // return err - // } - - console.log('here return false') - - return err + return enhanceErr(err) }) - // 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 - // ) - // }) - - // // 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) - // } - - // err.stack = $stackUtils.normalizedStack(err) - - // _validationError = $errUtils.enhanceStack({ - // err, - // userInvocationStack: $errUtils.getUserInvocationStack(err, Cypress.state), - // projectRoot: Cypress.config('projectRoot'), - // }) - - // // move to _commandToRunAfterValidation's index to ensure failures are handled correctly - // cy.state('index', index) - - // cy.state('onCommandFailed', null) - - // return true - // }) - - console.log('create validate', _.clone(cy.state())) // try { returnVal = existingSession.validate.call(cy.state('ctx')) @@ -508,19 +411,23 @@ export default function (Commands, Cypress, cy) { _commandToRunAfterValidation = cy.then(async () => { Cypress.state('current').set('name', '_commandToRunAfterValidation') - console.log('RUNNING _commandToRunAfterValidation', _validationError) - console.log('RUNNING _commandToRunAfterValidation', returnVal) - Cypress.state('onFail', null) Cypress.state('onCommandFailed', null) - if (_validationError) { - return onFail(_validationError) + if (caughtCommandErr) { + return !isValidSession } - if (returnVal === false) { - console.log('returning false') + const failValidation = (err) => { + err.onFail = (err) => { + validateLog.error(err) + } - return !isValidSession + if (step === 'restore') { + // move to recreate session flow + return !isValidSession + } + + throw enhanceErr(err) } // when the validate function returns a promise, ensure it does not return false or throw an error @@ -543,18 +450,18 @@ export default function (Commands, Cypress, cy) { err = $errUtils.errByPath('sessions.validate_callback_false', { reason: `promise rejected with ${String(err)}` }) } - return onFail(err) + return failValidation(err) }) } if (returnVal === undefined || Cypress.isCy(returnVal)) { - const val = cy.state('current').get('prev')?.attributes?.subject + const yielded = cy.state('current').get('prev')?.attributes?.subject - if (val === false) { - // set current command to cy.session for more accurate codeframe + if (yielded === false) { + // set current command to cy.session for more accurate codeframe cy.state('current', sessionCommand) - return onFail($errUtils.errByPath('sessions.validate_callback_false', { reason: 'callback yielded false' }), val) + return failValidation($errUtils.errByPath('sessions.validate_callback_false', { reason: 'callback yielded false' })) } } @@ -582,11 +489,8 @@ export default function (Commands, Cypress, cy) { }) .then(() => validateSession(existingSession, step)) .then((isValidSession: boolean) => { - Cypress.state('current').set('name', step) - console.log('after create validation....', isValidSession) - Cypress.state('current').set('name', `after ${step} validation`) if (!isValidSession) { - return + throw new Error('not a valid session :(') } setSessionLogStatus(statusMap.complete(step)) diff --git a/packages/driver/src/cypress/command_queue.ts b/packages/driver/src/cypress/command_queue.ts index a92f704837db..dbd5b9e021a3 100644 --- a/packages/driver/src/cypress/command_queue.ts +++ b/packages/driver/src/cypress/command_queue.ts @@ -258,14 +258,10 @@ export class CommandQueue extends Queue<$Command> { command.set({ subject }) - // we're finished with the current command so set it back to null - this.state('current', null) - this.setSubjectForChainer(command.get('chainerId'), subject) return subject }).catch((err) => { - console.log('on command err') if (this.state('onCommandFailed')) { err = this.state('onCommandFailed')(err, this) @@ -282,24 +278,22 @@ export class CommandQueue extends Queue<$Command> { err.name = 'CypressError' } - console.log('cypress command had an error: %o', err.isRecovered) - commandRunningFailed(Cypress, this.state, err) if (err.isRecovered) { - console.log('let the queue move on to the next command') - return // let the queue move on to the next command } throw err }).finally(() => { this.state('commandIntermediateValue', undefined) - // we're finished with the current command so set it back to null - this.state('current', null) + // end / snapshot our logs if they need it command.finishLogs() + // we're finished with the current command so set it back to null + this.state('current', null) + // reset the nestedIndex back to null this.state('nestedIndex', null) }) @@ -413,28 +407,6 @@ export class CommandQueue extends Queue<$Command> { return } - // if (this.state('onCommandFailed')) { - // const handledError = this.state('onCommandFailed')(err, this) - - // // cy.state('onCommandFailed', null) - - // if (handledError) { - // return next() - // } - // } - - // debugErrors('caught error in promise chain: %o', err) - - // // since this failed this means that a specific command failed - // // and we should highlight it in red or insert a new command - // // @ts-ignore - // if (_.isObject(err) && !err.name) { - // // @ts-ignore - // err.name = 'CypressError' - // } - - // commandRunningFailed(Cypress, this.state, err) - return this.fail(err) } diff --git a/packages/driver/src/cypress/error_utils.ts b/packages/driver/src/cypress/error_utils.ts index d40e497f8669..a13cf31b8372 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) } } diff --git a/packages/reporter/src/commands/command.cy.tsx b/packages/reporter/src/commands/command.cy.tsx index ccab7bf0cea1..aa8867a54163 100644 --- a/packages/reporter/src/commands/command.cy.tsx +++ b/packages/reporter/src/commands/command.cy.tsx @@ -3,30 +3,6 @@ import Command from './command' import CommandModel from './command-model' describe('commands', () => { - it('clamps long messages', () => { - const message = 'really long looooon glong message' - cy.mount( - - ) - - cy.percySnapshot() - }) - describe('sessionPill', () => { const statusList = [ 'creating', @@ -67,7 +43,5 @@ describe('commands', () => { cy.percySnapshot() }) - - }) }) 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 b0d12478a3f1..9d0426b7a48d 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 @@ -196,7 +196,7 @@ it('validate - promise resolves false', () => { function validate () { count += 1 - return new Promise(async (resolve, reject) => { + return new Promise((resolve, reject) => { if (testData?.restoreSessionWithValidationFailure) { if ( // create session for first command run From 2446db97c8a2fdb8cfe67dd5e82a0b786f3ae628 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Fri, 21 Oct 2022 15:32:55 -0500 Subject: [PATCH 22/52] :D whoops. --- packages/driver/src/cy/commands/sessions/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/driver/src/cy/commands/sessions/index.ts b/packages/driver/src/cy/commands/sessions/index.ts index eb1b2dbedb19..06398dd3f281 100644 --- a/packages/driver/src/cy/commands/sessions/index.ts +++ b/packages/driver/src/cy/commands/sessions/index.ts @@ -408,6 +408,7 @@ export default function (Commands, Cypress, cy) { // return onFail(e) // } console.log('after execute validate', returnVal) + const returnVal = existingSession.validate.call(cy.state('ctx')) _commandToRunAfterValidation = cy.then(async () => { Cypress.state('current').set('name', '_commandToRunAfterValidation') From 7d4e8e710837908662a6d642db9182b5b90856ff Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Mon, 24 Oct 2022 16:36:24 -0500 Subject: [PATCH 23/52] fix stack traces --- packages/driver/src/cypress/command_queue.ts | 24 +++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/packages/driver/src/cypress/command_queue.ts b/packages/driver/src/cypress/command_queue.ts index dbd5b9e021a3..cfe863ee532e 100644 --- a/packages/driver/src/cypress/command_queue.ts +++ b/packages/driver/src/cypress/command_queue.ts @@ -258,6 +258,14 @@ export class CommandQueue extends Queue<$Command> { command.set({ 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, + }) + this.setSubjectForChainer(command.get('chainerId'), subject) return subject @@ -281,21 +289,21 @@ export class CommandQueue extends Queue<$Command> { commandRunningFailed(Cypress, this.state, err) if (err.isRecovered) { + 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 // let the queue move on to the next command } throw err }).finally(() => { - this.state('commandIntermediateValue', undefined) - // end / snapshot our logs if they need it command.finishLogs() - - // we're finished with the current command so set it back to null - this.state('current', null) - - // reset the nestedIndex back to null - this.state('nestedIndex', null) }) } From 49024292a091e21d0c552ef2785fc3db3ccf90ac Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Mon, 24 Oct 2022 16:52:12 -0500 Subject: [PATCH 24/52] cleanup --- packages/driver/src/cypress/log.ts | 7 +------ packages/reporter/src/errors/err-model.ts | 4 ---- packages/reporter/src/lib/variables.scss | 6 ++++++ 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/packages/driver/src/cypress/log.ts b/packages/driver/src/cypress/log.ts index e2f843cad028..81e50bd5a709 100644 --- a/packages/driver/src/cypress/log.ts +++ b/packages/driver/src/cypress/log.ts @@ -600,11 +600,6 @@ class LogManager { createLogFn (cy, state, config) { return (options: any = {}) => { - // if (!this.isInteractive) { - // // if (_skipCollectingLogs || !this.isInteractive) { - // return - // } - if (!_.isObject(options)) { $errUtils.throwErrByPath('log.invalid_argument', { args: { arg: options } }) } @@ -660,7 +655,7 @@ class LogManager { export function create (Cypress, cy, state, config) { counter = 0 - const logManager = new LogManager(Cypress.state('isInteractive')) + const logManager = new LogManager() return logManager.createLogFn(cy, state, config) } diff --git a/packages/reporter/src/errors/err-model.ts b/packages/reporter/src/errors/err-model.ts index 668affb17dfa..22375ddcb013 100644 --- a/packages/reporter/src/errors/err-model.ts +++ b/packages/reporter/src/errors/err-model.ts @@ -49,10 +49,6 @@ export default class Err { this.update(props) } - @computed get hasError () { - return this.displayMessage - } - @computed get displayMessage () { return _.compact([this.name, this.message]).join(': ') } diff --git a/packages/reporter/src/lib/variables.scss b/packages/reporter/src/lib/variables.scss index a008c3882514..47f0c615df1e 100644 --- a/packages/reporter/src/lib/variables.scss +++ b/packages/reporter/src/lib/variables.scss @@ -104,6 +104,7 @@ $fail: $red-400; $pending: $indigo-400; $pinned: $purple-400; $retried: $orange-400; +$warn: $orange-800; $link-text: $indigo-600; @@ -117,6 +118,11 @@ $err-text: $red-400; $reporter-section-background: #171926; // not a brand color +$warn-background: $red-1000; +$warn-header-background: $orange-1000; +$warn-header-text: $orange-700; +$warn-text: $orange-600; + $header-height: 64px; $reporter-contents-min-width: 170px; From 898cab6948bf4a9c36fcc2541d0814247119199d Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Mon, 24 Oct 2022 18:03:35 -0500 Subject: [PATCH 25/52] some progress. still not there. need to fix session tests & likely fix codeframes still --- .../e2e/commands/sessions/sessions.cy.js | 145 +++++++++--------- .../driver/src/cy/commands/sessions/index.ts | 19 +-- packages/driver/src/cypress/command_queue.ts | 25 ++- packages/driver/src/cypress/cy.ts | 2 + 4 files changed, 93 insertions(+), 98 deletions(-) diff --git a/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js b/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js index 44ac1ea608e5..9af51b5a98f3 100644 --- a/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js +++ b/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js @@ -255,10 +255,21 @@ describe('cy.session', { retries: 0 }, () => { it('has session details in the consoleProps', () => { const consoleProps = logs[0].get('consoleProps')() - expect(consoleProps).to.deep.eq({ + expect(consoleProps).to.deep.contains({ Command: 'session', id: 'session-1', - table: [], + Domains: 'This session capture data from localhost.', + }) + + expect(consoleProps.groups).to.have.length(1) + expect(consoleProps.groups[0]).to.deep.contains({ + name: 'localhost data:', + }) + + expect(consoleProps.groups[0].groups).to.have.length(1) + expect(consoleProps.groups[0].groups[0]).to.deep.contains({ + name: '📁 Session Storage - (1)', + items: { cypressAuthToken: '{"body":{"username":"tester"}}' }, }) }) }) @@ -266,32 +277,20 @@ describe('cy.session', { retries: 0 }, () => { describe('create session with validation flow', () => { let sessionId - // before(() => { - // setupTestContext() - // cy.log('Creating new session with validation to test against') - - // // cy.session(`session-${Cypress.state('test').id}`, setup, { validate }) - // cy.session(`session-${Cypress.state('test').id}`, () => { - // cy.log('do setup') - // }, { validate: () => { - // expect(true).to.be.false - // } }) - // }) - - // // test must be first to run before blank page visit between each test - // it('does not clear page visit from validate function', () => { - // setupTestContext() - // cy.log('Creating new session with validation to test against') - - // // cy.session(`session-${Cypress.state('test').id}`, setup, { validate }) - // cy.session(`session-${Cypress.state('test').id}`, () => { - // cy.log('do setup') - // }, { validate: () => { - // expect(true).to.be.false - // } }) - - // cy.url().should('contain', '/fixtures/auth/index.html') - // }) + before(() => { + setupTestContext() + cy.log('Creating new session with validation to test against') + expect(clearPageCount, 'total times session cleared the page').to.eq(0) + sessionId = `session-${Cypress.state('test').id}` + cy.session(sessionId, setup, { validate }) + }) + + // test must be first to run before blank page visit between each test + it('does not clear page visit from validate function', () => { + cy.log('Creating new session with validation to test against') + + cy.url().should('contain', '/fixtures/auth/index.html') + }) it('successfully creates new session and validates it', () => { expect(setup).to.be.calledOnce @@ -349,59 +348,57 @@ describe('cy.session', { retries: 0 }, () => { setupTestContext() cy.log('Creating new session with validation to test against') - // cy.once('fail', (err) => { - // 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(logs[0].get()).to.deep.contain({ - // name: 'session', - // id: sessionGroupId, - // renderProps: { - // status: 'failed', - // }, - // }) - - // validateClearLogs([logs[1], logs[2]], sessionGroupId) - - // const createNewSessionGroup = logs[3].get() - - // expect(createNewSessionGroup).to.contain({ - // displayName: 'Create new session', - // groupStart: true, - // group: sessionGroupId, - // }) + cy.once('fail', (err) => { + 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 yielded false') + // expect(logs[0].get()).to.deep.contain({ + // name: 'session', + // id: sessionGroupId, + // renderProps: { + // status: 'failed', + // }, + // }) + + validateClearLogs([logs[1], logs[2]], sessionGroupId) + + const createNewSessionGroup = logs[3].get() + + expect(createNewSessionGroup).to.contain({ + displayName: 'Create new session', + groupStart: true, + group: sessionGroupId, + }) - // expect(logs[4].get()).to.deep.contain({ - // alias: ['setupSession'], - // group: createNewSessionGroup.id, - // }) + expect(logs[4].get()).to.deep.contain({ + alias: ['setupSession'], + group: createNewSessionGroup.id, + }) - // expect(logs[5].get()).to.contain({ - // name: 'Clear page', - // group: createNewSessionGroup.id, - // }) + expect(logs[5].get()).to.contain({ + name: 'Clear page', + group: createNewSessionGroup.id, + }) - // const validateSessionGroup = logs[6].get() + const validateSessionGroup = logs[6].get() - // expect(validateSessionGroup).to.contain({ - // displayName: 'Validate session', - // group: sessionGroupId, - // }) + expect(validateSessionGroup).to.contain({ + displayName: 'Validate session', + group: sessionGroupId, + }) - // expect(logs[7].get()).to.deep.contain({ - // alias: ['validateSession'], - // group: validateSessionGroup.id, - // }) + expect(logs[7].get()).to.deep.contain({ + alias: ['validateSession'], + group: validateSessionGroup.id, + }) - // done() - // }) + done() + }) - validate.callsFake(() => false) + validate.callsFake(() => cy.wrap(false)) - cy.session(`session-${Cypress.state('test').id}`, setup, { validate: () => { - cy.get('does_not_exist', { timeout: 500 }) - } }) + cy.session(`session-${Cypress.state('test').id}`, setup, { validate }) }) }) @@ -553,7 +550,7 @@ describe('cy.session', { retries: 0 }, () => { // test must be first to run before blank page visit between each test it('does not clear page visit from validate function', () => { - cy.url().should('contain', '/fixtures/auth/index.html') + cy.url().should('contain', 'about:blank') }) it('successfully recreates session', () => { diff --git a/packages/driver/src/cy/commands/sessions/index.ts b/packages/driver/src/cy/commands/sessions/index.ts index ff784c394f58..cb0491de98be 100644 --- a/packages/driver/src/cy/commands/sessions/index.ts +++ b/packages/driver/src/cy/commands/sessions/index.ts @@ -232,7 +232,7 @@ export default function (Commands, Cypress, cy) { return existingSession.setup() - return existingSession.setup.call(cy.state('ctx')) + // return existingSession.setup.call(cy.state('ctx')) }) .then(async () => { Cypress.state('current').set('name', 'save session') @@ -299,8 +299,6 @@ export default function (Commands, Cypress, cy) { return cy.then(async () => { Cypress.state('current').set('name', 'create validate session commands') - // // uses Cypress hackery to resolve `false` if validate() resolves/returns false or throws/fails a cypress command. - // function validate (existingSession, step) { const isValidSession = true let caughtCommandErr = false let _commandToRunAfterValidation @@ -387,7 +385,8 @@ export default function (Commands, Cypress, cy) { if (!(err instanceof Error) && err === false) { console.log('ITS FALSE') - // set current command to cy.session for more accurate codeFrame + // 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)}` }) } @@ -399,15 +398,17 @@ export default function (Commands, Cypress, cy) { return enhanceErr(err) }) - // try { - returnVal = existingSession.validate.call(cy.state('ctx')) - // } catch (e) { // console.log('caught err when executing') - // return onFail(e) // } + let returnVal + + try { + returnVal = existingSession.validate.call(cy.state('ctx')) + } catch (e) { + return onFail(e) + } console.log('after execute validate', returnVal) - const returnVal = existingSession.validate.call(cy.state('ctx')) _commandToRunAfterValidation = cy.then(async () => { Cypress.state('current').set('name', '_commandToRunAfterValidation') diff --git a/packages/driver/src/cypress/command_queue.ts b/packages/driver/src/cypress/command_queue.ts index cfe863ee532e..9f05dc57fc15 100644 --- a/packages/driver/src/cypress/command_queue.ts +++ b/packages/driver/src/cypress/command_queue.ts @@ -258,13 +258,6 @@ export class CommandQueue extends Queue<$Command> { command.set({ 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, - }) this.setSubjectForChainer(command.get('chainerId'), subject) @@ -289,21 +282,23 @@ export class CommandQueue extends Queue<$Command> { commandRunningFailed(Cypress, this.state, err) if (err.isRecovered) { - 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 // let the queue move on to the next command } throw err - }).finally(() => { + }) + .then(() => { // end / snapshot our logs if they need it command.finishLogs() + + 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, + }) }) } diff --git a/packages/driver/src/cypress/cy.ts b/packages/driver/src/cypress/cy.ts index f2bf158c71f3..d4dee86d3f86 100644 --- a/packages/driver/src/cypress/cy.ts +++ b/packages/driver/src/cypress/cy.ts @@ -471,6 +471,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 { + 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) { From 8d36df54d5e6c0f18ef3a0309679701d1d574bb0 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Tue, 25 Oct 2022 10:52:06 -0500 Subject: [PATCH 26/52] clean up. --- packages/driver/cypress/e2e/commands/sessions/sessions.cy.js | 2 +- packages/driver/src/cypress/command_queue.ts | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js b/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js index 9af51b5a98f3..102762e73af6 100644 --- a/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js +++ b/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js @@ -537,7 +537,7 @@ describe('cy.session', { retries: 0 }, () => { resetMocks() validate.callsFake(() => { if (validate.callCount === 1) { - return false + return cy.wrap(false) } handleValidate() diff --git a/packages/driver/src/cypress/command_queue.ts b/packages/driver/src/cypress/command_queue.ts index 9f05dc57fc15..5d69a5c60c4a 100644 --- a/packages/driver/src/cypress/command_queue.ts +++ b/packages/driver/src/cypress/command_queue.ts @@ -23,7 +23,6 @@ const commandRunningFailed = (Cypress, state, err) => { // allow for our own custom onFail function if (err.onFail) { err.onFail(err) - // clean up this onFail callback after it's been called delete err.onFail @@ -258,7 +257,6 @@ export class CommandQueue extends Queue<$Command> { command.set({ subject }) - this.setSubjectForChainer(command.get('chainerId'), subject) return subject @@ -282,7 +280,6 @@ export class CommandQueue extends Queue<$Command> { commandRunningFailed(Cypress, this.state, err) if (err.isRecovered) { - return // let the queue move on to the next command } From c41e537f3d7d2a9e9fa833028cc6e3bc74420985 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Tue, 25 Oct 2022 12:24:04 -0500 Subject: [PATCH 27/52] fix utils tests --- .../cypress/e2e/commands/sessions/utils.cy.js | 131 +++++++++++++----- .../driver/src/cy/commands/sessions/utils.ts | 2 +- 2 files changed, 97 insertions(+), 36 deletions(-) diff --git a/packages/driver/cypress/e2e/commands/sessions/utils.cy.js b/packages/driver/cypress/e2e/commands/sessions/utils.cy.js index 00a79672c7cd..107e93a65e1b 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 or session storage associated to 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 capture 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 capture 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 capture 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 capture 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 capture 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/src/cy/commands/sessions/utils.ts b/packages/driver/src/cy/commands/sessions/utils.ts index 750f707e629f..2adf8d47c0e3 100644 --- a/packages/driver/src/cy/commands/sessions/utils.ts +++ b/packages/driver/src/cy/commands/sessions/utils.ts @@ -145,7 +145,7 @@ const getConsoleProps = (session: SessionData) => { Warning: '⚠️ There are no cookies, local storage or session storage associated to this session.', }), ...(groupsByDomain.length && { - Domains: `This session capture data from ${Object.keys(sessionDetails).join(', ')}.`, + Domains: `This session capture data from ${Object.keys(sessionDetails).join(' and ')}.`, }), groups: _.compact(groupsByDomain), } From 44727c473c2c39937e5228becfdf5146efa9b16d Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Wed, 26 Oct 2022 08:31:06 -0500 Subject: [PATCH 28/52] app tests are passing!!! --- packages/app/cypress/e2e/runner/sessions.ui.cy.ts | 3 +-- packages/driver/src/cy/assertions.ts | 2 ++ packages/driver/src/cy/retries.ts | 6 ++---- packages/driver/src/cypress/cy.ts | 2 +- packages/reporter/src/commands/command-model.ts | 1 + .../e2e/session/new_session_and_fails_validation.cy.js | 4 +++- .../cypress/e2e/session/recreates_session.cy.js | 2 +- .../session/recreates_session_and_fails_validation.cy.js | 2 +- 8 files changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/app/cypress/e2e/runner/sessions.ui.cy.ts b/packages/app/cypress/e2e/runner/sessions.ui.cy.ts index cc525996ebbc..cb410b2fbd59 100644 --- a/packages/app/cypress/e2e/runner/sessions.ui.cy.ts +++ b/packages/app/cypress/e2e/runner/sessions.ui.cy.ts @@ -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 }) @@ -272,7 +272,6 @@ describe('runner/cypress sessions.ui.spec', { 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(() => { validateSessionsInstrumentPanel(['user1']) cy.get('.command-name-session').contains('created') diff --git a/packages/driver/src/cy/assertions.ts b/packages/driver/src/cy/assertions.ts index a73b2bed942b..c4a7e488c87b 100644 --- a/packages/driver/src/cy/assertions.ts +++ b/packages/driver/src/cy/assertions.ts @@ -231,6 +231,8 @@ export const create = (Cypress: ICypress, cy: $Cy) => { const e = log.get('_error') if (e) { + log.set('_error', undefined) + return log.error(e) } diff --git a/packages/driver/src/cy/retries.ts b/packages/driver/src/cy/retries.ts index 088b0b2766c3..672f896f7fe2 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: () => 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,7 @@ export const create = (Cypress: ICypress, state: StateFunc, timeout: $Cy['timeou const retryErr = mergeErrProps(error, retryErrProps) throwErr(retryErr, { - onFail: onFail || log, + onFail: finishAssertions, }) } } diff --git a/packages/driver/src/cypress/cy.ts b/packages/driver/src/cypress/cy.ts index d4dee86d3f86..d1245c54454e 100644 --- a/packages/driver/src/cypress/cy.ts +++ b/packages/driver/src/cypress/cy.ts @@ -270,7 +270,7 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert this.verifyUpcomingAssertions = assertions.verifyUpcomingAssertions const onFinishAssertions = function () { - return assertions.finishAssertions.apply(window, arguments as any) + return assertions.finishAssertions.apply(window) } const retries = createRetries(Cypress, state, this.timeout, this.clearTimeout, this.whenStable, onFinishAssertions) diff --git a/packages/reporter/src/commands/command-model.ts b/packages/reporter/src/commands/command-model.ts index 944957451de9..9e0a94fcc37a 100644 --- a/packages/reporter/src/commands/command-model.ts +++ b/packages/reporter/src/commands/command-model.ts @@ -91,6 +91,7 @@ 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 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/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') }) From 87ffa231e690a5cc052a27cbb7089ab7e8c587ec Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Wed, 26 Oct 2022 08:31:20 -0500 Subject: [PATCH 29/52] and this. --- packages/app/cypress/e2e/runner/sessions.ui.cy.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/app/cypress/e2e/runner/sessions.ui.cy.ts b/packages/app/cypress/e2e/runner/sessions.ui.cy.ts index cb410b2fbd59..28b46b05f8cf 100644 --- a/packages/app/cypress/e2e/runner/sessions.ui.cy.ts +++ b/packages/app/cypress/e2e/runner/sessions.ui.cy.ts @@ -269,9 +269,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).click().within(() => { validateSessionsInstrumentPanel(['user1']) cy.get('.command-name-session').contains('created') From 125df0551262a64cd7d5ae5de262fff8c76e91cd Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Wed, 26 Oct 2022 12:36:07 -0500 Subject: [PATCH 30/52] LETS GOOOOOOOOOOO!!!!! all my session tests are passing! lets see if this works for the rest of cypress --- .../e2e/commands/sessions/sessions.cy.js | 662 ++++++++++-------- packages/driver/src/cy/assertions.ts | 14 +- packages/driver/src/cy/commands/agents.ts | 79 ++- .../driver/src/cy/commands/sessions/index.ts | 40 +- packages/driver/src/cy/retries.ts | 11 +- packages/driver/src/cypress/cy.ts | 2 +- packages/driver/src/cypress/utils.ts | 2 +- packages/reporter/src/commands/command.tsx | 10 +- 8 files changed, 472 insertions(+), 348 deletions(-) diff --git a/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js b/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js index ffc81496cafa..a52396c2dfbf 100644 --- a/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js +++ b/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js @@ -260,54 +260,22 @@ 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 capture data from localhost.') - expect(logs[5].get()).to.contain({ - name: 'Clear page', - group: createNewSessionGroup.id, - }) - }) + 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) - it('creates new session instrument with session details', () => { - const sessionInfo = logs[0].get('sessionInfo') + const sessionStorageData = consoleProps.groups[0].groups[0] - expect(sessionInfo).to.deep.eq({ - id: 'session-1', - isGlobalSession: false, - status: 'created', + expect(sessionStorageData.name).to.contain('Session Storage - (1)') + expect(sessionStorageData.items).to.deep.eq({ cypressAuthToken: '{"body":{"username":"tester"}}' }) }) }) - it('has session details in the consoleProps', () => { - const consoleProps = logs[0].get('consoleProps')() - - expect(consoleProps).to.deep.contains({ - Command: 'session', - id: 'session-1', - Domains: 'This session capture data from localhost.', - }) - - expect(consoleProps.groups).to.have.length(1) - expect(consoleProps.groups[0]).to.deep.contains({ - name: 'localhost data:', - }) - - expect(consoleProps.groups[0].groups).to.have.length(1) - expect(consoleProps.groups[0].groups[0]).to.deep.contains({ - name: '📁 Session Storage - (1)', - items: { cypressAuthToken: '{"body":{"username":"tester"}}' }, - }) - }) - }) - - describe('create session with validation flow', () => { + describe('create session with validation flow', () => { let sessionId before(() => { @@ -370,67 +338,6 @@ describe('cy.session', { retries: 0 }, () => { alias: ['validateSession'], group: validateSessionGroup.id, }) - - done() - }) - - validate.callsFake(() => cy.wrap(false)) - - cy.session(`session-${Cypress.state('test').id}`, setup, { validate }) - }) - }) - - describe('restores saved session flow', () => { - let sessionId - - before(() => { - setupTestContext() - cy.log('Creating new session for test') - sessionId = `session-${Cypress.state('test').id}` - cy.session(sessionId, setup) - .then(() => { - // reset and only test restored session - resetMocks() - }) - - cy.log('restore session to test against') - cy.session(sessionId, setup) - }) - - // test must be first to run before blank page visit between each test - it('clears page after setup runs', () => { - cy.url().should('eq', 'about:blank') - }) - - it('successfully restores saved session', () => { - expect(setup).to.not.be.called - expect(validate).to.not.be.called - expect(clearPageCount, 'total times session cleared the page').to.eq(1) - }) - - it('groups session logs correctly', () => { - expect(logs[0].get()).to.contain({ - name: 'session', - id: sessionGroupId, - }) - - expect(logs[0].get()).to.deep.contain({ - name: 'session', - id: sessionGroupId, - sessionInfo: { - id: sessionId, - isGlobalSession: false, - status: 'restored', - }, - }) - - validateClearLogs([logs[1], logs[2]], sessionGroupId) - - const restoreSavedSessionGroup = logs[3].get() - - expect(restoreSavedSessionGroup).to.contain({ - displayName: 'Restore saved session', - group: sessionGroupId, }) }) @@ -443,7 +350,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, @@ -486,227 +393,395 @@ describe('cy.session', { retries: 0 }, () => { group: validateSessionGroup.id, }) - const validateSessionGroup = logs[4].get() + done() + }) - expect(validateSessionGroup).to.contain({ - displayName: 'Validate session', - group: sessionGroupId, - }) + validate.rejects(false) - expect(logs[5].get()).to.deep.contain({ - alias: ['validateSession'], - group: validateSessionGroup.id, + cy.session(`session-${Cypress.state('test').id}`, setup, { validate }) }) }) - }) - describe('recreates existing session flow', () => { - let sessionId + describe('restores saved session flow', () => { + let sessionId - before(() => { - setupTestContext() - cy.log('Creating new session for test') - sessionId = `session-${Cypress.state('test').id}` - cy.session(sessionId, setup, { validate }) - .then(() => { + before(() => { + setupTestContext() + cy.log('Creating new session for test') + sessionId = `session-${Cypress.state('test').id}` + cy.session(sessionId, setup) + .then(() => { // reset and only test restored session - resetMocks() - validate.callsFake(() => { - if (validate.callCount === 1) { - return cy.wrap(false) - } + resetMocks() + }) - handleValidate() + cy.log('restore session to test against') + cy.session(sessionId, setup) + }) + + // test must be first to run before blank page visit between each test + it('clears page after setup runs', () => { + cy.url().should('eq', 'about:blank') + }) + + it('successfully restores saved session', () => { + expect(setup).to.not.be.called + expect(validate).to.not.be.called + expect(clearPageCount, 'total times session cleared the page').to.eq(1) + }) + + it('groups session logs correctly', () => { + expect(logs[0].get()).to.contain({ + name: 'session', + id: sessionGroupId, }) - cy.log('restore session to test against') - cy.session(sessionId, setup, { validate }) - }) + expect(logs[0].get()).to.deep.contain({ + name: 'session', + id: sessionGroupId, + sessionInfo: { + id: sessionId, + isGlobalSession: false, + status: 'restored', + }, + }) - // test must be first to run before blank page visit between each test - it('does not clear page visit from validate function', () => { - cy.url().should('contain', 'about:blank') - }) + validateClearLogs([logs[1], logs[2]], sessionGroupId) - it('successfully recreates session', () => { - expect(setup).to.be.calledOnce - expect(validate).to.be.calledTwice - expect(clearPageCount, 'total times session cleared the page').to.eq(3) + const restoreSavedSessionGroup = logs[3].get() + + expect(restoreSavedSessionGroup).to.contain({ + displayName: 'Restore saved session', + group: sessionGroupId, + }) + }) }) - it('groups session logs correctly', () => { - expect(logs[0].get()).to.contain({ - name: 'session', - id: sessionGroupId, + describe('restores saved session with validation flow', () => { + let sessionId + + before(() => { + setupTestContext() + cy.log('Creating new session for test') + sessionId = `session-${Cypress.state('test').id}` + cy.session(sessionId, setup, { validate }) + .then(() => { + // reset and only test restored session + resetMocks() + }) + + cy.log('restore session to test against') + cy.session(sessionId, setup, { validate }) }) - expect(logs[0].get()).to.deep.contain({ - name: 'session', - id: sessionGroupId, - sessionInfo: { - id: sessionId, - isGlobalSession: false, - status: 'recreated', - }, + // test must be first to run before blank page visit between each test + it('does not clear page visit from validate function', () => { + cy.url().should('contain', '/fixtures/auth/index.html') }) - validateClearLogs([logs[1], logs[2]], sessionGroupId) + it('successfully restores saved session', () => { + expect(setup).to.not.be.called + expect(validate).to.be.calledOnce + expect(clearPageCount, 'total times session cleared the page').to.eq(1) + }) - const restoreSavedSessionGroup = logs[3].get() + it('groups session logs correctly', () => { + expect(logs[0].get()).to.contain({ + name: 'session', + id: sessionGroupId, + }) - expect(restoreSavedSessionGroup).to.contain({ - displayName: 'Restore saved session', - group: sessionGroupId, - }) + expect(logs[0].get()).to.deep.contain({ + name: 'session', + id: sessionGroupId, + sessionInfo: { + id: sessionId, + isGlobalSession: false, + status: 'restored', + }, + }) - const validateSessionGroup = logs[4].get() + validateClearLogs([logs[1], logs[2]], sessionGroupId) - expect(validateSessionGroup).to.contain({ - displayName: 'Validate session', - group: sessionGroupId, - }) + const restoreSavedSessionGroup = logs[3].get() - expect(logs[5].get()).to.deep.contain({ - alias: ['validateSession'], - group: validateSessionGroup.id, - }) + expect(restoreSavedSessionGroup).to.contain({ + displayName: 'Restore saved session', + group: sessionGroupId, + }) - expect(logs[6].get()).to.deep.contain({ - group: validateSessionGroup.id, + const validateSessionGroup = logs[4].get() + + expect(validateSessionGroup).to.contain({ + displayName: 'Validate session', + group: sessionGroupId, + }) + + expect(logs[5].get()).to.deep.contain({ + alias: ['validateSession'], + group: validateSessionGroup.id, + }) }) + }) - expect(logs[6].get('error').message).to.eq('Your `cy.session` **validate** callback returned false.') + describe('recreates existing session flow', () => { + let sessionId - validateClearLogs([logs[7], logs[8]], sessionGroupId) + before(() => { + setupTestContext() + cy.log('Creating new session for test') + sessionId = `session-${Cypress.state('test').id}` + cy.session(sessionId, setup, { validate }) + .then(() => { + // reset and only test restored session + resetMocks() + validate.callsFake(() => { + if (validate.callCount === 1) { + return Promise.reject(false) + } - const createNewSessionGroup = logs[9].get() + handleValidate() + }) + }) - expect(createNewSessionGroup).to.contain({ - displayName: 'Recreate session', - groupStart: true, - group: sessionGroupId, + cy.log('restore session to test against') + cy.session(sessionId, setup, { validate }) }) - expect(logs[10].get()).to.deep.contain({ - alias: ['setupSession'], - group: createNewSessionGroup.id, + // test must be first to run before blank page visit between each test + it('does not clear page visit from validate function', () => { + cy.url().should('contain', '/fixtures/auth/index.html') }) - expect(logs[11].get()).to.contain({ - name: 'Clear page', - group: createNewSessionGroup.id, + it('successfully recreates session', () => { + expect(setup).to.be.calledOnce + expect(validate).to.be.calledTwice + expect(clearPageCount, 'total times session cleared the page').to.eq(3) }) - const secondValidateSessionGroup = logs[12].get() + it('groups session logs correctly', () => { + expect(logs[0].get()).to.contain({ + name: 'session', + id: sessionGroupId, + }) - expect(secondValidateSessionGroup).to.contain({ - displayName: 'Validate session', - group: sessionGroupId, - }) + expect(logs[0].get()).to.deep.contain({ + name: 'session', + id: sessionGroupId, + sessionInfo: { + id: sessionId, + isGlobalSession: false, + status: 'recreated', + }, + }) + + validateClearLogs([logs[1], logs[2]], sessionGroupId) + + const restoreSavedSessionGroup = logs[3].get() + + expect(restoreSavedSessionGroup).to.contain({ + displayName: 'Restore saved session', + group: sessionGroupId, + }) + + const validateSessionGroup = logs[4].get() + + expect(validateSessionGroup).to.contain({ + displayName: 'Validate session', + group: sessionGroupId, + }) + + expect(logs[5].get()).to.deep.contain({ + alias: ['validateSession'], + group: validateSessionGroup.id, + }) + + // 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[6], logs[7]], sessionGroupId) + + const createNewSessionGroup = logs[8].get() + + expect(createNewSessionGroup).to.contain({ + displayName: 'Recreate session', + groupStart: true, + group: sessionGroupId, + }) + + expect(logs[9].get()).to.deep.contain({ + alias: ['setupSession'], + group: createNewSessionGroup.id, + }) + + expect(logs[10].get()).to.contain({ + name: 'Clear page', + group: createNewSessionGroup.id, + }) + + const secondValidateSessionGroup = logs[11].get() - expect(logs[13].get()).to.deep.contain({ - alias: ['validateSession'], - group: secondValidateSessionGroup.id, + expect(secondValidateSessionGroup).to.contain({ + displayName: 'Validate session', + group: sessionGroupId, + }) + + expect(logs[12].get()).to.deep.contain({ + alias: ['validateSession'], + group: secondValidateSessionGroup.id, + }) }) }) - }) - describe('recreates existing session with failed validation flow', () => { - it('fails to recreate session and logs correctly', function () { - setupTestContext() - cy.log('Creating new session for test') - cy.session(`session-${Cypress.state('test').id}`, setup, { validate }) - .then(() => { + describe('recreates existing session with failed validation flow', () => { + it('fails to recreate session and logs correctly', function (done) { + setupTestContext() + cy.log('Creating new session for test') + cy.session(`session-${Cypress.state('test').id}`, setup, { validate }) + .then(() => { // reset and only test restored session - resetMocks() - validate.callsFake(() => { - return false - // cy.get('does not exist') + resetMocks() + validate.callsFake(() => Promise.reject(false)) }) - }) - // cy.once('fail', (err) => { - // expect(err.message).to.contain('Your `cy.session` **validate** callback returned false') - // expect(setup).to.be.calledOnce - // expect(validate).to.be.calledTwice - // expect(clearPageCount, 'total times session cleared the page').to.eq(3) + cy.once('fail', (err) => { + 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) + + expect(logs[0].get()).to.contain({ + name: 'session', + id: sessionGroupId, + }) + + expect(logs[0].get()).to.deep.contain({ + name: 'session', + id: sessionGroupId, + sessionInfo: { + id: `session-${Cypress.state('test').id}`, + isGlobalSession: false, + status: 'failed', + }, + }) + + validateClearLogs([logs[1], logs[2]], sessionGroupId) + + const restoreSavedSessionGroup = logs[3].get() - // expect(logs[0].get()).to.contain({ - // name: 'session', - // id: sessionGroupId, - // }) + expect(restoreSavedSessionGroup).to.contain({ + displayName: 'Restore saved session', + group: sessionGroupId, + }) - // expect(logs[0].get()).to.deep.contain({ - // name: 'session', - // id: sessionGroupId, - // sessionInfo: { - // id: `session-${Cypress.state('test').id}`, - // isGlobalSession: false, - // status: 'failed', - // }, - // }) + const validateSessionGroup = logs[4].get() - // validateClearLogs([logs[1], logs[2]], sessionGroupId) + expect(validateSessionGroup).to.contain({ + displayName: 'Validate session', + group: sessionGroupId, + }) - // const restoreSavedSessionGroup = logs[3].get() + expect(logs[5].get()).to.deep.contain({ + alias: ['validateSession'], + group: validateSessionGroup.id, + }) + + // 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.') - // expect(restoreSavedSessionGroup).to.contain({ - // displayName: 'Restore saved session', - // group: sessionGroupId, - // }) + validateClearLogs([logs[6], logs[7]], sessionGroupId) - // const validateSessionGroup = logs[4].get() + const createNewSessionGroup = logs[8].get() + + expect(createNewSessionGroup).to.contain({ + displayName: 'Recreate session', + groupStart: true, + group: sessionGroupId, + }) + + expect(logs[9].get()).to.deep.contain({ + alias: ['setupSession'], + group: createNewSessionGroup.id, + }) + + expect(logs[10].get()).to.contain({ + name: 'Clear page', + group: createNewSessionGroup.id, + }) + + const secondValidateSessionGroup = logs[11].get() + + expect(secondValidateSessionGroup).to.contain({ + displayName: 'Validate session', + group: sessionGroupId, + }) + + expect(logs[12].get()).to.deep.contain({ + alias: ['validateSession'], + group: secondValidateSessionGroup.id, + }) + + done() + }) + + cy.log('restore session to test against') + cy.session(`session-${Cypress.state('test').id}`, setup, { validate }) + }) + }) + }) + }) - // expect(validateSessionGroup).to.contain({ - // displayName: 'Validate session', - // group: sessionGroupId, - // }) + describe('testIsolation=off', { testIsolation: 'off' }, () => { + before(async () => { + // manually ensure clear browser state! since we turned testIsolation off + await Cypress.session.clearCurrentSessionData() + }) - // expect(logs[5].get()).to.deep.contain({ - // alias: ['validateSession'], - // group: validateSessionGroup.id, - // }) + describe('test:before:run:async', () => { + it('does not clear page before each run', () => { + cy.visit('/fixtures/form.html') + .then(async () => { + cy.spy(Cypress, 'action').log(false) - // expect(logs[6].get()).to.deep.contain({ - // group: validateSessionGroup.id, - // }) + await Cypress.action('runner:test:before:run:async', {}) - // expect(logs[6].get('error').message).to.eq('Your `cy.session` **validate** callback returned false.') + expect(Cypress.action).not.to.be.calledWith('cy:url:changed') + expect(Cypress.action).not.to.be.calledWith('cy:visit:blank') + }) + .url('/fixtures/form.html') + }) - // validateClearLogs([logs[7], logs[8]], sessionGroupId) + it('does not clear session data before each run', async () => { + const clearCurrentSessionData = cy.spy(Cypress.session, 'clearCurrentSessionData') - // const createNewSessionGroup = logs[9].get() + await Cypress.action('runner:test:before:run:async', {}) - // expect(createNewSessionGroup).to.contain({ - // displayName: 'Recreate session', - // groupStart: true, - // group: sessionGroupId, - // }) + expect(clearCurrentSessionData).not.to.be.called + }) - // expect(logs[10].get()).to.deep.contain({ - // alias: ['setupSession'], - // group: createNewSessionGroup.id, - // }) + it('does not reset rendered html origins before each run', async () => { + const backendSpy = cy.spy(Cypress, 'backend') - // expect(logs[11].get()).to.contain({ - // name: 'Clear page', - // group: createNewSessionGroup.id, - // }) + await Cypress.action('runner:test:before:run:async', {}) - // const secondValidateSessionGroup = logs[12].get() + expect(backendSpy).not.to.be.calledWith('reset:rendered:html:origins') + }) - // expect(secondValidateSessionGroup).to.contain({ - // displayName: 'Validate session', - // group: sessionGroupId, - // }) + it('does not clear the browser context before each run', () => { + cy.window() + .then((win) => { + win.cookie = 'key=value; SameSite=Strict; Secure; Path=/fixtures' + win.localStorage.setItem('animal', 'bear') + win.sessionStorage.setItem('food', 'burgers') + }) + .then(async () => { + cy.spy(Cypress, 'action').log(false) - // expect(logs[13].get()).to.deep.contain({ - // alias: ['validateSession'], - // group: secondValidateSessionGroup.id, - // }) + await Cypress.action('runner:test:before:run:async', {}) - // done() - // }) + expect(Cypress.action).not.to.be.calledWith('cy:url:changed') + expect(Cypress.action).not.to.be.calledWith('cy:visit:blank') + }) cy.window().its('cookie').should('equal', 'key=value; SameSite=Strict; Secure; Path=/fixtures') cy.window().its('localStorage').should('have.length', 1).should('deep.contain', { animal: 'bear' }) @@ -842,11 +917,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 capture 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"}}' }) }) }) @@ -922,7 +1004,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, @@ -966,7 +1048,7 @@ describe('cy.session', { retries: 0 }, () => { done() }) - validate.callsFake(() => false) + validate.callsFake(() => Promise.reject(false)) cy.session(`session-${Cypress.state('test').id}`, setup, { validate }) }) @@ -1111,7 +1193,7 @@ describe('cy.session', { retries: 0 }, () => { resetMocks() validate.callsFake(() => { if (validate.callCount === 1) { - return false + return Promise.reject(false) } handleValidate() @@ -1172,18 +1254,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', @@ -1191,19 +1270,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, }) @@ -1218,11 +1297,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(0) @@ -1266,18 +1345,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', @@ -1285,19 +1361,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, }) diff --git a/packages/driver/src/cy/assertions.ts b/packages/driver/src/cy/assertions.ts index c4a7e488c87b..2cf4fc2dde94 100644 --- a/packages/driver/src/cy/assertions.ts +++ b/packages/driver/src/cy/assertions.ts @@ -222,12 +222,18 @@ export const create = (Cypress: ICypress, cy: $Cy) => { return null } - const finishAssertions = () => { - cy.state('current').get('logs').forEach((log) => { + const finishAssertions = (err) => { + const logs = cy.state('current').get('logs') + + logs.forEach((log, index) => { if (log.get('next') || !log.get('snapshots')) { log.snapshot() } + if (err && index === logs.length - 1) { + return log.error(err) + } + const e = log.get('_error') if (e) { @@ -445,11 +451,11 @@ export const create = (Cypress: ICypress, cy: $Cy) => { // when we're told not to retry if (err.retry === false) { // finish the assertions - finishAssertions() + // finishAssertions() // and then push our command into this err try { - $errUtils.throwErr(err, { onFail: options._log }) + $errUtils.throwErr(err, { onFail: finishAssertions }) } catch (e) { err = e } diff --git a/packages/driver/src/cy/commands/agents.ts b/packages/driver/src/cy/commands/agents.ts index 611e2573a951..3c266a50fc37 100644 --- a/packages/driver/src/cy/commands/agents.ts +++ b/packages/driver/src/cy/commands/agents.ts @@ -68,9 +68,9 @@ const onInvoke = function (Cypress, obj, args) { const logProps: Record = { name: agentName, message: obj.message, - error: obj.error, + // error: obj.error, type: 'parent', - end: true, + // end: true, snapshot: !agent._noSnapshot, event: true, consoleProps () { @@ -85,13 +85,13 @@ const onInvoke = function (Cypress, obj, args) { consoleObj.Alias = agent._cyAlias consoleObj[display(obj.name)] = obj.obj - consoleObj.Arguments = obj.call.args - consoleObj.Context = obj.call.thisValue - consoleObj.Returned = obj.call.returnValue + consoleObj.Arguments = obj.call?.args + consoleObj.Context = obj.call?.thisValue + consoleObj.Returned = obj.call?.returnValue - if (obj.error) { - consoleObj.Error = obj.error.stack - } + // if (obj.error) { + // consoleObj.Error = obj.error.stack + // } for (let fake of fakes) { const count = fake._cyCount @@ -117,10 +117,6 @@ const onInvoke = function (Cypress, obj, args) { return Cypress.log(logProps) } -const onError = (err) => { - return $errUtils.throwErr(err) -} - // create a global sandbox // to be used through all the tests const sandbox = createSandbox() @@ -180,19 +176,8 @@ export default function (Commands, Cypress, cy, state) { const { invoke } = agent agent.invoke = function (func, thisValue, args) { - let error = null let returned = null - // because our spy could potentially fail here - // we need to wrap this in a try / catch - // so we still emit the command that failed - // and the user can easily find the error - try { - returned = invoke.call(this, func, thisValue, args) - } catch (e: any) { - error = e - } - const props = { count, name: type, @@ -201,18 +186,54 @@ export default function (Commands, Cypress, cy, state) { agent, call: agent.lastCall, callCount: agent.callCount, - error, log, } - onInvoke(Cypress, props, args) + const invokeLog = onInvoke(Cypress, props, args) + + // return invoke.call(this, func, thisValue, args) + // because our spy could potentially fail here + // we need to wrap this in a try / catch + // so we still emit the command that failed + // and the user can easily find the error + try { + returned = invoke.call(this, func, thisValue, args) + } catch (err: any) { + $errUtils.throwErr(err) + // { + // onFail: () => { + // // emily too do...setting error on this log caused double error rendering + // // for restored errors. want this for console props & what not. + // // might be the reporter need to verify the last child log only shows in-line + // // errors if reported multiple times. + // invokeLog.set({ + // state: 'failed', + // }) + // }, + // }) + } - // if an error did exist then we need - // to bubble it up - if (error) { - onError(error) + if ($utils.isPromiseLike(returned)) { + return returned + .then((val) => { + invokeLog?.end() + console.log('val', val) + + return val + }) + .catch((err) => { + console.log('AGENT HERE', err) + invokeLog?.set({ + state: 'failed', + error: err, + }) + + $errUtils.throwErr(err) + }) } + invokeLog?.end() + // make sure we return the invoked return value // of the spy return returned diff --git a/packages/driver/src/cy/commands/sessions/index.ts b/packages/driver/src/cy/commands/sessions/index.ts index bf82700ac567..7a14ca1a9297 100644 --- a/packages/driver/src/cy/commands/sessions/index.ts +++ b/packages/driver/src/cy/commands/sessions/index.ts @@ -1,5 +1,7 @@ import _ from 'lodash' import stringifyStable from 'json-stable-stringify' +import $utils from '../../../cypress/utils' + import $errUtils from '../../../cypress/error_utils' import $stackUtils from '../../../cypress/stack_utils' import logGroup from '../../logGroup' @@ -341,7 +343,7 @@ export default function (Commands, Cypress, cy) { } }, snapshot: true, - error: err, + // error: err, }) $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) @@ -388,12 +390,12 @@ export default function (Commands, Cypress, cy) { // err = new Error(err) // } - if (!(err instanceof Error) && err === false) { - console.log('ITS FALSE') - // 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)}` }) - } + // if (!(err instanceof Error) && err === false) { + // console.log('ITS FALSE') + // // 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)}` }) + // } err.isRecovered = true @@ -408,13 +410,13 @@ export default function (Commands, Cypress, cy) { // } let returnVal - try { - returnVal = existingSession.validate.call(cy.state('ctx')) - } catch (e) { - return onFail(e) - } - console.log('after execute validate', returnVal) + // try { + returnVal = existingSession.validate.call(cy.state('ctx')) + // } catch (e) { + // debugger + // return onFail(e) + // } _commandToRunAfterValidation = cy.then(async () => { Cypress.state('current').set('name', '_commandToRunAfterValidation') Cypress.state('onCommandFailed', null) @@ -424,20 +426,22 @@ export default function (Commands, Cypress, cy) { } const failValidation = (err) => { - err.onFail = (err) => { - validateLog.error(err) - } - if (step === 'restore') { + enhanceErr(err) + // move to recreate session flow return !isValidSession } + err.onFail = (err) => { + validateLog.error(err) + } + throw enhanceErr(err) } // 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') { + if ($utils.isPromiseLike(returnVal)) { return returnVal .then((val) => { if (val === false) { diff --git a/packages/driver/src/cy/retries.ts b/packages/driver/src/cy/retries.ts index 672f896f7fe2..039fb0d72146 100644 --- a/packages/driver/src/cy/retries.ts +++ b/packages/driver/src/cy/retries.ts @@ -86,7 +86,16 @@ export const create = (Cypress: ICypress, state: StateFunc, timeout: $Cy['timeou const retryErr = mergeErrProps(error, retryErrProps) throwErr(retryErr, { - onFail: finishAssertions, + onFail: (err) => { + console.log('timed out', err) + if (onFail) { + err = onFail(err) + } + + console.log('onFail timed out', err) + + finishAssertions(err) + }, }) } } diff --git a/packages/driver/src/cypress/cy.ts b/packages/driver/src/cypress/cy.ts index d1245c54454e..d4dee86d3f86 100644 --- a/packages/driver/src/cypress/cy.ts +++ b/packages/driver/src/cypress/cy.ts @@ -270,7 +270,7 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert this.verifyUpcomingAssertions = assertions.verifyUpcomingAssertions const onFinishAssertions = function () { - return assertions.finishAssertions.apply(window) + return assertions.finishAssertions.apply(window, arguments as any) } const retries = createRetries(Cypress, state, this.timeout, this.clearTimeout, this.whenStable, onFinishAssertions) diff --git a/packages/driver/src/cypress/utils.ts b/packages/driver/src/cypress/utils.ts index 1a983dfc6f2f..992c1bd3145b 100644 --- a/packages/driver/src/cypress/utils.ts +++ b/packages/driver/src/cypress/utils.ts @@ -397,6 +397,6 @@ export default { }, isPromiseLike (ret) { - return ret && _.isFunction(ret.then) + return ret && _.isObject(ret) && _.isFunction(ret.then) && _.isFunction(ret.catch) }, } diff --git a/packages/reporter/src/commands/command.tsx b/packages/reporter/src/commands/command.tsx index dfad79463337..a6d1695ce225 100644 --- a/packages/reporter/src/commands/command.tsx +++ b/packages/reporter/src/commands/command.tsx @@ -401,7 +401,15 @@ class Command extends Component { {this._children()} {model.err?.isRecovered && ( -
  • +
  • + +
  • )} ) From 200b7f7d37b107f4ed5615fe05f112af67651b56 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Wed, 26 Oct 2022 17:32:46 -0500 Subject: [PATCH 31/52] fix a handful of errors --- .../cypress/e2e/commands/actions/click.cy.js | 31 +++--- .../cypress/e2e/commands/connectors.cy.js | 94 +++++++++++++------ .../driver/src/cy/commands/actions/click.ts | 17 ++-- 3 files changed, 87 insertions(+), 55 deletions(-) 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/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/src/cy/commands/actions/click.ts b/packages/driver/src/cy/commands/actions/click.ts index a14d43773ed9..d91599057d93 100644 --- a/packages/driver/src/cy/commands/actions/click.ts +++ b/packages/driver/src/cy/commands/actions/click.ts @@ -220,16 +220,19 @@ 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() - } - } + console.log('error!') // 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) { + console.log('on click fail') + if (options._log) { + // snapshot only on click failure + options._log.snapshot().error(err) + } + }, + }) }) } From e17ee7db80f435725bb2db2c868e5f5eca84d826 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Thu, 27 Oct 2022 11:29:40 -0500 Subject: [PATCH 32/52] fix tests !! --- .../cypress/e2e/commands/assertions.cy.js | 22 ++-- .../driver/cypress/e2e/commands/files.cy.js | 93 ++++++++-------- .../e2e/commands/querying/querying.cy.js | 104 ++++++++++++++---- .../cypress/e2e/commands/traversals.cy.js | 21 +++- packages/driver/src/cy/assertions.ts | 37 +++---- packages/driver/src/cy/commands/agents.ts | 79 +++++-------- .../src/cy/commands/querying/querying.ts | 4 +- 7 files changed, 213 insertions(+), 147 deletions(-) diff --git a/packages/driver/cypress/e2e/commands/assertions.cy.js b/packages/driver/cypress/e2e/commands/assertions.cy.js index 7dbaed31fbb6..db54ada0822d 100644 --- a/packages/driver/cypress/e2e/commands/assertions.cy.js +++ b/packages/driver/cypress/e2e/commands/assertions.cy.js @@ -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() }) diff --git a/packages/driver/cypress/e2e/commands/files.cy.js b/packages/driver/cypress/e2e/commands/files.cy.js index b5d2a8da64f7..e0d72ac37523 100644 --- a/packages/driver/cypress/e2e/commands/files.cy.js +++ b/packages/driver/cypress/e2e/commands/files.cy.js @@ -165,11 +165,17 @@ describe('src/cy/commands/files', () => { this.logs = [] - cy.on('log:added', (attrs, log) => { + const collectLogs = (attrs, log) => { if (attrs.name === 'readFile') { - this.lastLog = log - this.logs.push(log) + this.fileLog = log } + + this.logs?.push(log) + } + + cy.on('log:added', collectLogs) + cy.on('fail', () => { + cy.off('log:added', collectLogs) }) return null @@ -177,11 +183,10 @@ 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 +198,10 @@ 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 +213,10 @@ 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 +236,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`\ \`cy.readFile(\"foo\")\` failed while trying to read the file at the following path: @@ -265,11 +267,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 +301,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 +325,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 +354,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 +377,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 +401,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/querying/querying.cy.js b/packages/driver/cypress/e2e/commands/querying/querying.cy.js index 412953cb491f..16374054c22b 100644 --- a/packages/driver/cypress/e2e/commands/querying/querying.cy.js +++ b/packages/driver/cypress/e2e/commands/querying/querying.cy.js @@ -738,12 +738,16 @@ 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) => { + console.log('collect 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 @@ -860,6 +864,38 @@ 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.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.on('fail', (err) => { + const { lastLog } = this + + 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 }).should('have.class', 'hi') + }) + it('does not include message about why element was not visible', (done) => { cy.on('fail', (err) => { expect(err.message).not.to.include('why this element is not visible') @@ -962,16 +998,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() }) @@ -1674,11 +1718,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 @@ -1779,15 +1821,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 +1847,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/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/src/cy/assertions.ts b/packages/driver/src/cy/assertions.ts index 2cf4fc2dde94..6229d5a38570 100644 --- a/packages/driver/src/cy/assertions.ts +++ b/packages/driver/src/cy/assertions.ts @@ -225,24 +225,26 @@ export const create = (Cypress: ICypress, cy: $Cy) => { const finishAssertions = (err) => { const logs = cy.state('current').get('logs') - logs.forEach((log, index) => { - if (log.get('next') || !log.get('snapshots')) { - log.snapshot() - } + let hasLoggedError = false - if (err && index === logs.length - 1) { - return log.error(err) - } + logs.reverse().forEach((log, index) => { + if (log._shouldAutoEnd()) { + if (log.get('next') || !log.get('snapshots')) { + log.snapshot() + } - const e = log.get('_error') + if (err && !hasLoggedError) { + hasLoggedError = true - if (e) { - log.set('_error', undefined) + return log.error(err) + } - return log.error(e) - } + if (err && err.issuesCommunicatingOrFinding && index === logs.length - 1) { + return log.error(err) + } - return log.end() + return log.end() + } }) } @@ -319,6 +321,7 @@ export const create = (Cypress: ICypress, cy: $Cy) => { cy.ensureCommandCanCommunicateWithAUT(err) ensureExistence() } catch (e2) { + e2.issuesCommunicatingOrFinding = true err = e2 } @@ -327,13 +330,9 @@ 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)) { throw err } @@ -346,7 +345,7 @@ export const create = (Cypress: ICypress, cy: $Cy) => { onFail.call(this, err, isDefaultAssertionErr, cmds) } } catch (e3) { - finishAssertions() + e3.onFail = finishAssertions throw e3 } diff --git a/packages/driver/src/cy/commands/agents.ts b/packages/driver/src/cy/commands/agents.ts index 3c266a50fc37..611e2573a951 100644 --- a/packages/driver/src/cy/commands/agents.ts +++ b/packages/driver/src/cy/commands/agents.ts @@ -68,9 +68,9 @@ const onInvoke = function (Cypress, obj, args) { const logProps: Record = { name: agentName, message: obj.message, - // error: obj.error, + error: obj.error, type: 'parent', - // end: true, + end: true, snapshot: !agent._noSnapshot, event: true, consoleProps () { @@ -85,13 +85,13 @@ const onInvoke = function (Cypress, obj, args) { consoleObj.Alias = agent._cyAlias consoleObj[display(obj.name)] = obj.obj - consoleObj.Arguments = obj.call?.args - consoleObj.Context = obj.call?.thisValue - consoleObj.Returned = obj.call?.returnValue + consoleObj.Arguments = obj.call.args + consoleObj.Context = obj.call.thisValue + consoleObj.Returned = obj.call.returnValue - // if (obj.error) { - // consoleObj.Error = obj.error.stack - // } + if (obj.error) { + consoleObj.Error = obj.error.stack + } for (let fake of fakes) { const count = fake._cyCount @@ -117,6 +117,10 @@ const onInvoke = function (Cypress, obj, args) { return Cypress.log(logProps) } +const onError = (err) => { + return $errUtils.throwErr(err) +} + // create a global sandbox // to be used through all the tests const sandbox = createSandbox() @@ -176,8 +180,19 @@ export default function (Commands, Cypress, cy, state) { const { invoke } = agent agent.invoke = function (func, thisValue, args) { + let error = null let returned = null + // because our spy could potentially fail here + // we need to wrap this in a try / catch + // so we still emit the command that failed + // and the user can easily find the error + try { + returned = invoke.call(this, func, thisValue, args) + } catch (e: any) { + error = e + } + const props = { count, name: type, @@ -186,54 +201,18 @@ export default function (Commands, Cypress, cy, state) { agent, call: agent.lastCall, callCount: agent.callCount, + error, log, } - const invokeLog = onInvoke(Cypress, props, args) - - // return invoke.call(this, func, thisValue, args) - // because our spy could potentially fail here - // we need to wrap this in a try / catch - // so we still emit the command that failed - // and the user can easily find the error - try { - returned = invoke.call(this, func, thisValue, args) - } catch (err: any) { - $errUtils.throwErr(err) - // { - // onFail: () => { - // // emily too do...setting error on this log caused double error rendering - // // for restored errors. want this for console props & what not. - // // might be the reporter need to verify the last child log only shows in-line - // // errors if reported multiple times. - // invokeLog.set({ - // state: 'failed', - // }) - // }, - // }) - } + onInvoke(Cypress, props, args) - if ($utils.isPromiseLike(returned)) { - return returned - .then((val) => { - invokeLog?.end() - console.log('val', val) - - return val - }) - .catch((err) => { - console.log('AGENT HERE', err) - invokeLog?.set({ - state: 'failed', - error: err, - }) - - $errUtils.throwErr(err) - }) + // if an error did exist then we need + // to bubble it up + if (error) { + onError(error) } - invokeLog?.end() - // make sure we return the invoked return value // of the spy return returned 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 From 7a490bae26b021a79c277392de754ed2e1ddf437 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Thu, 27 Oct 2022 11:52:28 -0500 Subject: [PATCH 33/52] and fix. --- packages/driver/src/cy/commands/agents.ts | 3 ++- packages/driver/src/cy/retries.ts | 3 --- packages/reporter/cypress/e2e/commands.cy.ts | 3 ++- packages/reporter/src/commands/command.tsx | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/driver/src/cy/commands/agents.ts b/packages/driver/src/cy/commands/agents.ts index 611e2573a951..bea5cb12ff71 100644 --- a/packages/driver/src/cy/commands/agents.ts +++ b/packages/driver/src/cy/commands/agents.ts @@ -68,7 +68,8 @@ const onInvoke = function (Cypress, obj, args) { const logProps: Record = { name: agentName, message: obj.message, - error: obj.error, + state: obj.error ? 'failed' : 'passed', + // error: obj.error, type: 'parent', end: true, snapshot: !agent._noSnapshot, diff --git a/packages/driver/src/cy/retries.ts b/packages/driver/src/cy/retries.ts index 039fb0d72146..4663bf5f53f0 100644 --- a/packages/driver/src/cy/retries.ts +++ b/packages/driver/src/cy/retries.ts @@ -87,13 +87,10 @@ export const create = (Cypress: ICypress, state: StateFunc, timeout: $Cy['timeou throwErr(retryErr, { onFail: (err) => { - console.log('timed out', err) if (onFail) { err = onFail(err) } - console.log('onFail timed out', err) - finishAssertions(err) }, }) 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.tsx b/packages/reporter/src/commands/command.tsx index a6d1695ce225..dd80e3bb256b 100644 --- a/packages/reporter/src/commands/command.tsx +++ b/packages/reporter/src/commands/command.tsx @@ -407,7 +407,7 @@ class Command extends Component { testId={model.testId} commandId={model.id} // if the err is recovered and the current command is a log group, nest the test error within the group - groupLevel={this.props.groupId ? ++groupLevel : groupLevel} + groupLevel={this.props.groupId && groupLevel === this.props.groupId ? ++groupLevel : groupLevel} /> )} From 3719c919b327c8b16a992f3426479bdf76c497b6 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Tue, 1 Nov 2022 12:22:11 -0500 Subject: [PATCH 34/52] handle multiple command queue errors --- .../cypress/e2e/commands/assertions.cy.js | 12 +- .../cypress/e2e/commands/navigation.cy.js | 24 +- .../e2e/commands/querying/querying.cy.js | 18 +- packages/driver/src/cy/assertions.ts | 9 +- packages/driver/src/cy/commands/asserting.ts | 4 +- packages/driver/src/cy/commands/misc.ts | 4 +- .../driver/src/cy/commands/querying/within.ts | 3 +- .../driver/src/cy/commands/sessions/index.ts | 8 +- packages/driver/src/cy/logGroup.ts | 2 +- packages/driver/src/cypress/command.ts | 32 ++- packages/driver/src/cypress/command_queue.ts | 243 ++++++++++++------ packages/driver/src/cypress/commands.ts | 2 - packages/driver/src/cypress/cy.ts | 40 +-- packages/driver/src/cypress/log.ts | 1 + packages/driver/src/cypress/state.ts | 1 - packages/driver/src/util/queue.ts | 22 +- 16 files changed, 264 insertions(+), 161 deletions(-) diff --git a/packages/driver/cypress/e2e/commands/assertions.cy.js b/packages/driver/cypress/e2e/commands/assertions.cy.js index db54ada0822d..865cbd24cbb7 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 }) @@ -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() @@ -737,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) @@ -828,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 @@ -1185,7 +1185,7 @@ describe('src/cy/commands/assertions', () => { this.logs = [] cy.on('log:added', (attrs, log) => { - this.logs.push(log) + this.logs?.push(log) }) return null @@ -1627,7 +1627,7 @@ describe('src/cy/commands/assertions', () => { } cy.on('log:added', (attrs, log) => { - this.logs.push(log) + this.logs?.push(log) }) return null diff --git a/packages/driver/cypress/e2e/commands/navigation.cy.js b/packages/driver/cypress/e2e/commands/navigation.cy.js index c757f9a47cfb..1b61435ec3bb 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 @@ -2109,7 +2109,7 @@ describe('src/cy/commands/navigation', () => { return null }) - describe('can time out', { retries: 1 }, () => { + describe('can time out', () => { let pageLoadTimeout before(() => { @@ -2123,17 +2123,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 @@ -2157,7 +2155,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 16374054c22b..9fc49815d480 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) } }) @@ -739,7 +739,6 @@ describe('src/cy/commands/querying', () => { this.logs = [] const collectLogs = (attrs, log) => { - console.log('collect log') this.lastLog = log this.logs?.push(log) @@ -881,12 +880,17 @@ describe('src/cy/commands/querying', () => { }) it('fails get command when element is not found and has chained assertions', function (done) { - cy.on('fail', (err) => { - const { lastLog } = this + cy.once('fail', (err) => { + const { logs, lastLog } = this + const getLog = logs[logs.length - 2] 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(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) @@ -1604,7 +1608,7 @@ space this.lastLog = log } - this.logs.push(log) + this.logs?.push(log) }) return null @@ -1720,7 +1724,7 @@ space cy.on('log:added', (attrs, log) => { this.lastLog = log - this.logs.push(log) + this.logs?.push(log) }) return null diff --git a/packages/driver/src/cy/assertions.ts b/packages/driver/src/cy/assertions.ts index 6229d5a38570..a98e2a4b24c9 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[] = [] @@ -333,6 +333,7 @@ export const create = (Cypress: ICypress, cy: $Cy) => { const { onFail, onRetry } = callbacks if (err.retry === false || (!onFail && !onRetry)) { + err.onFail = finishAssertions throw err } @@ -398,12 +399,12 @@ export const create = (Cypress: ICypress, cy: $Cy) => { // and force the assertion to return // this value so it does not get // invoked again - const setSubjectAndSkip = () => { + const setSubjectAndPass = () => { subjects.forEach((subject, i) => { const cmd = cmds[i] cmd.set('subject', subject) - cmd.skip() + cmd.pass() }) return cmds @@ -438,7 +439,7 @@ export const create = (Cypress: ICypress, cy: $Cy) => { .then(() => { restore() - setSubjectAndSkip() + setSubjectAndPass() finishAssertions() diff --git a/packages/driver/src/cy/commands/asserting.ts b/packages/driver/src/cy/commands/asserting.ts index cdb37022ec31..9b98c8ea2e6d 100644 --- a/packages/driver/src/cy/commands/asserting.ts +++ b/packages/driver/src/cy/commands/asserting.ts @@ -95,7 +95,7 @@ export default function (Commands, Cypress, cy, state) { // without going through the assertion we need // to ensure our .should command gets logged logIndex++ - const log = Cypress.log({ + Cypress.log({ name: 'should', type: 'child', message: ([] as any[]).concat(originalChainers, args), @@ -104,7 +104,7 @@ export default function (Commands, Cypress, cy, state) { error: err, }) - return $errUtils.throwErr(err, { onFail: log }) + return $errUtils.throwErr(err) } chainers = chainers.split('.') 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/within.ts b/packages/driver/src/cy/commands/querying/within.ts index 7b894e0f0066..f182beda995e 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,7 +31,6 @@ export default (Commands, Cypress, cy, state) => { fn: (subject) => subject, })) - state('index', restoreCmdIndex) fn.call(cy.state('ctx'), subject) diff --git a/packages/driver/src/cy/commands/sessions/index.ts b/packages/driver/src/cy/commands/sessions/index.ts index 7eb0089ccf63..e653f9bf0a42 100644 --- a/packages/driver/src/cy/commands/sessions/index.ts +++ b/packages/driver/src/cy/commands/sessions/index.ts @@ -361,7 +361,7 @@ export default function (Commands, Cypress, cy) { }) // skip all commands between this command and _commandToRunAfterValidation - for (let i = Cypress.state('index'); i < index; i++) { + for (let i = cy.queue.index; i < index; i++) { const cmd = commands[i] if (!cmd.get('restore-within')) { @@ -374,7 +374,7 @@ export default function (Commands, Cypress, cy) { Cypress.state('withinSubject', withinSubject) // move to _commandToRunAfterValidation's index to ensure failures are handled correctly - queue.state('index', index) + queue.index = index console.log('sessionStatus', step, typeof err) @@ -405,7 +405,9 @@ export default function (Commands, Cypress, cy) { let returnVal // try { - returnVal = existingSession.validate.call(cy.state('ctx')) + cy.then(() => { + returnVal = existingSession.validate.call(cy.state('ctx')) + }) // } catch (e) { // debugger 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/cypress/command.ts b/packages/driver/src/cypress/command.ts index afcd6d0a391f..42c1bcfc2637 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,30 @@ export class $Command { this.set(attrs) } + pass () { + this.state = 'passed' + } + + skip () { + this.state = 'skipped' + } + + fail () { + if (this.get('onFail')) { + console.log('command has onFail') + } + + this.state = 'failed' + } + + recovered () { + this.state = 'recovered' + } + + start () { + this.state = 'pending' + } + set (key, val?) { let obj @@ -35,7 +61,7 @@ export class $Command { finishLogs () { // finish each of the logs we have - return _.invokeMap(this.get('logs'), 'finish') + return _.invokeMap(this.get('logs'), 'finish', this.state) } log (log) { @@ -102,10 +128,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 5d69a5c60c4a..2e415e462da0 100644 --- a/packages/driver/src/cypress/command_queue.ts +++ b/packages/driver/src/cypress/command_queue.ts @@ -64,10 +64,10 @@ const commandRunningFailed = (Cypress, state, err) => { } export class CommandQueue extends Queue<$Command> { + index: number state: StateFunc timeout: $Cy['timeout'] stability: IStability - cleanup: $Cy['cleanup'] fail: $Cy['fail'] isCy: $Cy['isCy'] clearTimeout: ITimeouts['clearTimeout'] @@ -77,21 +77,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.index = 0 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) { @@ -112,6 +113,33 @@ export class CommandQueue extends Queue<$Command> { return _.invokeMap(this.get(), 'get', 'name') } + add (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.queue.length) { + this.state('nestedIndex', (nestedIndex += 1)) + + return this.insert(index, command) + } + + return super.add(command) + } + insert (index: number, command: $Command) { super.insert(index, command) @@ -139,12 +167,23 @@ 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 () { + 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 + this.index = this.length } private runCommand (command: $Command) { @@ -160,7 +199,7 @@ export class CommandQueue extends Queue<$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') }) @@ -187,6 +226,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 @@ -201,6 +241,8 @@ export class CommandQueue extends Queue<$Command> { // back into bluebird else it will create a thenable // which is never resolved if (this.isCy(ret)) { + // console.log('return null') + return null } @@ -259,33 +301,42 @@ export class CommandQueue extends Queue<$Command> { this.setSubjectForChainer(command.get('chainerId'), subject) - return subject - }).catch((err) => { - if (this.state('onCommandFailed')) { - err = this.state('onCommandFailed')(err, this) + console.log('cmd passed', command.get('name')) + command.pass() - this.state('onCommandFailed', null) - } + // }) + // .catch((err) => { + // console.log('command err') + // if (this.state('onCommandFailed')) { + // err = this.state('onCommandFailed')(err, this) - debugErrors('cypress command had an error: %o', err) + // this.state('onCommandFailed', null) + // } - // since this failed this means that a specific command failed - // and we should highlight it in red or insert a new command - // @ts-ignore - if (_.isObject(err) && !err.name) { - // @ts-ignore - err.name = 'CypressError' - } + // debugErrors('cypress command had an error: %o', err) - commandRunningFailed(Cypress, this.state, err) + // // since this failed this means that a specific command failed + // // and we should highlight it in red or insert a new command + // // @ts-ignore + // if (_.isObject(err) && !err.name) { + // // @ts-ignore + // err.name = 'CypressError' + // } - if (err.isRecovered) { - return // let the queue move on to the next command - } + // commandRunningFailed(Cypress, this.state, err) - throw err - }) - .then(() => { + // if (err.isRecovered) { + // command.recovered() + + // return // let the queue move on to the next command + // } + + // command.failed() + + // throw err + // }) + // .then(() => { + // console.log('finish logs') // end / snapshot our logs if they need it command.finishLogs() @@ -296,56 +347,57 @@ export class CommandQueue extends Queue<$Command> { // we're finished with the current command so set it back to null current: null, }) + + return subject }) } - // 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 + const runQueue = () => { + const command = this.at(this.index) + + if (!command) { + Cypress.action('cy:command:queue:before:end') + + // we need to wait after all commands have + // finished running if the application under + // test is no longer stable because we cannot + // 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 + }) } // start at 0 index if one is not already set - let index = this.state('index') || this.state('index', 0) - - const command = this.at(index) + // console.log('next', this.index, command, cmd) // if the command should be skipped, just bail and increment index - if (command && command.get('skip')) { + if (command && (command.state === 'passed' || command.get('skip'))) { // 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) - - return next() - } - - // if we're at the very end - if (!command) { - // trigger queue is almost finished - Cypress.action('cy:command:queue:before:end') + if (command.get('skip')) { + Cypress.action('cy:skipped:command:end', command) + } - // we need to wait after all commands have - // finished running if the application under - // test is no longer stable because we cannot - // move onto the next test until its finished - return this.stability.whenStable(() => { - Cypress.action('cy:command:queue:end') + this.index += 1 - return null - }) + return runQueueable() } // store the previous timeout @@ -370,7 +422,6 @@ 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) @@ -381,24 +432,27 @@ export class CommandQueue extends Queue<$Command> { // 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') + this.index += 1 + + const pauseFn = this.state('onPaused') - if (fn) { + if (pauseFn) { return new Bluebird((resolve) => { - return fn(resolve) - }).then(next) + return pauseFn(resolve) + .then(() => { + return runQueueable() + }) + }) } - return next() + return runQueueable() }) } - const onError = (err: Error | string) => { + const onQueueError = (err: Error | string) => { // If the runnable was marked as pending, this test was skipped // go ahead and just return const runnable = this.state('runnable') @@ -407,17 +461,56 @@ export class CommandQueue extends Queue<$Command> { return } + const current = this.state('current') + + if (this.state('onCommandFailed')) { + err = this.state('onCommandFailed')(err, this) + + this.state('onCommandFailed', null) + } + + debugErrors('cypress command had an error: %o', err) + + // since this failed this means that a specific command failed + // and we should highlight it in red or insert a new command + // @ts-ignore + if (_.isObject(err) && !err.name) { + // @ts-ignore + err.name = 'CypressError' + } + + commandRunningFailed(Cypress, this.state, err) + + if (err.isRecovered) { + current?.recovered() + + return // let the queue move on to the next command + } + + if (current?.state === 'queued') { + current.skip() + } else if (current?.state === 'pending') { + current.fail() + } + return this.fail(err) } + if (this.stopped) { + this.cleanup() + + return Promise.resolve() + } + const { promise, reject, cancel } = super.run({ - onRun: next, - onError, - onFinish: this.cleanup, + onRun: runQueue, + onError: onQueueError, + onFinish: this.run, }) this.state('promise', promise) this.state('reject', reject) + this.state('cancel', () => { cancel() 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 4cf26b98b6fb..d482e47a572a 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) @@ -693,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 }) { @@ -1098,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 @@ -1366,7 +1331,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 @@ -1382,6 +1347,5 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert this.state('cancel')() } - return this.cleanup() } } diff --git a/packages/driver/src/cypress/log.ts b/packages/driver/src/cypress/log.ts index 81e50bd5a709..2c84732664f7 100644 --- a/packages/driver/src/cypress/log.ts +++ b/packages/driver/src/cypress/log.ts @@ -406,6 +406,7 @@ export class Log { this.set({ ended: true, error: err, + _error: undefined, state: 'failed', }) diff --git a/packages/driver/src/cypress/state.ts b/packages/driver/src/cypress/state.ts index d417a72d39c2..69cd0a64fbaf 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 diff --git a/packages/driver/src/util/queue.ts b/packages/driver/src/util/queue.ts index 21fa8aacc9e2..8811167f202e 100644 --- a/packages/driver/src/util/queue.ts +++ b/packages/driver/src/util/queue.ts @@ -9,6 +9,7 @@ interface QueueRunProps { export class Queue { private queueables: T[] = [] private _stopped = false + index: number = 0 constructor (queueables: T[] = []) { this.queueables = queueables @@ -19,6 +20,12 @@ export class Queue { } add (queueable: T) { + if (this.length) { + const prev = this.at(this.length - 1) + + queueable.set('prev', prev) + } + this.queueables.push(queueable) } @@ -42,9 +49,12 @@ export class Queue { reset () { this._stopped = false + this.index = 0 + this.queueables.length = 0 } clear () { + this.index = 0 this.queueables.length = 0 } @@ -52,6 +62,16 @@ export class Queue { this._stopped = true } + hasNext () { + return this.index < this.length + } + + next () { + const next = this.at(this.index) + + return next + } + run ({ onRun, onError, onFinish }: QueueRunProps) { let inner let rejectOuterAndCancelInner @@ -81,7 +101,7 @@ export class Queue { } }) .catch(onError) - .finally(onFinish) + .then(onFinish) const cancel = () => { promise.cancel() From 3789d5d5070381e25f84f72e98bdc3286a507f78 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Tue, 1 Nov 2022 12:36:42 -0500 Subject: [PATCH 35/52] fix --- .../driver/src/cy/commands/sessions/index.ts | 12 ++++++------ packages/driver/src/cypress/command_queue.ts | 16 ++++++++-------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/driver/src/cy/commands/sessions/index.ts b/packages/driver/src/cy/commands/sessions/index.ts index e653f9bf0a42..acd7657f5f01 100644 --- a/packages/driver/src/cy/commands/sessions/index.ts +++ b/packages/driver/src/cy/commands/sessions/index.ts @@ -215,7 +215,7 @@ export default function (Commands, Cypress, cy) { Cypress.state('current').set('name', 'setup session') // 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) => { + cy.state('onQueueFailed', (err) => { setupLogGroup.set({ state: 'failed', consoleProps: { @@ -237,7 +237,7 @@ export default function (Commands, Cypress, cy) { }) .then(async () => { Cypress.state('current').set('name', 'save session') - cy.state('onCommandFailed', null) + cy.state('onQueueFailed', null) await navigateAboutBlank() const data = await sessions.getCurrentSessionData() @@ -305,7 +305,7 @@ export default function (Commands, Cypress, cy) { let _commandToRunAfterValidation const enhanceErr = (err) => { - Cypress.state('onCommandFailed', null) + Cypress.state('onQueueFailed', null) console.log('[onFail] step', step, err) // show validation error and allow sessions workflow to recreate the session @@ -347,8 +347,8 @@ export default function (Commands, Cypress, cy) { return err } - cy.state('onCommandFailed', (err: Error | string, queue): boolean => { - console.log('onCommandFailed', step, err) + cy.state('onQueueFailed', (err: Error | string, queue): boolean => { + console.log('onQueueFailed', step, err) if (step === 'restore') { const commands = queue.get() @@ -415,7 +415,7 @@ export default function (Commands, Cypress, cy) { // } _commandToRunAfterValidation = cy.then(async () => { Cypress.state('current').set('name', '_commandToRunAfterValidation') - Cypress.state('onCommandFailed', null) + Cypress.state('onQueueFailed', null) if (caughtCommandErr) { return !isValidSession diff --git a/packages/driver/src/cypress/command_queue.ts b/packages/driver/src/cypress/command_queue.ts index 2e415e462da0..2d80e94cd032 100644 --- a/packages/driver/src/cypress/command_queue.ts +++ b/packages/driver/src/cypress/command_queue.ts @@ -307,10 +307,10 @@ export class CommandQueue extends Queue<$Command> { // }) // .catch((err) => { // console.log('command err') - // if (this.state('onCommandFailed')) { - // err = this.state('onCommandFailed')(err, this) + // if (this.state('onQueueFailed')) { + // err = this.state('onQueueFailed')(err, this) - // this.state('onCommandFailed', null) + // this.state('onQueueFailed', null) // } // debugErrors('cypress command had an error: %o', err) @@ -353,7 +353,7 @@ export class CommandQueue extends Queue<$Command> { } run () { - const runQueue = () => { + const runQueueable = () => { const command = this.at(this.index) if (!command) { @@ -463,10 +463,10 @@ export class CommandQueue extends Queue<$Command> { const current = this.state('current') - if (this.state('onCommandFailed')) { - err = this.state('onCommandFailed')(err, this) + if (this.state('onQueueFailed')) { + err = this.state('onQueueFailed')(err, this) - this.state('onCommandFailed', null) + this.state('onQueueFailed', null) } debugErrors('cypress command had an error: %o', err) @@ -503,7 +503,7 @@ export class CommandQueue extends Queue<$Command> { } const { promise, reject, cancel } = super.run({ - onRun: runQueue, + onRun: runQueueable, onError: onQueueError, onFinish: this.run, }) From 0cdadc810b87dfb3c6fc8b2d1137e3e7b03aab0a Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Tue, 1 Nov 2022 15:03:02 -0500 Subject: [PATCH 36/52] fix tests --- packages/driver/src/cy/assertions.ts | 16 +- .../driver/src/cy/commands/actions/click.ts | 3 - packages/driver/src/cy/commands/agents.ts | 1 - packages/driver/src/cy/commands/asserting.ts | 4 +- packages/driver/src/cy/commands/debugging.ts | 8 +- .../driver/src/cy/commands/querying/within.ts | 1 - .../driver/src/cy/commands/sessions/utils.ts | 9 - packages/driver/src/cy/retries.ts | 2 +- packages/driver/src/cypress/command.ts | 6 +- packages/driver/src/cypress/command_queue.ts | 86 ++--- packages/driver/src/cypress/cy.ts | 28 +- packages/driver/src/cypress/runner.ts | 8 - packages/driver/src/cypress/state.ts | 2 +- packages/driver/src/util/queue.ts | 14 - .../cypress/e2e/session/error-in-progress.js | 342 ------------------ .../cypress/e2e/session/session_errors.cy.js | 13 - 16 files changed, 39 insertions(+), 504 deletions(-) delete mode 100644 system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/error-in-progress.js delete mode 100644 system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/session_errors.cy.js diff --git a/packages/driver/src/cy/assertions.ts b/packages/driver/src/cy/assertions.ts index a98e2a4b24c9..66c14aafe1c5 100644 --- a/packages/driver/src/cy/assertions.ts +++ b/packages/driver/src/cy/assertions.ts @@ -399,12 +399,12 @@ export const create = (Cypress: ICypress, cy: $Cy) => { // and force the assertion to return // this value so it does not get // invoked again - const setSubjectAndPass = () => { + const setSubjectAndSkip = () => { subjects.forEach((subject, i) => { const cmd = cmds[i] cmd.set('subject', subject) - cmd.pass() + cmd.skip() // technically this passed because it already ran }) return cmds @@ -439,7 +439,7 @@ export const create = (Cypress: ICypress, cy: $Cy) => { .then(() => { restore() - setSubjectAndPass() + setSubjectAndSkip() finishAssertions() @@ -450,15 +450,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: finishAssertions }) - } 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 d91599057d93..b3f1d1692080 100644 --- a/packages/driver/src/cy/commands/actions/click.ts +++ b/packages/driver/src/cy/commands/actions/click.ts @@ -220,13 +220,10 @@ export default (Commands, Cypress, cy: $Cy, state, config) => { }, }) .catch((err) => { - console.log('error!') - // if we give up on waiting for actionability then // lets throw this error and log the command return $errUtils.throwErr(err, { onFail (err) { - console.log('on click fail') 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 bea5cb12ff71..7bef1eebe782 100644 --- a/packages/driver/src/cy/commands/agents.ts +++ b/packages/driver/src/cy/commands/agents.ts @@ -69,7 +69,6 @@ const onInvoke = function (Cypress, obj, args) { name: agentName, message: obj.message, state: obj.error ? 'failed' : 'passed', - // error: obj.error, type: 'parent', end: true, snapshot: !agent._noSnapshot, diff --git a/packages/driver/src/cy/commands/asserting.ts b/packages/driver/src/cy/commands/asserting.ts index 9b98c8ea2e6d..cdb37022ec31 100644 --- a/packages/driver/src/cy/commands/asserting.ts +++ b/packages/driver/src/cy/commands/asserting.ts @@ -95,7 +95,7 @@ export default function (Commands, Cypress, cy, state) { // without going through the assertion we need // to ensure our .should command gets logged logIndex++ - Cypress.log({ + const log = Cypress.log({ name: 'should', type: 'child', message: ([] as any[]).concat(originalChainers, args), @@ -104,7 +104,7 @@ export default function (Commands, Cypress, cy, state) { error: err, }) - return $errUtils.throwErr(err) + return $errUtils.throwErr(err, { onFail: log }) } chainers = chainers.split('.') diff --git a/packages/driver/src/cy/commands/debugging.ts b/packages/driver/src/cy/commands/debugging.ts index 8ac152cf0604..e195f572003c 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 + 1) } 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/querying/within.ts b/packages/driver/src/cy/commands/querying/within.ts index f182beda995e..8ac280a3dd96 100644 --- a/packages/driver/src/cy/commands/querying/within.ts +++ b/packages/driver/src/cy/commands/querying/within.ts @@ -31,7 +31,6 @@ export default (Commands, Cypress, cy, state) => { fn: (subject) => subject, })) - fn.call(cy.state('ctx'), subject) const cleanup = () => { diff --git a/packages/driver/src/cy/commands/sessions/utils.ts b/packages/driver/src/cy/commands/sessions/utils.ts index 2adf8d47c0e3..83024acd770d 100644 --- a/packages/driver/src/cy/commands/sessions/utils.ts +++ b/packages/driver/src/cy/commands/sessions/utils.ts @@ -108,15 +108,6 @@ const getConsoleProps = (session: SessionData) => { name: `${domain} data:`, expand: true, label: false, - // items: _.compact([ - // // !val.cookies && `There are no cookies associated to ${domain}.`, - // !val.cookies && `🍪 Cookies - (0)`, - // // !val.localStorage && `There are no local storage items associated to ${domain}.`, - // !val.localStorage && `📁 Local Storage - (0)`, - // // !val.sessionStorage && `There are no session storage items associated to ${domain}.`, - // !val.sessionStorage && `📁 Session Storage - (0)`, - // ]), - // items: [] groups: _.compact([ val.cookies && { name: `🍪 Cookies - (${val.cookies.length})`, diff --git a/packages/driver/src/cy/retries.ts b/packages/driver/src/cy/retries.ts index 4663bf5f53f0..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: () => void) => ({ +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 diff --git a/packages/driver/src/cypress/command.ts b/packages/driver/src/cypress/command.ts index 42c1bcfc2637..f0b7c14929bb 100644 --- a/packages/driver/src/cypress/command.ts +++ b/packages/driver/src/cypress/command.ts @@ -29,10 +29,6 @@ export class $Command { } fail () { - if (this.get('onFail')) { - console.log('command has onFail') - } - this.state = 'failed' } @@ -61,7 +57,7 @@ export class $Command { finishLogs () { // finish each of the logs we have - return _.invokeMap(this.get('logs'), 'finish', this.state) + return _.invokeMap(this.get('logs'), 'finish') } log (log) { diff --git a/packages/driver/src/cypress/command_queue.ts b/packages/driver/src/cypress/command_queue.ts index 2d80e94cd032..d7e2b0313d23 100644 --- a/packages/driver/src/cypress/command_queue.ts +++ b/packages/driver/src/cypress/command_queue.ts @@ -64,7 +64,6 @@ const commandRunningFailed = (Cypress, state, err) => { } export class CommandQueue extends Queue<$Command> { - index: number state: StateFunc timeout: $Cy['timeout'] stability: IStability @@ -83,7 +82,7 @@ export class CommandQueue extends Queue<$Command> { setSubjectForChainer: $Cy['setSubjectForChainer'], ) { super() - this.index = 0 + this.state = state this.timeout = timeout this.stability = stability @@ -113,7 +112,7 @@ export class CommandQueue extends Queue<$Command> { return _.invokeMap(this.get(), 'get', 'name') } - add (command: $Command) { + 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 @@ -131,13 +130,16 @@ export class CommandQueue extends Queue<$Command> { // 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) { + if (_.isNumber(nestedIndex) && nestedIndex < this.length) { this.state('nestedIndex', (nestedIndex += 1)) - - return this.insert(index, command) } - return super.add(command) + // 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) { @@ -241,8 +243,6 @@ export class CommandQueue extends Queue<$Command> { // back into bluebird else it will create a thenable // which is never resolved if (this.isCy(ret)) { - // console.log('return null') - return null } @@ -276,8 +276,7 @@ export class CommandQueue extends Queue<$Command> { } return ret - }) - .then((subject) => { + }).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 @@ -298,48 +297,13 @@ export class CommandQueue extends Queue<$Command> { } command.set({ subject }) - - this.setSubjectForChainer(command.get('chainerId'), subject) - - console.log('cmd passed', command.get('name')) command.pass() - // }) - // .catch((err) => { - // console.log('command err') - // if (this.state('onQueueFailed')) { - // err = this.state('onQueueFailed')(err, this) - - // this.state('onQueueFailed', null) - // } - - // debugErrors('cypress command had an error: %o', err) - - // // since this failed this means that a specific command failed - // // and we should highlight it in red or insert a new command - // // @ts-ignore - // if (_.isObject(err) && !err.name) { - // // @ts-ignore - // err.name = 'CypressError' - // } - - // commandRunningFailed(Cypress, this.state, err) - - // if (err.isRecovered) { - // command.recovered() - - // return // let the queue move on to the next command - // } - - // command.failed() - - // throw err - // }) - // .then(() => { - // console.log('finish logs') // end / snapshot our logs if they need it command.finishLogs() + this.setSubjectForChainer(command.get('chainerId'), subject) + this.state({ commandIntermediateValue: undefined, // reset the nestedIndex back to null @@ -356,7 +320,9 @@ export class CommandQueue extends Queue<$Command> { const runQueueable = () => { const command = this.at(this.index) + // if we're at the very end if (!command) { + // trigger queue is almost finished Cypress.action('cy:command:queue:before:end') // we need to wait after all commands have @@ -377,11 +343,8 @@ export class CommandQueue extends Queue<$Command> { }) } - // start at 0 index if one is not already set - // console.log('next', this.index, command, cmd) - // if the command should be skipped, just bail and increment index - if (command && (command.state === 'passed' || command.get('skip'))) { + if (command && (command.state === 'passed' || command.state === 'skipped')) { // must set prev + next since other // operations depend on this state being correct command.set({ @@ -391,10 +354,11 @@ export class CommandQueue extends Queue<$Command> { this.setSubjectForChainer(command.get('chainerId'), command.get('subject')) - if (command.get('skip')) { + if (command.state === 'skipped') { Cypress.action('cy:skipped:command:end', command) } + // move on to the next queueable this.index += 1 return runQueueable() @@ -427,27 +391,25 @@ export class CommandQueue extends Queue<$Command> { 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 - Cypress.action('cy:command:end', command) - this.index += 1 - const pauseFn = this.state('onPaused') if (pauseFn) { return new Bluebird((resolve) => { return pauseFn(resolve) .then(() => { + // move on to the next queueable + this.index += 1 + return runQueueable() }) }) } + // move on to the next queueable + this.index += 1 + return runQueueable() }) } @@ -493,6 +455,8 @@ export class CommandQueue extends Queue<$Command> { current.fail() } + this.cleanup() + return this.fail(err) } diff --git a/packages/driver/src/cypress/cy.ts b/packages/driver/src/cypress/cy.ts index d482e47a572a..278fed1c55b0 100644 --- a/packages/driver/src/cypress/cy.ts +++ b/packages/driver/src/cypress/cy.ts @@ -1150,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) } diff --git a/packages/driver/src/cypress/runner.ts b/packages/driver/src/cypress/runner.ts index cb545a8f3b1b..1e06b9c11410 100644 --- a/packages/driver/src/cypress/runner.ts +++ b/packages/driver/src/cypress/runner.ts @@ -1514,14 +1514,6 @@ export default { // original err next(err) - // return null here to signal to bluebird - // that we did not forget to return a promise - // because mocha internally does not return - // the test.run(fn) - return null - }).catch((err) => { - next(err) - // return null here to signal to bluebird // that we did not forget to return a promise // because mocha internally does not return diff --git a/packages/driver/src/cypress/state.ts b/packages/driver/src/cypress/state.ts index 69cd0a64fbaf..ef93964d30e0 100644 --- a/packages/driver/src/cypress/state.ts +++ b/packages/driver/src/cypress/state.ts @@ -51,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: any, queue: any) => boolean): (err: any, queue: any) => boolean (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/util/queue.ts b/packages/driver/src/util/queue.ts index 8811167f202e..6482ba80a129 100644 --- a/packages/driver/src/util/queue.ts +++ b/packages/driver/src/util/queue.ts @@ -20,12 +20,6 @@ export class Queue { } add (queueable: T) { - if (this.length) { - const prev = this.at(this.length - 1) - - queueable.set('prev', prev) - } - this.queueables.push(queueable) } @@ -49,8 +43,6 @@ export class Queue { reset () { this._stopped = false - this.index = 0 - this.queueables.length = 0 } clear () { @@ -66,12 +58,6 @@ export class Queue { return this.index < this.length } - next () { - const next = this.at(this.index) - - return next - } - run ({ onRun, onError, onFinish }: QueueRunProps) { let inner let rejectOuterAndCancelInner diff --git a/system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/error-in-progress.js b/system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/error-in-progress.js deleted file mode 100644 index 0b69296166db..000000000000 --- a/system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/error-in-progress.js +++ /dev/null @@ -1,342 +0,0 @@ -/** - * Used in cy-in-cy tests in @packages/app. - */ - -before(async () => { - Cypress.state('activeSessions', {}) - await Cypress.session.clearAllSavedSessions() -}) - -// afterEach(() => { -// console.log(Cypress.state('runnable')) -// expect(Cypress.state('runnable').ctx.currentTest.state).to.eq('failed') -// }) - -describe('create session', () => { - describe('seems correct', () => { - it('setup has failing command', () => { - cy.session('session_1', () => { - cy.get('does_not_exist', { timeout: 1 }) - }) - }) - - it.only('created command validate threw error', () => { - cy.session('session_4', () => { - cy.get('div') - }, { - async validate () { - cy.get('div') - .within(() => { - throw new Error('Something went wrong!') - }) - }, - }) - }) - - it('created command reject false', () => { - cy.session('session_4', () => { - cy.get('div') - }, { - async validate () { - return new Promise(async (resolve, reject) => { - // Cypress.log('getCurrentSessionData') // throws uncaught exception - const { cookies } = await Cypress.session.getCurrentSessionData() - - if (cookies.length === 0) { // this is always zero! we aren't setting any :D - reject(false) - } - }) - }, - }) - }) - - it('recreated reject false', function () { - let count = 0 - const validate = () => { - return new Promise(async (resolve, reject) => { - // Cypress.log('getCurrentSessionData') // throws uncaught exception - const { cookies } = await Cypress.session.getCurrentSessionData() - - if (count === 0) { - resolve(true) - } - - if (count > 0 && cookies.length === 0) { // this is always zero! we aren't setting any :D - reject(false) - } - }) - } - - cy.session('session_44', function () { - cy.get('div').as('hello') - }, { validate }) - - cy.then(() => { - console.log('ABOUT TO CREATE NEW SESSION') - console.log('') - count = 1 - }) - - cy.session('session_44', function () { - cy.get('div').as('hello') - }, { validate }) - }) - - // FIX ME!!!! - it('created command validate has failing command', function () { - cy.log('hi') - cy.session('session_5', function () { - cy.get('div').as('hello') - }, { - validate () { - cy.get('does_not_exist', { timeout: 1 }) - // cy.get('does_not_exist_2', { timeout: 1 }) - }, - }) - }) - - // FIX ME!!!! - it('created command validate has failing command', function () { - let count = 0 - - cy.session('session_55', function () { - cy.get('div').as('hello') - }, { - validate () { - if (count > 0) { - count++ - cy.get('does_not_exist', { timeout: 1 }) - // cy.get('does_not_exist_2', { timeout: 1 }) - } - }, - }) - - cy.then(() => { - console.log('ABOUT TO CREATE NEW SESSION') - console.log('') - count = 1 - }) - - cy.session('session_55', function () { - cy.get('div').as('hello') - }, { - validate () { - if (count > 0) { - count++ - cy.get('does_not_exist', { timeout: 1 }) - // cy.get('does_not_exist_2', { timeout: 1 }) - } - }, - }) - }) - - // correct - it('created command validate has .then yield false', () => { - cy.session('session_6', () => { - cy.get('div') - }, { - validate () { - cy.then(() => { - return false - }) - }, - }) - }) - - // correct - it('recreated command validate has .then yield false ', () => { - let yieldTF = true - - cy.session('session_66', () => { - cy.get('div') - }, { - validate () { - cy.then(() => { - return yieldTF - }) - }, - }) - - cy.then(() => { - yieldTF = false - }) - - cy.session('session_66', () => { - cy.get('div') - }, { - validate () { - cy.then(() => { - return yieldTF - }) - }, - }) - }) - - // correct - it('created command validate has .wrap yield false', () => { - cy.session('session_7', () => { - cy.get('div') - }, { - validate () { - return cy.wrap(false) - }, - }) - }) - - // correct - it('recreated command validate has .wrap yield false ', () => { - let yieldTF = true - - cy.session('session_77', () => { - cy.get('div') - }, { - validate () { - cy.then(() => { - return cy.wrap(yieldTF) - }) - }, - }) - - cy.then(() => { - yieldTF = false - }) - - cy.session('session_77', () => { - cy.get('div') - }, { - validate () { - cy.then(() => { - return cy.wrap(yieldTF) - }) - }, - }) - }) - - it('created command validate has .then with log before yield false ', () => { - cy.session('session_8', () => { - cy.get('div') - }, { - validate () { - cy.window().then((win) => { - console.log(win) - - return false - }) - }, - }) - }) - }) - - // unhappy with stack trace - // ISSUE: missing stack trace (no cy commands to reference) - // FIX BY: could (should) fallback to cy.session stack trace but doesn't feel the best - // FIXED BY DROPPING LAST COMMAND: also saying last command yielded false when it resolved false but maybe yielded false is fine - it('created command resolves false', () => { - cy.session('session_2', () => { - cy.get('div') - }, { - async validate () { - return new Promise(async (resolve, reject) => { - // Cypress.log('getCurrentSessionData') // throws uncaught exception - // Cypress.log({ - // name: 'getCurrentSessionData', - // type: 'system', - // state: 'passed', - // }) - const { cookies } = await Cypress.session.getCurrentSessionData() - - if (cookies.length === 0) { // this is always zero! we aren't setting any :D - resolve(false) - } - }) - }, - }) - }) - - // correct - it('created command resolves false', () => { - let yeildTF = true - - cy.session('session_22', () => { - cy.get('div') - }, { - async validate () { - return new Promise(async (resolve, reject) => { - const { cookies } = await Cypress.session.getCurrentSessionData() - - if (cookies.length === 0) { // this is always zero! we aren't setting any :D - resolve(yeildTF) - } - }) - }, - }) - - cy.then(() => { - yeildTF = false - }) - - cy.session('session_22', () => { - cy.get('div') - }, { - async validate () { - return new Promise(async (resolve, reject) => { - const { cookies } = await Cypress.session.getCurrentSessionData() - - if (cookies.length === 0) { // this is always zero! we aren't setting any :D - resolve(yeildTF) - } - }) - }, - }) - }) - - // missing stack trace (no cy commands) - // unhappy with missing stack trace - // duplicate logs to capture & also fail session - // could just end curr command without failing the rest - it('created command resolves false with log between', () => { - cy.session('session_3', () => { - cy.get('div') - cy.window().then((win) => { - // win.localStorage.setItem('hi', 'there') - }) - }, { - async validate () { - // Cypress.log({ - // name: 'getCurrentSessionData', - // message: '', - // type: 'system', - // state: 'passed', - // }) - - return new Promise(async (resolve, reject) => { - Cypress.log('getCurrentSessionData') // throws uncaught exception - - const { cookies } = await Cypress.session.getCurrentSessionData() - - if (cookies.length === 0) { // this is always zero! we aren't setting any :D - resolve(false) - } - }) - }, - }) - }) - - it.skip('ref', () => { - cy.then(() => { - return new Promise(async (resolve, reject) => { - Cypress.log('getCurrentSessionData') // throws uncaught exception - - const { cookies } = await Cypress.session.getCurrentSessionData() - - if (cookies.length === 0) { // this is always zero! we aren't setting any :D - resolve(false) - } - }) - }) - }) -}) - -// it('for reference', () => { -// cy.get('does_not_exist', { timeout: 1 }) -// }) diff --git a/system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/session_errors.cy.js b/system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/session_errors.cy.js deleted file mode 100644 index b03e4f47df4b..000000000000 --- a/system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/session_errors.cy.js +++ /dev/null @@ -1,13 +0,0 @@ -it('setup has failing command', () => { - cy.session('session_1', () => { - cy.get('does_not_exist', { timeout: 500 }) - }) -}) - -it('validate has failing command', () => { - cy.session('session_1', () => { - cy.log('do setup') - }, () => { - cy.get('does_not_exist', { timeout: 500 }) - }) -}) From 7a3ece9ff608a562022904b512559ba5b6c27cb1 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Tue, 1 Nov 2022 17:38:17 -0500 Subject: [PATCH 37/52] fix tests & some type errors --- .../app/cypress/e2e/runner/sessions.ui.cy.ts | 2 + packages/app/src/runner/logger.ts | 6 +- .../cypress/e2e/cypress/command_queue.cy.ts | 3 +- packages/driver/src/cy/assertions.ts | 9 +- packages/driver/src/cy/commands/debugging.ts | 2 +- .../driver/src/cy/commands/sessions/index.ts | 53 ++------- packages/driver/src/cypress/command_queue.ts | 109 +++++++++--------- packages/driver/src/cypress/cy.ts | 3 +- packages/driver/src/cypress/log.ts | 5 +- packages/driver/src/cypress/runner.ts | 12 +- packages/driver/src/cypress/utils.ts | 1 + packages/driver/src/util/queue.ts | 2 +- packages/driver/types/cy/logGroup.d.ts | 2 + .../reporter/src/sessions/sessions-model.ts | 2 +- 14 files changed, 91 insertions(+), 120 deletions(-) diff --git a/packages/app/cypress/e2e/runner/sessions.ui.cy.ts b/packages/app/cypress/e2e/runner/sessions.ui.cy.ts index 88a43332b626..800b19b7a455 100644 --- a/packages/app/cypress/e2e/runner/sessions.ui.cy.ts +++ b/packages/app/cypress/e2e/runner/sessions.ui.cy.ts @@ -493,6 +493,7 @@ describe('runner/cypress sessions.ui.spec', { failCount: 0, setup () { cy.window().then((win) => { + // @ts-ignore return win.CYPRESS_TEST_DATA = { restoreSessionWithValidationFailure: true, successfullyRecreatedSession: true, @@ -562,6 +563,7 @@ describe('runner/cypress sessions.ui.spec', { failCount: 7, setup () { cy.window().then((win) => { + // @ts-ignore return win.CYPRESS_TEST_DATA = { restoreSessionWithValidationFailure: true, successfullyRecreatedSession: false, diff --git a/packages/app/src/runner/logger.ts b/packages/app/src/runner/logger.ts index 24240b5b7fcc..9b08b26f4301 100644 --- a/packages/app/src/runner/logger.ts +++ b/packages/app/src/runner/logger.ts @@ -3,8 +3,8 @@ import _ from 'lodash' interface Table { name: string - data?: object - columns?: any + data: object + columns: any } interface Group { @@ -127,7 +127,7 @@ export const logger = { }, _logTables (consoleProps: any) { - const logTable = ({ name, data, columns }) => { + const logTable = ({ name, data, columns }: Table) => { let tableData = data if (Cypress.isBrowser('webkit')) { 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 66c14aafe1c5..ad004f12cd18 100644 --- a/packages/driver/src/cy/assertions.ts +++ b/packages/driver/src/cy/assertions.ts @@ -222,7 +222,7 @@ export const create = (Cypress: ICypress, cy: $Cy) => { return null } - const finishAssertions = (err) => { + const finishAssertions = (err?: Error) => { const logs = cy.state('current').get('logs') let hasLoggedError = false @@ -233,16 +233,13 @@ export const create = (Cypress: ICypress, cy: $Cy) => { log.snapshot() } - if (err && !hasLoggedError) { + // @ts-ignore + if (err && (!hasLoggedError || (err.issuesCommunicatingOrFinding && index === logs.length - 1))) { hasLoggedError = true return log.error(err) } - if (err && err.issuesCommunicatingOrFinding && index === logs.length - 1) { - return log.error(err) - } - return log.end() } }) diff --git a/packages/driver/src/cy/commands/debugging.ts b/packages/driver/src/cy/commands/debugging.ts index e195f572003c..f4e86c7217ba 100644 --- a/packages/driver/src/cy/commands/debugging.ts +++ b/packages/driver/src/cy/commands/debugging.ts @@ -30,7 +30,7 @@ const getNextQueuedCommand = (queue) => { return cmd } - return search(queue.index + 1) + return search(queue.index) } interface InternalPauseOptions extends Partial { diff --git a/packages/driver/src/cy/commands/sessions/index.ts b/packages/driver/src/cy/commands/sessions/index.ts index acd7657f5f01..073b8834d219 100644 --- a/packages/driver/src/cy/commands/sessions/index.ts +++ b/packages/driver/src/cy/commands/sessions/index.ts @@ -3,7 +3,7 @@ import stringifyStable from 'json-stable-stringify' import $utils from '../../../cypress/utils' import $errUtils from '../../../cypress/error_utils' -import $stackUtils from '../../../cypress/stack_utils' +// import $stackUtils from '../../../cypress/stack_utils' import logGroup from '../../logGroup' import SessionsManager from './manager' import { @@ -212,7 +212,6 @@ export default function (Commands, Cypress, cy) { }, (setupLogGroup) => { return cy.then(async () => { - Cypress.state('current').set('name', 'setup session') // Catch when a cypress command fails in the setup function to correctly update log status // before failing command and ending command queue. cy.state('onQueueFailed', (err) => { @@ -232,11 +231,8 @@ export default function (Commands, Cypress, cy) { }) return existingSession.setup() - - // return existingSession.setup.call(cy.state('ctx')) }) .then(async () => { - Cypress.state('current').set('name', 'save session') cy.state('onQueueFailed', null) await navigateAboutBlank() const data = await sessions.getCurrentSessionData() @@ -298,28 +294,27 @@ export default function (Commands, Cypress, cy) { }, }, (validateLog) => { return cy.then(async () => { - Cypress.state('current').set('name', 'create validate session commands') - const isValidSession = true let caughtCommandErr = false let _commandToRunAfterValidation const enhanceErr = (err) => { Cypress.state('onQueueFailed', null) - console.log('[onFail] step', step, err) + if (_.isString(err)) { + err = new Error(err) + } // show validation error and allow sessions workflow to recreate the session 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 validateLog.set({ state: 'failed', - consoleProps: (args) => { - return { - Error: err.stack, - } + consoleProps: { + Error: err.stack, }, // explicitly set via .set() so we don't end the log group early ...(!caughtCommandErr && { error: err }), @@ -337,19 +332,14 @@ export default function (Commands, Cypress, cy) { } }, snapshot: true, - // error: err, }) $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) - console.log('return err', err) - return err } cy.state('onQueueFailed', (err: Error | string, queue): boolean => { - console.log('onQueueFailed', step, err) - if (step === 'restore') { const commands = queue.get() // determine command queue index of _commandToRunAfterValidation's index @@ -376,21 +366,6 @@ export default function (Commands, Cypress, cy) { // move to _commandToRunAfterValidation's index to ensure failures are handled correctly queue.index = index - console.log('sessionStatus', step, typeof err) - - // // 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) - // } - - // if (!(err instanceof Error) && err === false) { - // console.log('ITS FALSE') - // // 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)}` }) - // } - err.isRecovered = true caughtCommandErr = true @@ -399,22 +374,13 @@ export default function (Commands, Cypress, cy) { return enhanceErr(err) }) - // console.log('caught err when executing') - - // } let returnVal - // try { cy.then(() => { returnVal = existingSession.validate.call(cy.state('ctx')) }) - // } catch (e) { - // debugger - // return onFail(e) - // } _commandToRunAfterValidation = cy.then(async () => { - Cypress.state('current').set('name', '_commandToRunAfterValidation') Cypress.state('onQueueFailed', null) if (caughtCommandErr) { @@ -485,7 +451,6 @@ export default function (Commands, Cypress, cy) { */ const createSessionWorkflow = (existingSession, step: 'create' | 'recreate') => { cy.then(async () => { - Cypress.state('current').set('name', step) setSessionLogStatus(statusMap.inProgress(step)) await navigateAboutBlank() @@ -511,7 +476,6 @@ export default function (Commands, Cypress, cy) { */ const restoreSessionWorkflow = (existingSession) => { cy.then(async () => { - Cypress.state('current').set('name', 'restore') setSessionLogStatus('restoring') await navigateAboutBlank() await sessions.clearCurrentSessionData() @@ -520,7 +484,6 @@ export default function (Commands, Cypress, cy) { }) .then(() => validateSession(existingSession, 'restore')) .then((isValidSession: boolean) => { - Cypress.state('current').set('name', 'after restore validation') if (!isValidSession) { return createSessionWorkflow(existingSession, 'recreate') } @@ -546,7 +509,6 @@ export default function (Commands, Cypress, cy) { return logGroup(Cypress, groupDetails, (log) => { return cy.then(async () => { - Cypress.state('current').set('name', 'start session workflow') _log = log if (!session.hydrated) { @@ -563,7 +525,6 @@ export default function (Commands, Cypress, cy) { return restoreSessionWorkflow(session) }).then(() => { - Cypress.state('current').set('name', 'after session workflow') _log.set({ state: 'passed' }) }) }) diff --git a/packages/driver/src/cypress/command_queue.ts b/packages/driver/src/cypress/command_queue.ts index d7e2b0313d23..3f85bf942e12 100644 --- a/packages/driver/src/cypress/command_queue.ts +++ b/packages/driver/src/cypress/command_queue.ts @@ -19,9 +19,10 @@ const __stackReplacementMarker = (fn, args) => { return fn(...args) } -const commandRunningFailed = (Cypress, state, err) => { +const commandRunningFailed = (Cypress, err: Error | string, current?: $Command) => { // allow for our own custom onFail function - if (err.onFail) { + // @ts-ignore + if (_.isObject(err) && err.onFail) { err.onFail(err) // clean up this onFail callback after it's been called delete err.onFail @@ -29,7 +30,6 @@ const commandRunningFailed = (Cypress, state, err) => { return } - const current = state('current') const lastLog = current?.getLastLog() const consoleProps = () => { @@ -170,9 +170,11 @@ export class CommandQueue extends Queue<$Command> { } cleanup () { - if (this.state('runnable')) { + const runnable = this.state('runnable') + + if (!runnable.isPending()) { // make sure we reset the runnable's timeout now - this.state('runnable').resetTimeout() + runnable.resetTimeout() } // if a command fails then after each commands @@ -276,7 +278,8 @@ export class CommandQueue extends Queue<$Command> { } return ret - }).then((subject) => { + }) + .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 @@ -317,9 +320,36 @@ export class CommandQueue extends Queue<$Command> { } run () { - const runQueueable = () => { + if (this.stopped) { + this.cleanup() + + return Promise.resolve() + } + + const next = () => { const command = this.at(this.index) + // 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(this.index - 1), + next: this.at(this.index + 1), + }) + + this.setSubjectForChainer(command.get('chainerId'), command.get('subject')) + + if (command.state === 'skipped') { + Cypress.action('cy:skipped:command:end', command) + } + + // move on to the next queueable + this.index += 1 + + return next() + } + // if we're at the very end if (!command) { // trigger queue is almost finished @@ -343,27 +373,6 @@ export class CommandQueue extends Queue<$Command> { }) } - // if the command 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(this.index - 1), - next: this.at(this.index + 1), - }) - - this.setSubjectForChainer(command.get('chainerId'), command.get('subject')) - - if (command.state === 'skipped') { - Cypress.action('cy:skipped:command:end', command) - } - - // move on to the next queueable - this.index += 1 - - return runQueueable() - } - // store the previous timeout const prevTimeout = this.timeout() @@ -393,45 +402,42 @@ export class CommandQueue extends Queue<$Command> { Cypress.action('cy:command:end', command) + // move on to the next queueable + this.index += 1 + const pauseFn = this.state('onPaused') if (pauseFn) { return new Bluebird((resolve) => { return pauseFn(resolve) - .then(() => { - // move on to the next queueable - this.index += 1 - - return runQueueable() - }) + }) + .then(() => { + return next() }) } - // move on to the next queueable - this.index += 1 - - return runQueueable() + return next() }) } - const onQueueError = (err: Error | string) => { + const onError = (err: Error | string) => { // 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 } - const current = this.state('current') - if (this.state('onQueueFailed')) { err = this.state('onQueueFailed')(err, this) this.state('onQueueFailed', null) } - debugErrors('cypress command had an error: %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 @@ -441,12 +447,14 @@ 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) { + if (_.isObject(err) && err.isRecovered) { current?.recovered() - return // let the queue move on to the next command + return // let the queue end & restart on to the next command index (set in onQueueFailed) } if (current?.state === 'queued') { @@ -460,21 +468,14 @@ export class CommandQueue extends Queue<$Command> { return this.fail(err) } - if (this.stopped) { - this.cleanup() - - return Promise.resolve() - } - const { promise, reject, cancel } = super.run({ - onRun: runQueueable, - onError: onQueueError, + onRun: next, + onError, onFinish: this.run, }) this.state('promise', promise) this.state('reject', reject) - this.state('cancel', () => { cancel() diff --git a/packages/driver/src/cypress/cy.ts b/packages/driver/src/cypress/cy.ts index 278fed1c55b0..b46e04403c8e 100644 --- a/packages/driver/src/cypress/cy.ts +++ b/packages/driver/src/cypress/cy.ts @@ -470,7 +470,7 @@ 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 { - Cypress.state('logGroupIds', []) // reset log groups so assertions are at the top level + 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')) @@ -1320,6 +1320,5 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert this.state('canceled', true) this.state('cancel')() } - } } diff --git a/packages/driver/src/cypress/log.ts b/packages/driver/src/cypress/log.ts index 2c84732664f7..75b131db2cc4 100644 --- a/packages/driver/src/cypress/log.ts +++ b/packages/driver/src/cypress/log.ts @@ -554,10 +554,9 @@ export class Log { } class LogManager { - logs: Record = {} + logs: Record = {} - constructor (isInteractive: boolean) { - this.isInteractive = isInteractive + constructor () { this.fireChangeEvent = this.fireChangeEvent.bind(this) } diff --git a/packages/driver/src/cypress/runner.ts b/packages/driver/src/cypress/runner.ts index 1e06b9c11410..79b5301059be 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 @@ -1514,6 +1514,16 @@ export default { // original err next(err) + // return null here to signal to bluebird + // that we did not forget to return a promise + // because mocha internally does not return + // the test.run(fn) + return null + }).catch((err) => { + // once we complete callback with the + // original err + next(err) + // return null here to signal to bluebird // that we did not forget to return a promise // because mocha internally does not return diff --git a/packages/driver/src/cypress/utils.ts b/packages/driver/src/cypress/utils.ts index 992c1bd3145b..a6c8956a6621 100644 --- a/packages/driver/src/cypress/utils.ts +++ b/packages/driver/src/cypress/utils.ts @@ -397,6 +397,7 @@ export default { }, isPromiseLike (ret) { + // @ts-ignore return ret && _.isObject(ret) && _.isFunction(ret.then) && _.isFunction(ret.catch) }, } diff --git a/packages/driver/src/util/queue.ts b/packages/driver/src/util/queue.ts index 6482ba80a129..7fd2e9e832e2 100644 --- a/packages/driver/src/util/queue.ts +++ b/packages/driver/src/util/queue.ts @@ -3,7 +3,7 @@ import Bluebird from 'bluebird' interface QueueRunProps { onRun: () => Bluebird | Promise onError: (err: Error) => void - onFinish: () => void + onFinish: () => Bluebird | Promise } export class Queue { diff --git a/packages/driver/types/cy/logGroup.d.ts b/packages/driver/types/cy/logGroup.d.ts index b50e1adb97f1..ea36fd72fd0e 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 | ObjectLike // the type of log // system - log generated by Cypress // parent - log generated by Command diff --git a/packages/reporter/src/sessions/sessions-model.ts b/packages/reporter/src/sessions/sessions-model.ts index fbfc39e256ae..1e1028704672 100644 --- a/packages/reporter/src/sessions/sessions-model.ts +++ b/packages/reporter/src/sessions/sessions-model.ts @@ -27,7 +27,7 @@ export default class Session extends Instrument { } update (props: Partial) { - const { sessionInfo, state } = props + const { sessionInfo } = props this.status = sessionInfo?.status || '' } From 5b137ec80059098088a88ee0ab9244363cef0be4 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Tue, 1 Nov 2022 18:14:00 -0500 Subject: [PATCH 38/52] fix tests --- .husky/pre-commit | 2 +- packages/app/src/runner/logger.ts | 2 +- .../cypress/e2e/commands/sessions/sessions.cy.js | 4 ++-- .../driver/cypress/e2e/commands/sessions/utils.cy.js | 10 +++++----- packages/driver/src/cy/commands/sessions/index.ts | 6 +++--- packages/driver/src/cy/commands/sessions/utils.ts | 2 -- packages/driver/src/cypress/command_queue.ts | 9 ++++----- packages/driver/src/cypress/state.ts | 2 +- 8 files changed, 17 insertions(+), 20 deletions(-) diff --git a/.husky/pre-commit b/.husky/pre-commit index 9f5c7d244218..36af219892fd 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,4 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" -# npx lint-staged +npx lint-staged diff --git a/packages/app/src/runner/logger.ts b/packages/app/src/runner/logger.ts index 9b08b26f4301..074d668369b9 100644 --- a/packages/app/src/runner/logger.ts +++ b/packages/app/src/runner/logger.ts @@ -127,7 +127,7 @@ export const logger = { }, _logTables (consoleProps: any) { - const logTable = ({ name, data, columns }: Table) => { + const logTable = ({ name, data, columns }) => { let tableData = data if (Cypress.isBrowser('webkit')) { diff --git a/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js b/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js index 1d405c055997..2eba2f6cb46e 100644 --- a/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js +++ b/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js @@ -261,7 +261,7 @@ describe('cy.session', { retries: 0 }, () => { expect(consoleProps.Command).to.eq('session') expect(consoleProps.id).to.eq('session-1') - expect(consoleProps.Domains).to.eq('This session capture data from localhost.') + 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:') @@ -918,7 +918,7 @@ describe('cy.session', { retries: 0 }, () => { expect(consoleProps.Command).to.eq('session') expect(consoleProps.id).to.eq('session-1') - expect(consoleProps.Domains).to.eq('This session capture data from localhost.') + 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:') diff --git a/packages/driver/cypress/e2e/commands/sessions/utils.cy.js b/packages/driver/cypress/e2e/commands/sessions/utils.cy.js index 107e93a65e1b..f9dbb4cc3aa0 100644 --- a/packages/driver/cypress/e2e/commands/sessions/utils.cy.js +++ b/packages/driver/cypress/e2e/commands/sessions/utils.cy.js @@ -40,7 +40,7 @@ describe('src/cy/commands/sessions/utils.ts', () => { logForDebugging(consoleProps) expect(consoleProps.id).to.eq('session1') - expect(consoleProps.Domains).to.eq('This session capture data from localhost.') + 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:') @@ -64,7 +64,7 @@ describe('src/cy/commands/sessions/utils.ts', () => { logForDebugging(consoleProps) expect(consoleProps.id).to.eq('session1') - expect(consoleProps.Domains).to.eq('This session capture data from localhost.') + 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:') @@ -88,7 +88,7 @@ describe('src/cy/commands/sessions/utils.ts', () => { logForDebugging(consoleProps) expect(consoleProps.id).to.eq('session1') - expect(consoleProps.Domains).to.eq('This session capture data from localhost.') + 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:') @@ -116,7 +116,7 @@ describe('src/cy/commands/sessions/utils.ts', () => { logForDebugging(consoleProps) expect(consoleProps.id).to.eq('session1') - expect(consoleProps.Domains).to.eq('This session capture data from localhost.') + 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:') @@ -150,7 +150,7 @@ describe('src/cy/commands/sessions/utils.ts', () => { logForDebugging(consoleProps) expect(consoleProps.id).to.eq('session1') - expect(consoleProps.Domains).to.eq('This session capture data from localhost and example.com.') + 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:') diff --git a/packages/driver/src/cy/commands/sessions/index.ts b/packages/driver/src/cy/commands/sessions/index.ts index 073b8834d219..f3c6f32feb20 100644 --- a/packages/driver/src/cy/commands/sessions/index.ts +++ b/packages/driver/src/cy/commands/sessions/index.ts @@ -214,12 +214,12 @@ export default function (Commands, Cypress, cy) { 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('onQueueFailed', (err) => { + cy.state('onQueueFailed', (err: Error | string, _queue): Error => { setupLogGroup.set({ state: 'failed', consoleProps: { Step: statusMap.stepName(step), - Error: err.stack || err.message, + Error: err?.stack || err?.message || err, }, }) @@ -339,7 +339,7 @@ export default function (Commands, Cypress, cy) { return err } - cy.state('onQueueFailed', (err: Error | string, queue): boolean => { + cy.state('onQueueFailed', (err: Error | string, queue): Error => { if (step === 'restore') { const commands = queue.get() // determine command queue index of _commandToRunAfterValidation's index diff --git a/packages/driver/src/cy/commands/sessions/utils.ts b/packages/driver/src/cy/commands/sessions/utils.ts index 83024acd770d..f8276d45bc8c 100644 --- a/packages/driver/src/cy/commands/sessions/utils.ts +++ b/packages/driver/src/cy/commands/sessions/utils.ts @@ -102,8 +102,6 @@ const getConsoleProps = (session: SessionData) => { const sessionDetails = getSessionDetailsByDomain(session) const groupsByDomain = _.flatMap(sessionDetails, (val, domain) => { - // const info = '⚠️ There are no ${cookies, local storage or session} storage associated to this domain.', - return { name: `${domain} data:`, expand: true, diff --git a/packages/driver/src/cypress/command_queue.ts b/packages/driver/src/cypress/command_queue.ts index 3f85bf942e12..66601ab40374 100644 --- a/packages/driver/src/cypress/command_queue.ts +++ b/packages/driver/src/cypress/command_queue.ts @@ -172,7 +172,7 @@ export class CommandQueue extends Queue<$Command> { cleanup () { const runnable = this.state('runnable') - if (!runnable.isPending()) { + if (runnable && !runnable.isPending()) { // make sure we reset the runnable's timeout now runnable.resetTimeout() } @@ -319,6 +319,8 @@ export class CommandQueue extends Queue<$Command> { }) } + // TypeScript doesn't allow overriding functions with different type signatures + // @ts-ignore run () { if (this.stopped) { this.cleanup() @@ -410,10 +412,7 @@ export class CommandQueue extends Queue<$Command> { if (pauseFn) { return new Bluebird((resolve) => { return pauseFn(resolve) - }) - .then(() => { - return next() - }) + }).then(next) } return next() diff --git a/packages/driver/src/cypress/state.ts b/packages/driver/src/cypress/state.ts index ef93964d30e0..491ff329a8dd 100644 --- a/packages/driver/src/cypress/state.ts +++ b/packages/driver/src/cypress/state.ts @@ -51,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: 'onQueueFailed', v?: (err: any, queue: any) => boolean): (err: any, queue: any) => boolean + (k: 'onQueueFailed', v?: (err: Error | string, queue?: any) => Error): (err: Error | string, queue?: any) => Error (k: 'promise', v?: Bluebird): Bluebird (k: 'reject', v?: (err: any) => any): (err: any) => any (k: 'cancel', v?: () => void): () => void From f34fac4401fca5def02df81b3d06d632df013610 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Wed, 2 Nov 2022 13:23:14 -0500 Subject: [PATCH 39/52] fix types --- .../driver/src/cy/commands/sessions/index.ts | 71 +++++++++++-------- .../driver/src/cy/commands/sessions/utils.ts | 2 +- packages/driver/src/cypress/command_queue.ts | 9 ++- packages/driver/src/cypress/error_utils.ts | 6 +- packages/driver/src/cypress/state.ts | 2 +- packages/driver/types/cy/logGroup.d.ts | 2 +- .../cypress/e2e/session/session.cy.js | 6 -- 7 files changed, 53 insertions(+), 45 deletions(-) diff --git a/packages/driver/src/cy/commands/sessions/index.ts b/packages/driver/src/cy/commands/sessions/index.ts index f3c6f32feb20..8642fec0311f 100644 --- a/packages/driver/src/cy/commands/sessions/index.ts +++ b/packages/driver/src/cy/commands/sessions/index.ts @@ -1,15 +1,16 @@ import _ from 'lodash' import stringifyStable from 'json-stable-stringify' -import $utils from '../../../cypress/utils' 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, } from './utils' + import type { ServerSessionData } from '@packages/types' type SessionData = Cypress.Commands.Session.SessionData @@ -214,12 +215,18 @@ export default function (Commands, Cypress, cy) { 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('onQueueFailed', (err: Error | string, _queue): Error => { + cy.state('onQueueFailed', (err, _queue) => { + if (!_.isObject(err)) { + err = new Error(err) + } + setupLogGroup.set({ state: 'failed', - consoleProps: { - Step: statusMap.stepName(step), - Error: err?.stack || err?.message || err, + consoleProps: () => { + return { + Step: statusMap.stepName(step), + Error: err?.stack || err?.message, + } }, }) @@ -256,25 +263,23 @@ export default function (Commands, Cypress, cy) { }) } - function restoreSession (testSession) { - return cy.then(async () => { - Cypress.log({ - name: 'session', - displayName: 'Restore saved session', - message: '', - type: 'system', - consoleProps: () => { - return { - Step: 'Restore saved session', - ...getConsoleProps(testSession), - } - }, - }) + 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, step: SESSION_STEPS) { @@ -289,8 +294,10 @@ export default function (Commands, Cypress, cy) { displayName: 'Validate session', message: '', type: 'system', - consoleProps: { - Step: 'Validate Session', + consoleProps: () => { + return { + Step: 'Validate Session', + } }, }, (validateLog) => { return cy.then(async () => { @@ -313,8 +320,10 @@ export default function (Commands, Cypress, cy) { validateLog.set({ state: 'failed', - consoleProps: { - Error: err.stack, + consoleProps: () => { + return { + Error: err.stack, + } }, // explicitly set via .set() so we don't end the log group early ...(!caughtCommandErr && { error: err }), @@ -326,7 +335,7 @@ export default function (Commands, Cypress, cy) { setSessionLogStatus('failed') validateLog.set({ state: 'failed', - consoleProps: (args) => { + consoleProps: () => { return { Error: err.stack, } @@ -339,7 +348,11 @@ export default function (Commands, Cypress, cy) { return err } - cy.state('onQueueFailed', (err: Error | string, queue): Error => { + cy.state('onQueueFailed', (err, queue): Error => { + if (!_.isObject(err)) { + err = new Error(err) + } + if (step === 'restore') { const commands = queue.get() // determine command queue index of _commandToRunAfterValidation's index @@ -350,7 +363,7 @@ export default function (Commands, Cypress, cy) { ) }) - // skip all commands between this command and _commandToRunAfterValidation + // skip all commands between this command which errored and _commandToRunAfterValidation for (let i = cy.queue.index; i < index; i++) { const cmd = commands[i] diff --git a/packages/driver/src/cy/commands/sessions/utils.ts b/packages/driver/src/cy/commands/sessions/utils.ts index f8276d45bc8c..cabc773f50c4 100644 --- a/packages/driver/src/cy/commands/sessions/utils.ts +++ b/packages/driver/src/cy/commands/sessions/utils.ts @@ -134,7 +134,7 @@ const getConsoleProps = (session: SessionData) => { Warning: '⚠️ There are no cookies, local storage or session storage associated to this session.', }), ...(groupsByDomain.length && { - Domains: `This session capture data from ${Object.keys(sessionDetails).join(' and ')}.`, + Domains: `This session captured data from ${Object.keys(sessionDetails).join(' and ')}.`, }), groups: _.compact(groupsByDomain), } diff --git a/packages/driver/src/cypress/command_queue.ts b/packages/driver/src/cypress/command_queue.ts index 66601ab40374..0510e307e3f6 100644 --- a/packages/driver/src/cypress/command_queue.ts +++ b/packages/driver/src/cypress/command_queue.ts @@ -19,10 +19,9 @@ const __stackReplacementMarker = (fn, args) => { return fn(...args) } -const commandRunningFailed = (Cypress, err: Error | string, current?: $Command) => { +const commandRunningFailed = (Cypress, err, current?: $Command) => { // allow for our own custom onFail function - // @ts-ignore - if (_.isObject(err) && err.onFail) { + if (err.onFail && _.isFunction(err.onFail)) { err.onFail(err) // clean up this onFail callback after it's been called delete err.onFail @@ -419,7 +418,7 @@ 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') @@ -450,7 +449,7 @@ export class CommandQueue extends Queue<$Command> { commandRunningFailed(Cypress, err, current) - if (_.isObject(err) && err.isRecovered) { + if (err.isRecovered) { current?.recovered() return // let the queue end & restart on to the next command index (set in onQueueFailed) diff --git a/packages/driver/src/cypress/error_utils.ts b/packages/driver/src/cypress/error_utils.ts index a13cf31b8372..36f65e99571d 100644 --- a/packages/driver/src/cypress/error_utils.ts +++ b/packages/driver/src/cypress/error_utils.ts @@ -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/state.ts b/packages/driver/src/cypress/state.ts index 491ff329a8dd..1b6e3914b4ce 100644 --- a/packages/driver/src/cypress/state.ts +++ b/packages/driver/src/cypress/state.ts @@ -51,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: 'onQueueFailed', v?: (err: Error | string, queue?: any) => Error): (err: Error | string, queue?: any) => Error + (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/types/cy/logGroup.d.ts b/packages/driver/types/cy/logGroup.d.ts index ea36fd72fd0e..ef553039fe06 100644 --- a/packages/driver/types/cy/logGroup.d.ts +++ b/packages/driver/types/cy/logGroup.d.ts @@ -20,7 +20,7 @@ declare namespace Cypress { // timeout of the group command - defaults to defaultCommandTimeout timeout?: number // Return an object that will be printed in the dev tools console - consoleProps?: () => ObjectLike | ObjectLike + consoleProps?: () => ObjectLike // the type of log // system - log generated by Cypress // parent - log generated by Command 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) From d6a6f90851f463bf2ac0e78b074d805fadf50f8d Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Wed, 2 Nov 2022 15:37:06 -0500 Subject: [PATCH 40/52] and fix code frames --- .../app/cypress/e2e/runner/sessions.ui.cy.ts | 54 ++++++++++++++---- .../e2e/commands/sessions/sessions.cy.js | 10 ++-- .../driver/src/cy/commands/sessions/index.ts | 57 ++++--------------- .../driver/src/cy/commands/sessions/utils.ts | 42 ++++++++++++++ packages/reporter/src/lib/variables.scss | 2 +- .../cypress/e2e/session/errors.cy.js | 2 - 6 files changed, 102 insertions(+), 65 deletions(-) diff --git a/packages/app/cypress/e2e/runner/sessions.ui.cy.ts b/packages/app/cypress/e2e/runner/sessions.ui.cy.ts index 800b19b7a455..028afacba2a1 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, }, () => { @@ -349,6 +349,11 @@ describe('runner/cypress sessions.ui.spec', { .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', () => { @@ -382,8 +387,16 @@ describe('runner/cypress sessions.ui.spec', { testCase: 'throws an error', systemTestTitle: 'validate - throws an error', errMessage: 'Something went wrong!', + hasCodeFrame: false, }, - ].forEach(({ testCase, systemTestTitle, errMessage }, index) => { + ].forEach((opts, index) => { + if (index !== 5) { + return + } + + const { testCase, systemTestTitle, errMessage } = opts + const hasCodeFrame = opts.hasCodeFrame !== undefined ? opts.hasCodeFrame : true + it(`has test error when validate ${testCase}`, () => { cy.contains('.test', systemTestTitle).as('example_test') cy.get('@example_test') @@ -421,18 +434,28 @@ describe('runner/cypress sessions.ui.spec', { .find('.attempt-error-region') .contains(errMessage) .contains(validateErrPostFix) + + if (hasCodeFrame) { + cy.get('@example_test') + .find('.attempt-error-region') + .find('.test-err-code-frame') + .should('exist') + } }) }) }) }) describe('recreated session', () => { - const assertRecreatedSession = ({ - testAlias, - validationErrMessage, - commandPassed, - successfullyRecreatedSession, - }) => { + const assertRecreatedSession = (opts) => { + const { + testAlias, + validationErrMessage, + commandPassed, + successfullyRecreatedSession, + } = opts + const hasCodeFrame = opts.hasCodeFrame !== undefined ? opts.hasCodeFrame : true + cy.get(testAlias) .should('have.attr', 'data-model-state', commandPassed ? 'passed' : 'failed') .children('.collapsible') @@ -476,6 +499,13 @@ describe('runner/cypress sessions.ui.spec', { .contains(validationErrMessage) .contains(restoredMessagePostfix) + if (hasCodeFrame) { + 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') @@ -533,8 +563,9 @@ describe('runner/cypress sessions.ui.spec', { testCase: 'throws an error', systemTestTitle: 'validate - throws an error', errMessage: 'Something went wrong!', + hasCodeFrame: false, }, - ].forEach(({ testCase, systemTestTitle, errMessage }, index) => { + ].forEach(({ testCase, systemTestTitle, errMessage, hasCodeFrame }, index) => { it(`has test error when validate ${testCase}`, () => { cy.contains('.test', systemTestTitle).as('example_test') @@ -544,6 +575,7 @@ describe('runner/cypress sessions.ui.spec', { validationErrMessage: errMessage, commandPassed: true, successfullyRecreatedSession: true, + hasCodeFrame, }) }) @@ -624,8 +656,9 @@ describe('runner/cypress sessions.ui.spec', { testCase: 'throws an error', systemTestTitle: 'validate - throws an error', errMessage: 'Something went wrong!', + hasCodeFrame: false, }, - ].forEach(({ testCase, systemTestTitle, errMessage }, index) => { + ].forEach(({ testCase, systemTestTitle, errMessage, hasCodeFrame }, index) => { it(`has test error when validate ${testCase}`, () => { cy.contains('.test', systemTestTitle).as('example_test') @@ -635,6 +668,7 @@ describe('runner/cypress sessions.ui.spec', { validationErrMessage: errMessage, commandPassed: false, successfullyRecreatedSession: true, + hasCodeFrame, }) }) diff --git a/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js b/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js index 2eba2f6cb46e..99dda1961d61 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.') @@ -1062,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() }) @@ -1119,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() }) @@ -1294,7 +1294,7 @@ 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(() => Promise.reject(false)) }) diff --git a/packages/driver/src/cy/commands/sessions/index.ts b/packages/driver/src/cy/commands/sessions/index.ts index 8642fec0311f..d59c8eaf94db 100644 --- a/packages/driver/src/cy/commands/sessions/index.ts +++ b/packages/driver/src/cy/commands/sessions/index.ts @@ -2,13 +2,13 @@ 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' @@ -24,8 +24,6 @@ type SessionData = Cypress.Commands.Session.SessionData * - 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', { @@ -42,46 +40,6 @@ export default function (Commands, Cypress, cy) { const sessions = sessionsManager.sessions type SESSION_STEPS = 'create' | 'restore' | 'recreate' | 'validate' - 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.`) - } - }, - } Cypress.on('run:start', () => { // @ts-ignore @@ -210,7 +168,6 @@ export default function (Commands, Cypress, cy) { 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 @@ -307,10 +264,16 @@ export default function (Commands, Cypress, cy) { const enhanceErr = (err) => { Cypress.state('onQueueFailed', null) - if (_.isString(err)) { + if (!(err instanceof Error)) { err = new Error(err) } + 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 (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) @@ -349,7 +312,7 @@ export default function (Commands, Cypress, cy) { } cy.state('onQueueFailed', (err, queue): Error => { - if (!_.isObject(err)) { + if (!(err instanceof Error)) { err = new Error(err) } @@ -443,7 +406,7 @@ export default function (Commands, Cypress, cy) { const yielded = cy.state('current').get('prev')?.attributes?.subject if (yielded === false) { - // set current command to cy.session for more accurate codeframe + // 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' })) diff --git a/packages/driver/src/cy/commands/sessions/utils.ts b/packages/driver/src/cy/commands/sessions/utils.ts index cabc773f50c4..a069f9a48982 100644 --- a/packages/driver/src/cy/commands/sessions/utils.ts +++ b/packages/driver/src/cy/commands/sessions/utils.ts @@ -203,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/reporter/src/lib/variables.scss b/packages/reporter/src/lib/variables.scss index 47f0c615df1e..5dc085420b5d 100644 --- a/packages/reporter/src/lib/variables.scss +++ b/packages/reporter/src/lib/variables.scss @@ -104,7 +104,7 @@ $fail: $red-400; $pending: $indigo-400; $pinned: $purple-400; $retried: $orange-400; -$warn: $orange-800; +$yellow-medium: $orange-800; $link-text: $indigo-600; 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 9d0426b7a48d..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 @@ -77,7 +77,6 @@ it('setup - has failing command', () => { } }) -// FIX ME (recreated stuck in ''restoring, double error and test marked as passed) it('validate - has failing Cypress command', function () { function validate () { count += 1 @@ -219,7 +218,6 @@ it('validate - promise resolves false', () => { } }) -// FIX ME (recreated stuck in ''restoring, double error and test marked as passed) it('validate - throws an error', () => { function validate () { count += 1 From 28f6add42b8146c1fb95ea01f1e48d868d0a5f5e Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Thu, 3 Nov 2022 15:52:25 -0500 Subject: [PATCH 41/52] fix ensure exists to be a cypress error instead of an assertion error --- .../e2e/commands/querying/querying.cy.js | 28 ++++++++++++++++++- packages/driver/src/cy/ensures.ts | 2 ++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/packages/driver/cypress/e2e/commands/querying/querying.cy.js b/packages/driver/cypress/e2e/commands/querying/querying.cy.js index 9fc49815d480..6c6eda512dea 100644 --- a/packages/driver/cypress/e2e/commands/querying/querying.cy.js +++ b/packages/driver/cypress/e2e/commands/querying/querying.cy.js @@ -766,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() @@ -788,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('CypressError') expect(err.message).to.include('Expected to find element: `#missing-el`, but never found it.') done() @@ -798,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('CypressError') expect(err.message).to.include('Expected to find element: `#missing-el`, but never found it.') done() @@ -808,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('CypressError') expect(err.message).to.include('Expected to find element: `#missing-el`, but never found it.') done() @@ -816,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('CypressError') expect(err.message).to.include('Expected to find element: `getJsonButton`, but never found it.') done() @@ -845,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() @@ -855,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() @@ -867,6 +883,7 @@ describe('src/cy/commands/querying', () => { cy.on('fail', (err) => { const { lastLog } = this + expect(err.name).to.eq('CypressError') 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') @@ -884,6 +901,7 @@ describe('src/cy/commands/querying', () => { const { logs, lastLog } = this const getLog = logs[logs.length - 2] + expect(err.name).to.eq('CypressError') 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') @@ -902,6 +920,7 @@ describe('src/cy/commands/querying', () => { 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() @@ -1733,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') @@ -1745,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') @@ -1774,6 +1795,7 @@ space it('throws when there is no filter and no subject', (done) => { cy.on('fail', (err) => { + expect(err.name).to.eq('CypressError') expect(err.message).to.include('Expected to find content: \'brand new content\' but never did.') done() @@ -1784,6 +1806,7 @@ space it('throws when there is a filter', (done) => { cy.on('fail', (err) => { + expect(err.name).to.eq('CypressError') expect(err.message).to.include('Expected to find content: \'brand new content\' within the selector: \'span\' but never did.') done() @@ -1794,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('CypressError') expect(err.message).to.include('Expected to find content: \'0\' within the element: but never did.') done() @@ -1804,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('CypressError') expect(err.message).to.include('Expected to find content: \'foo\' within the element: and with the selector: \'ul\' but never did.') done() @@ -1814,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() diff --git a/packages/driver/src/cy/ensures.ts b/packages/driver/src/cy/ensures.ts index 107541854512..a24bccf869e0 100644 --- a/packages/driver/src/cy/ensures.ts +++ b/packages/driver/src/cy/ensures.ts @@ -251,6 +251,8 @@ export const create = (state: StateFunc, expect: $Cy['expect']) => { // verify the $el exists and use our default error messages try { expect(subject).to.exist + } catch (err) { + throw $errUtils.mergeErrProps(err, $errUtils.cypressErr(new Error(err.message))) } finally { state('onBeforeLog', null) } From 443e311d8da42fc2b28926d5564433e9135f672a Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Thu, 3 Nov 2022 15:52:46 -0500 Subject: [PATCH 42/52] snapshot cache --- tooling/v8-snapshot/cache/dev-darwin/snapshot-meta.cache.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tooling/v8-snapshot/cache/dev-darwin/snapshot-meta.cache.json b/tooling/v8-snapshot/cache/dev-darwin/snapshot-meta.cache.json index 1545a365fdea..39c95f513609 100644 --- a/tooling/v8-snapshot/cache/dev-darwin/snapshot-meta.cache.json +++ b/tooling/v8-snapshot/cache/dev-darwin/snapshot-meta.cache.json @@ -1101,7 +1101,6 @@ "./node_modules/ansi_up/ansi_up.js", "./node_modules/any-base/index.js", "./node_modules/any-base/src/converter.js", - "./node_modules/anymatch/index.js", "./node_modules/archiver-utils/file.js", "./node_modules/archiver-utils/index.js", "./node_modules/archiver-utils/node_modules/glob/common.js", @@ -3285,6 +3284,7 @@ "./node_modules/yn/lenient.js", "./packages/data-context/node_modules/@babel/code-frame/lib/index.js", "./packages/data-context/node_modules/@babel/parser/lib/index.js", + "./packages/data-context/node_modules/anymatch/index.js", "./packages/data-context/node_modules/cross-spawn/index.js", "./packages/data-context/node_modules/cross-spawn/lib/enoent.js", "./packages/data-context/node_modules/cross-spawn/lib/parse.js", @@ -3528,5 +3528,5 @@ "./tooling/v8-snapshot/cache/dev-darwin/snapshot-entry.js" ], "deferredHashFile": "yarn.lock", - "deferredHash": "66156a5424fbd0d70c750ef2b16c22b0084a30220c67b632cff9212a7de7a226" + "deferredHash": "38a57222b063271f0a219966e246dafa9d1b2a118dd3cb545a691f06bddce332" } \ No newline at end of file From ff8deb5c8f17a466947dd981762b1d7a10e808d0 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Fri, 4 Nov 2022 13:50:08 -0500 Subject: [PATCH 43/52] Fix codeframe issue & redundant reported log message & error nesting. --- .../app/cypress/e2e/runner/sessions.ui.cy.ts | 33 +++++++------------ .../cypress/e2e/commands/assertions.cy.js | 2 +- packages/driver/src/cy/assertions.ts | 4 +-- .../driver/src/cy/commands/sessions/index.ts | 28 +++++++++++----- packages/driver/src/cypress/runner.ts | 2 -- .../reporter/src/commands/command-model.ts | 8 +++++ packages/reporter/src/commands/command.tsx | 4 +-- 7 files changed, 43 insertions(+), 38 deletions(-) diff --git a/packages/app/cypress/e2e/runner/sessions.ui.cy.ts b/packages/app/cypress/e2e/runner/sessions.ui.cy.ts index 028afacba2a1..45fc975c8d93 100644 --- a/packages/app/cypress/e2e/runner/sessions.ui.cy.ts +++ b/packages/app/cypress/e2e/runner/sessions.ui.cy.ts @@ -387,7 +387,6 @@ describe('runner/cypress sessions.ui.spec', { testCase: 'throws an error', systemTestTitle: 'validate - throws an error', errMessage: 'Something went wrong!', - hasCodeFrame: false, }, ].forEach((opts, index) => { if (index !== 5) { @@ -395,7 +394,6 @@ describe('runner/cypress sessions.ui.spec', { } const { testCase, systemTestTitle, errMessage } = opts - const hasCodeFrame = opts.hasCodeFrame !== undefined ? opts.hasCodeFrame : true it(`has test error when validate ${testCase}`, () => { cy.contains('.test', systemTestTitle).as('example_test') @@ -435,12 +433,10 @@ describe('runner/cypress sessions.ui.spec', { .contains(errMessage) .contains(validateErrPostFix) - if (hasCodeFrame) { - cy.get('@example_test') - .find('.attempt-error-region') - .find('.test-err-code-frame') - .should('exist') - } + cy.get('@example_test') + .find('.attempt-error-region') + .find('.test-err-code-frame') + .should('exist') }) }) }) @@ -454,7 +450,6 @@ describe('runner/cypress sessions.ui.spec', { commandPassed, successfullyRecreatedSession, } = opts - const hasCodeFrame = opts.hasCodeFrame !== undefined ? opts.hasCodeFrame : true cy.get(testAlias) .should('have.attr', 'data-model-state', commandPassed ? 'passed' : 'failed') @@ -499,12 +494,10 @@ describe('runner/cypress sessions.ui.spec', { .contains(validationErrMessage) .contains(restoredMessagePostfix) - if (hasCodeFrame) { - cy.get('@session_command') - .find('.recovered-test-err') - .find('.test-err-code-frame') - .should('exist') - } + 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') @@ -563,9 +556,8 @@ describe('runner/cypress sessions.ui.spec', { testCase: 'throws an error', systemTestTitle: 'validate - throws an error', errMessage: 'Something went wrong!', - hasCodeFrame: false, }, - ].forEach(({ testCase, systemTestTitle, errMessage, hasCodeFrame }, index) => { + ].forEach(({ testCase, systemTestTitle, errMessage }, index) => { it(`has test error when validate ${testCase}`, () => { cy.contains('.test', systemTestTitle).as('example_test') @@ -575,7 +567,6 @@ describe('runner/cypress sessions.ui.spec', { validationErrMessage: errMessage, commandPassed: true, successfullyRecreatedSession: true, - hasCodeFrame, }) }) @@ -617,7 +608,7 @@ describe('runner/cypress sessions.ui.spec', { }) }) - const recreatedErrPostfix = 'This error occurred while creating the session. Because the session setup failed, we failed the test.' + 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') @@ -656,9 +647,8 @@ describe('runner/cypress sessions.ui.spec', { testCase: 'throws an error', systemTestTitle: 'validate - throws an error', errMessage: 'Something went wrong!', - hasCodeFrame: false, }, - ].forEach(({ testCase, systemTestTitle, errMessage, hasCodeFrame }, index) => { + ].forEach(({ testCase, systemTestTitle, errMessage }) => { it(`has test error when validate ${testCase}`, () => { cy.contains('.test', systemTestTitle).as('example_test') @@ -668,7 +658,6 @@ describe('runner/cypress sessions.ui.spec', { validationErrMessage: errMessage, commandPassed: false, successfullyRecreatedSession: true, - hasCodeFrame, }) }) diff --git a/packages/driver/cypress/e2e/commands/assertions.cy.js b/packages/driver/cypress/e2e/commands/assertions.cy.js index 865cbd24cbb7..6ce3dac18306 100644 --- a/packages/driver/cypress/e2e/commands/assertions.cy.js +++ b/packages/driver/cypress/e2e/commands/assertions.cy.js @@ -1029,7 +1029,7 @@ describe('src/cy/commands/assertions', () => { expected: false, actual: true, Message: 'expected true to be false', - Error: log.get('error').stack, + Error: log.get('error').message, }) done() diff --git a/packages/driver/src/cy/assertions.ts b/packages/driver/src/cy/assertions.ts index ad004f12cd18..e655000643ba 100644 --- a/packages/driver/src/cy/assertions.ts +++ b/packages/driver/src/cy/assertions.ts @@ -171,7 +171,7 @@ export const create = (Cypress: ICypress, cy: $Cy) => { } else { obj.end = true obj.snapshot = true - obj.error = error + obj.error = _.clone(error) } const isChildLike = (subject, current) => { @@ -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, diff --git a/packages/driver/src/cy/commands/sessions/index.ts b/packages/driver/src/cy/commands/sessions/index.ts index d59c8eaf94db..e1fa34795d45 100644 --- a/packages/driver/src/cy/commands/sessions/index.ts +++ b/packages/driver/src/cy/commands/sessions/index.ts @@ -189,7 +189,7 @@ export default function (Commands, Cypress, cy) { setSessionLogStatus('failed') - $errUtils.modifyErrMsg(err, `\n\nThis error occurred while creating the 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 err }) @@ -264,7 +264,7 @@ export default function (Commands, Cypress, cy) { const enhanceErr = (err) => { Cypress.state('onQueueFailed', null) - if (!(err instanceof Error)) { + if (typeof err !== 'object') { err = new Error(err) } @@ -312,14 +312,14 @@ export default function (Commands, Cypress, cy) { } cy.state('onQueueFailed', (err, queue): Error => { - if (!(err instanceof Error)) { + if (typeof err !== 'object') { err = new Error(err) } if (step === 'restore') { const commands = queue.get() // determine command queue index of _commandToRunAfterValidation's index - const index = _.findIndex(commands, (command: any) => { + let index = _.findIndex(commands, (command: any) => { return ( _commandToRunAfterValidation && command.attributes.chainerId === _commandToRunAfterValidation.chainerId @@ -339,8 +339,11 @@ export default function (Commands, Cypress, cy) { // the session command kicked off Cypress.state('withinSubject', withinSubject) - // move to _commandToRunAfterValidation's index to ensure failures are handled correctly - queue.index = index + // 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 err.isRecovered = true @@ -352,9 +355,18 @@ export default function (Commands, Cypress, cy) { let returnVal - cy.then(() => { + try { returnVal = existingSession.validate.call(cy.state('ctx')) - }) + } catch (err) { + err.onFail = (err) => { + validateLog.set({ + error: err, + state: 'failed', + }) + } + + throw err + } _commandToRunAfterValidation = cy.then(async () => { Cypress.state('onQueueFailed', null) diff --git a/packages/driver/src/cypress/runner.ts b/packages/driver/src/cypress/runner.ts index 79b5301059be..b618adeeb57b 100644 --- a/packages/driver/src/cypress/runner.ts +++ b/packages/driver/src/cypress/runner.ts @@ -1520,8 +1520,6 @@ export default { // the test.run(fn) return null }).catch((err) => { - // once we complete callback with the - // original err next(err) // return null here to signal to bluebird diff --git a/packages/reporter/src/commands/command-model.ts b/packages/reporter/src/commands/command-model.ts index 9e0a94fcc37a..57b7a37cd042 100644 --- a/packages/reporter/src/commands/command-model.ts +++ b/packages/reporter/src/commands/command-model.ts @@ -120,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 dd80e3bb256b..92c387d86f1b 100644 --- a/packages/reporter/src/commands/command.tsx +++ b/packages/reporter/src/commands/command.tsx @@ -400,14 +400,14 @@ class Command extends Component { {this._children()} - {model.err?.isRecovered && ( + {model.showError && (
  • )} From 4fe9cf0ae1644bea7ea2fc00fa06d3986e63195f Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Fri, 4 Nov 2022 14:41:53 -0500 Subject: [PATCH 44/52] address PR feedback --- packages/driver/cypress/e2e/commands/files.cy.js | 16 +++++++++++----- .../cypress/e2e/commands/sessions/utils.cy.js | 2 +- .../driver/src/cy/commands/sessions/utils.ts | 2 +- packages/driver/src/cypress/stack_utils.ts | 4 ++-- packages/driver/src/cypress/utils.ts | 3 +-- packages/driver/src/util/queue.ts | 4 ---- 6 files changed, 16 insertions(+), 15 deletions(-) diff --git a/packages/driver/cypress/e2e/commands/files.cy.js b/packages/driver/cypress/e2e/commands/files.cy.js index e0d72ac37523..8648ee4bae28 100644 --- a/packages/driver/cypress/e2e/commands/files.cy.js +++ b/packages/driver/cypress/e2e/commands/files.cy.js @@ -161,10 +161,6 @@ describe('src/cy/commands/files', () => { defaultCommandTimeout: 50, }, () => { beforeEach(function () { - cy.visit('/fixtures/empty.html') - - this.logs = [] - const collectLogs = (attrs, log) => { if (attrs.name === 'readFile') { this.fileLog = log @@ -173,7 +169,13 @@ describe('src/cy/commands/files', () => { this.logs?.push(log) } - cy.on('log:added', collectLogs) + cy.visit('/fixtures/empty.html') + .then(() => { + cy.on('log:added', collectLogs) + }) + + this.logs = [] + cy.on('fail', () => { cy.off('log:added', collectLogs) }) @@ -185,6 +187,7 @@ describe('src/cy/commands/files', () => { cy.on('fail', (err) => { const { fileLog } = this + assertLogLength(this.logs, 1) 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`.') @@ -200,6 +203,7 @@ describe('src/cy/commands/files', () => { cy.on('fail', (err) => { const { fileLog } = this + assertLogLength(this.logs, 1) 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`.') @@ -215,6 +219,7 @@ describe('src/cy/commands/files', () => { cy.on('fail', (err) => { const { fileLog } = this + assertLogLength(this.logs, 1) 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: ``.') @@ -238,6 +243,7 @@ describe('src/cy/commands/files', () => { cy.on('fail', (err) => { const { fileLog } = this + assertLogLength(this.logs, 2) expect(fileLog.get('error')).to.eq(err) expect(fileLog.get('state')).to.eq('failed') expect(err.message).to.eq(stripIndent`\ diff --git a/packages/driver/cypress/e2e/commands/sessions/utils.cy.js b/packages/driver/cypress/e2e/commands/sessions/utils.cy.js index f9dbb4cc3aa0..1276a10fd10e 100644 --- a/packages/driver/cypress/e2e/commands/sessions/utils.cy.js +++ b/packages/driver/cypress/e2e/commands/sessions/utils.cy.js @@ -22,7 +22,7 @@ describe('src/cy/commands/sessions/utils.ts', () => { const consoleProps = getConsoleProps(sessionState) logForDebugging(consoleProps) - expect(consoleProps.Warning).to.eq('⚠️ There are no cookies, local storage or session storage associated to this session.') + 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.groups).to.have.length(0) }) diff --git a/packages/driver/src/cy/commands/sessions/utils.ts b/packages/driver/src/cy/commands/sessions/utils.ts index a069f9a48982..0c4e7ba3c017 100644 --- a/packages/driver/src/cy/commands/sessions/utils.ts +++ b/packages/driver/src/cy/commands/sessions/utils.ts @@ -131,7 +131,7 @@ const getConsoleProps = (session: SessionData) => { const props = { id: session.id, ...(!groupsByDomain.length && { - Warning: '⚠️ There are no cookies, local storage or session storage associated to this session.', + 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 ')}.`, diff --git a/packages/driver/src/cypress/stack_utils.ts b/packages/driver/src/cypress/stack_utils.ts index 459741d88c44..1b838f9b6404 100644 --- a/packages/driver/src/cypress/stack_utils.ts +++ b/packages/driver/src/cypress/stack_utils.ts @@ -482,7 +482,7 @@ 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 windowedStackLines = _.reject(stackLines, (line) => { + 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) @@ -494,7 +494,7 @@ const normalizedUserInvocationStack = (userInvocationStack) => { || line.includes('$Chainer.') }).join('\n') - return normalizeStackIndentation(windowedStackLines) + return normalizeStackIndentation(nonCypressStackLines) } export default { diff --git a/packages/driver/src/cypress/utils.ts b/packages/driver/src/cypress/utils.ts index a6c8956a6621..81d0915e6f35 100644 --- a/packages/driver/src/cypress/utils.ts +++ b/packages/driver/src/cypress/utils.ts @@ -397,7 +397,6 @@ export default { }, isPromiseLike (ret) { - // @ts-ignore - return ret && _.isObject(ret) && _.isFunction(ret.then) && _.isFunction(ret.catch) + 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 7fd2e9e832e2..77157d672bd1 100644 --- a/packages/driver/src/util/queue.ts +++ b/packages/driver/src/util/queue.ts @@ -54,10 +54,6 @@ export class Queue { this._stopped = true } - hasNext () { - return this.index < this.length - } - run ({ onRun, onError, onFinish }: QueueRunProps) { let inner let rejectOuterAndCancelInner From 2921590b628756deb3e070a652ad2cc0d5b56d27 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Mon, 7 Nov 2022 21:02:13 -0600 Subject: [PATCH 45/52] fix tests --- .../cypress/e2e/commands/assertions.cy.js | 18 +++++------ .../e2e/commands/querying/querying.cy.js | 2 +- packages/driver/src/cy/ensures.ts | 3 +- packages/driver/src/cy/retries.ts | 14 ++++----- packages/driver/src/cypress/command_queue.ts | 2 +- .../caught_uncaught_hook_errors_spec.js | 2 +- system-tests/__snapshots__/config_spec.js | 2 +- .../__snapshots__/issue_173_spec.ts.js | 2 +- system-tests/__snapshots__/session_spec.ts.js | 14 ++++----- .../__snapshots__/spec_isolation_spec.js | 8 ++--- system-tests/__snapshots__/stdout_spec.js | 2 +- .../vite_dev_server_fresh_spec.ts.js | 6 ++-- .../__snapshots__/web_security_spec.js | 4 +-- .../webpack_dev_server_fresh_spec.ts.js | 8 ++--- .../e2e/session/recreated_session.cy.js | 30 ------------------- 15 files changed, 42 insertions(+), 75 deletions(-) delete mode 100644 system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/recreated_session.cy.js diff --git a/packages/driver/cypress/e2e/commands/assertions.cy.js b/packages/driver/cypress/e2e/commands/assertions.cy.js index 6ce3dac18306..d28fb2886627 100644 --- a/packages/driver/cypress/e2e/commands/assertions.cy.js +++ b/packages/driver/cypress/e2e/commands/assertions.cy.js @@ -2177,6 +2177,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 @@ -2193,15 +2201,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/querying/querying.cy.js b/packages/driver/cypress/e2e/commands/querying/querying.cy.js index 6c6eda512dea..746d3e31562f 100644 --- a/packages/driver/cypress/e2e/commands/querying/querying.cy.js +++ b/packages/driver/cypress/e2e/commands/querying/querying.cy.js @@ -823,7 +823,7 @@ describe('src/cy/commands/querying', () => { 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.') + expect(err.message).to.include('could not find a registered alias for: `@alias`.\nYou have not aliased anything yet.') done() }) diff --git a/packages/driver/src/cy/ensures.ts b/packages/driver/src/cy/ensures.ts index a24bccf869e0..260e2a6393a5 100644 --- a/packages/driver/src/cy/ensures.ts +++ b/packages/driver/src/cy/ensures.ts @@ -252,7 +252,8 @@ export const create = (state: StateFunc, expect: $Cy['expect']) => { try { expect(subject).to.exist } catch (err) { - throw $errUtils.mergeErrProps(err, $errUtils.cypressErr(new Error(err.message))) + err.stack = $errUtils.stackWithReplacedProps(err, { name: 'CypressError' }) + throw $errUtils.mergeErrProps(err, { name: 'CypressError' }) } finally { state('onBeforeLog', null) } diff --git a/packages/driver/src/cy/retries.ts b/packages/driver/src/cy/retries.ts index bc5e6207864a..7338aeb5ac0d 100644 --- a/packages/driver/src/cy/retries.ts +++ b/packages/driver/src/cy/retries.ts @@ -74,15 +74,15 @@ export const create = (Cypress: ICypress, state: StateFunc, timeout: $Cy['timeou ({ error, onFail } = options) - const prependMsg = errByPath('miscellaneous.retry_timed_out', { - ms: options._runnableTimeout, - }).message + if (error) { + const prependMsg = errByPath('miscellaneous.retry_timed_out', { + ms: options._runnableTimeout, + }).message - const retryErrProps = modifyErrMsg(error, prependMsg, (msg1, msg2) => { - return `${msg2}${msg1}` - }) + const retryErrProps = modifyErrMsg(error, prependMsg, (msg1, msg2) => { + return `${msg2}${msg1}` + }) - if (error) { const retryErr = mergeErrProps(error, retryErrProps) throwErr(retryErr, { diff --git a/packages/driver/src/cypress/command_queue.ts b/packages/driver/src/cypress/command_queue.ts index 0510e307e3f6..b8b42012b803 100644 --- a/packages/driver/src/cypress/command_queue.ts +++ b/packages/driver/src/cypress/command_queue.ts @@ -195,7 +195,7 @@ 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) diff --git a/system-tests/__snapshots__/caught_uncaught_hook_errors_spec.js b/system-tests/__snapshots__/caught_uncaught_hook_errors_spec.js index ed52fe6edc4c..a0badb0eaf08 100644 --- a/system-tests/__snapshots__/caught_uncaught_hook_errors_spec.js +++ b/system-tests/__snapshots__/caught_uncaught_hook_errors_spec.js @@ -41,7 +41,7 @@ exports['e2e caught and uncaught hooks errors / failing1'] = ` 1) s1a "before each" hook for "t2a": - AssertionError: Timed out retrying after 100ms: Expected to find element: \`.does-not-exist\`, but never found it. + CypressError: Timed out retrying after 100ms: Expected to find element: \`.does-not-exist\`, but never found it. Because this error occurred during a \`before each\` hook we are skipping the remaining tests in the current suite: \`s1a\` [stack trace lines] diff --git a/system-tests/__snapshots__/config_spec.js b/system-tests/__snapshots__/config_spec.js index 1b3b34aed208..50beb3119e09 100644 --- a/system-tests/__snapshots__/config_spec.js +++ b/system-tests/__snapshots__/config_spec.js @@ -93,7 +93,7 @@ exports['e2e config applies defaultCommandTimeout globally 1'] = ` 1) short defaultCommandTimeout times out looking for a missing element: - AssertionError: Timed out retrying after 1000ms: Expected to find element: \`#bar\`, but never found it. + CypressError: Timed out retrying after 1000ms: Expected to find element: \`#bar\`, but never found it. [stack trace lines] diff --git a/system-tests/__snapshots__/issue_173_spec.ts.js b/system-tests/__snapshots__/issue_173_spec.ts.js index 885ccef6320e..4f2ca2d5a37d 100644 --- a/system-tests/__snapshots__/issue_173_spec.ts.js +++ b/system-tests/__snapshots__/issue_173_spec.ts.js @@ -24,7 +24,7 @@ exports['e2e issue 173 / failing'] = ` 1 failing 1) fails: - AssertionError: Timed out retrying after 200ms: Expected to find element: \`element_does_not_exist\`, but never found it. + CypressError: Timed out retrying after 200ms: Expected to find element: \`element_does_not_exist\`, but never found it. [stack trace lines] 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/__snapshots__/spec_isolation_spec.js b/system-tests/__snapshots__/spec_isolation_spec.js index 964ba207b200..a04099c95eb3 100644 --- a/system-tests/__snapshots__/spec_isolation_spec.js +++ b/system-tests/__snapshots__/spec_isolation_spec.js @@ -393,7 +393,7 @@ exports['e2e spec_isolation fails [electron] 1'] = { ], "state": "failed", "body": "() => {\n cy.wrap(true, {\n timeout: 100\n }).should('be.false');\n }", - "displayError": "AssertionError: Timed out retrying after 100ms: expected true to be false\n [stack trace lines]", + "displayError": "CypressError: Timed out retrying after 100ms: expected true to be false\n [stack trace lines]", "attempts": [ { "state": "failed", @@ -977,7 +977,7 @@ exports['e2e spec_isolation fails [chrome] 1'] = { ], "state": "failed", "body": "() => {\n cy.wrap(true, {\n timeout: 100\n }).should('be.false');\n }", - "displayError": "AssertionError: Timed out retrying after 100ms: expected true to be false\n [stack trace lines]", + "displayError": "CypressError: Timed out retrying after 100ms: expected true to be false\n [stack trace lines]", "attempts": [ { "state": "failed", @@ -1561,7 +1561,7 @@ exports['e2e spec_isolation fails [firefox] 1'] = { ], "state": "failed", "body": "() => {\n cy.wrap(true, {\n timeout: 100\n }).should('be.false');\n }", - "displayError": "AssertionError: Timed out retrying after 100ms: expected true to be false\n [stack trace lines]", + "displayError": "CypressError: Timed out retrying after 100ms: expected true to be false\n [stack trace lines]", "attempts": [ { "state": "failed", @@ -3903,7 +3903,7 @@ exports['e2e spec_isolation fails [webkit] 1'] = { ], "state": "failed", "body": "() => {\n cy.wrap(true, {\n timeout: 100\n }).should('be.false');\n }", - "displayError": "AssertionError: Timed out retrying after 100ms: expected true to be false\n [stack trace lines]", + "displayError": "CypressError: Timed out retrying after 100ms: expected true to be false\n [stack trace lines]", "attempts": [ { "state": "failed", diff --git a/system-tests/__snapshots__/stdout_spec.js b/system-tests/__snapshots__/stdout_spec.js index 4293b5e75298..8794bf053035 100644 --- a/system-tests/__snapshots__/stdout_spec.js +++ b/system-tests/__snapshots__/stdout_spec.js @@ -504,7 +504,7 @@ exports['e2e stdout / displays assertion errors'] = ` 4) assertion errors fails with dom assertion without diff, with retries: - AssertionError: Timed out retrying after 4000ms: expected '' to have class 'foo' + CypressError: Timed out retrying after 4000ms: expected '' to have class 'foo' [stack trace lines] diff --git a/system-tests/__snapshots__/vite_dev_server_fresh_spec.ts.js b/system-tests/__snapshots__/vite_dev_server_fresh_spec.ts.js index 8e0d03dcf76a..80e7ae89096e 100644 --- a/system-tests/__snapshots__/vite_dev_server_fresh_spec.ts.js +++ b/system-tests/__snapshots__/vite_dev_server_fresh_spec.ts.js @@ -153,7 +153,7 @@ https://on.cypress.io/uncaught-exception-from-application 4) Errors command failure: - AssertionError: Timed out retrying after 50ms: Expected to find element: \`element-that-does-not-exist\`, but never found it. + CypressError: Timed out retrying after 50ms: Expected to find element: \`element-that-does-not-exist\`, but never found it. [stack trace lines] @@ -575,7 +575,7 @@ https://on.cypress.io/uncaught-exception-from-application 4) Errors command failure: - AssertionError: Timed out retrying after 50ms: Expected to find element: \`element-that-does-not-exist\`, but never found it. + CypressError: Timed out retrying after 50ms: Expected to find element: \`element-that-does-not-exist\`, but never found it. [stack trace lines] @@ -997,7 +997,7 @@ https://on.cypress.io/uncaught-exception-from-application 4) Errors command failure: - AssertionError: Timed out retrying after 50ms: Expected to find element: \`element-that-does-not-exist\`, but never found it. + CypressError: Timed out retrying after 50ms: Expected to find element: \`element-that-does-not-exist\`, but never found it. [stack trace lines] diff --git a/system-tests/__snapshots__/web_security_spec.js b/system-tests/__snapshots__/web_security_spec.js index 0b9631948213..bcb84d1b0d83 100644 --- a/system-tests/__snapshots__/web_security_spec.js +++ b/system-tests/__snapshots__/web_security_spec.js @@ -100,7 +100,7 @@ https://on.cypress.io/cross-origin-violation 4) web security fails when doing a CORS request cross-origin: - AssertionError: Timed out retrying after 500ms: Expected to find content: 'success!' but never did. + CypressError: Timed out retrying after 500ms: Expected to find content: 'success!' but never did. [stack trace lines] @@ -347,7 +347,7 @@ exports['e2e web security / when experimentalSessionAndOrigin is enabled / fails 4) web security fails when doing a CORS request cross-origin: - AssertionError: Timed out retrying after 500ms: Expected to find content: 'success!' but never did. + CypressError: Timed out retrying after 500ms: Expected to find content: 'success!' but never did. [stack trace lines] diff --git a/system-tests/__snapshots__/webpack_dev_server_fresh_spec.ts.js b/system-tests/__snapshots__/webpack_dev_server_fresh_spec.ts.js index b6822aac2175..deeac4211531 100644 --- a/system-tests/__snapshots__/webpack_dev_server_fresh_spec.ts.js +++ b/system-tests/__snapshots__/webpack_dev_server_fresh_spec.ts.js @@ -164,7 +164,7 @@ https://on.cypress.io/uncaught-exception-from-application 4) Errors command failure: - AssertionError: Timed out retrying after 50ms: Expected to find element: \`element-that-does-not-exist\`, but never found it. + CypressError: Timed out retrying after 50ms: Expected to find element: \`element-that-does-not-exist\`, but never found it. [stack trace lines] @@ -606,7 +606,7 @@ https://on.cypress.io/uncaught-exception-from-application 4) Errors command failure: - AssertionError: Timed out retrying after 50ms: Expected to find element: \`element-that-does-not-exist\`, but never found it. + CypressError: Timed out retrying after 50ms: Expected to find element: \`element-that-does-not-exist\`, but never found it. [stack trace lines] @@ -1039,7 +1039,7 @@ https://on.cypress.io/uncaught-exception-from-application 4) Errors command failure: - AssertionError: Timed out retrying after 50ms: Expected to find element: \`element-that-does-not-exist\`, but never found it. + CypressError: Timed out retrying after 50ms: Expected to find element: \`element-that-does-not-exist\`, but never found it. [stack trace lines] @@ -1484,7 +1484,7 @@ https://on.cypress.io/uncaught-exception-from-application 4) Errors command failure: - AssertionError: Timed out retrying after 50ms: Expected to find element: \`element-that-does-not-exist\`, but never found it. + CypressError: Timed out retrying after 50ms: Expected to find element: \`element-that-does-not-exist\`, but never found it. [stack trace lines] 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) -}) From 3f2e50d23ad025623d7f93f6863cca98a8d02908 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Tue, 8 Nov 2022 07:48:05 -0600 Subject: [PATCH 46/52] maintain error message parity with current cypress behavior. --- .../cypress/e2e/commands/sessions/sessions.cy.js | 2 +- packages/driver/src/cy/ensures.ts | 3 --- packages/driver/src/cy/retries.ts | 14 +++++++------- .../caught_uncaught_hook_errors_spec.js | 2 +- system-tests/__snapshots__/config_spec.js | 2 +- system-tests/__snapshots__/issue_173_spec.ts.js | 2 +- system-tests/__snapshots__/spec_isolation_spec.js | 8 ++++---- system-tests/__snapshots__/stdout_spec.js | 2 +- .../__snapshots__/vite_dev_server_fresh_spec.ts.js | 6 +++--- system-tests/__snapshots__/web_security_spec.js | 4 ++-- .../webpack_dev_server_fresh_spec.ts.js | 8 ++++---- 11 files changed, 25 insertions(+), 28 deletions(-) diff --git a/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js b/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js index 99dda1961d61..5496293f081a 100644 --- a/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js +++ b/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js @@ -1613,7 +1613,7 @@ 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 the session. Because the session setup failed, we failed the test.') expect(lastSessionLog.get('state')).to.eq('failed') diff --git a/packages/driver/src/cy/ensures.ts b/packages/driver/src/cy/ensures.ts index 260e2a6393a5..107541854512 100644 --- a/packages/driver/src/cy/ensures.ts +++ b/packages/driver/src/cy/ensures.ts @@ -251,9 +251,6 @@ export const create = (state: StateFunc, expect: $Cy['expect']) => { // verify the $el exists and use our default error messages try { expect(subject).to.exist - } catch (err) { - err.stack = $errUtils.stackWithReplacedProps(err, { name: 'CypressError' }) - throw $errUtils.mergeErrProps(err, { name: 'CypressError' }) } finally { state('onBeforeLog', null) } diff --git a/packages/driver/src/cy/retries.ts b/packages/driver/src/cy/retries.ts index 7338aeb5ac0d..bc5e6207864a 100644 --- a/packages/driver/src/cy/retries.ts +++ b/packages/driver/src/cy/retries.ts @@ -74,15 +74,15 @@ export const create = (Cypress: ICypress, state: StateFunc, timeout: $Cy['timeou ({ error, onFail } = options) - if (error) { - const prependMsg = errByPath('miscellaneous.retry_timed_out', { - ms: options._runnableTimeout, - }).message + const prependMsg = errByPath('miscellaneous.retry_timed_out', { + ms: options._runnableTimeout, + }).message - const retryErrProps = modifyErrMsg(error, prependMsg, (msg1, msg2) => { - return `${msg2}${msg1}` - }) + const retryErrProps = modifyErrMsg(error, prependMsg, (msg1, msg2) => { + return `${msg2}${msg1}` + }) + if (error) { const retryErr = mergeErrProps(error, retryErrProps) throwErr(retryErr, { diff --git a/system-tests/__snapshots__/caught_uncaught_hook_errors_spec.js b/system-tests/__snapshots__/caught_uncaught_hook_errors_spec.js index a0badb0eaf08..ed52fe6edc4c 100644 --- a/system-tests/__snapshots__/caught_uncaught_hook_errors_spec.js +++ b/system-tests/__snapshots__/caught_uncaught_hook_errors_spec.js @@ -41,7 +41,7 @@ exports['e2e caught and uncaught hooks errors / failing1'] = ` 1) s1a "before each" hook for "t2a": - CypressError: Timed out retrying after 100ms: Expected to find element: \`.does-not-exist\`, but never found it. + AssertionError: Timed out retrying after 100ms: Expected to find element: \`.does-not-exist\`, but never found it. Because this error occurred during a \`before each\` hook we are skipping the remaining tests in the current suite: \`s1a\` [stack trace lines] diff --git a/system-tests/__snapshots__/config_spec.js b/system-tests/__snapshots__/config_spec.js index 50beb3119e09..1b3b34aed208 100644 --- a/system-tests/__snapshots__/config_spec.js +++ b/system-tests/__snapshots__/config_spec.js @@ -93,7 +93,7 @@ exports['e2e config applies defaultCommandTimeout globally 1'] = ` 1) short defaultCommandTimeout times out looking for a missing element: - CypressError: Timed out retrying after 1000ms: Expected to find element: \`#bar\`, but never found it. + AssertionError: Timed out retrying after 1000ms: Expected to find element: \`#bar\`, but never found it. [stack trace lines] diff --git a/system-tests/__snapshots__/issue_173_spec.ts.js b/system-tests/__snapshots__/issue_173_spec.ts.js index 4f2ca2d5a37d..885ccef6320e 100644 --- a/system-tests/__snapshots__/issue_173_spec.ts.js +++ b/system-tests/__snapshots__/issue_173_spec.ts.js @@ -24,7 +24,7 @@ exports['e2e issue 173 / failing'] = ` 1 failing 1) fails: - CypressError: Timed out retrying after 200ms: Expected to find element: \`element_does_not_exist\`, but never found it. + AssertionError: Timed out retrying after 200ms: Expected to find element: \`element_does_not_exist\`, but never found it. [stack trace lines] diff --git a/system-tests/__snapshots__/spec_isolation_spec.js b/system-tests/__snapshots__/spec_isolation_spec.js index a04099c95eb3..964ba207b200 100644 --- a/system-tests/__snapshots__/spec_isolation_spec.js +++ b/system-tests/__snapshots__/spec_isolation_spec.js @@ -393,7 +393,7 @@ exports['e2e spec_isolation fails [electron] 1'] = { ], "state": "failed", "body": "() => {\n cy.wrap(true, {\n timeout: 100\n }).should('be.false');\n }", - "displayError": "CypressError: Timed out retrying after 100ms: expected true to be false\n [stack trace lines]", + "displayError": "AssertionError: Timed out retrying after 100ms: expected true to be false\n [stack trace lines]", "attempts": [ { "state": "failed", @@ -977,7 +977,7 @@ exports['e2e spec_isolation fails [chrome] 1'] = { ], "state": "failed", "body": "() => {\n cy.wrap(true, {\n timeout: 100\n }).should('be.false');\n }", - "displayError": "CypressError: Timed out retrying after 100ms: expected true to be false\n [stack trace lines]", + "displayError": "AssertionError: Timed out retrying after 100ms: expected true to be false\n [stack trace lines]", "attempts": [ { "state": "failed", @@ -1561,7 +1561,7 @@ exports['e2e spec_isolation fails [firefox] 1'] = { ], "state": "failed", "body": "() => {\n cy.wrap(true, {\n timeout: 100\n }).should('be.false');\n }", - "displayError": "CypressError: Timed out retrying after 100ms: expected true to be false\n [stack trace lines]", + "displayError": "AssertionError: Timed out retrying after 100ms: expected true to be false\n [stack trace lines]", "attempts": [ { "state": "failed", @@ -3903,7 +3903,7 @@ exports['e2e spec_isolation fails [webkit] 1'] = { ], "state": "failed", "body": "() => {\n cy.wrap(true, {\n timeout: 100\n }).should('be.false');\n }", - "displayError": "CypressError: Timed out retrying after 100ms: expected true to be false\n [stack trace lines]", + "displayError": "AssertionError: Timed out retrying after 100ms: expected true to be false\n [stack trace lines]", "attempts": [ { "state": "failed", diff --git a/system-tests/__snapshots__/stdout_spec.js b/system-tests/__snapshots__/stdout_spec.js index 8794bf053035..4293b5e75298 100644 --- a/system-tests/__snapshots__/stdout_spec.js +++ b/system-tests/__snapshots__/stdout_spec.js @@ -504,7 +504,7 @@ exports['e2e stdout / displays assertion errors'] = ` 4) assertion errors fails with dom assertion without diff, with retries: - CypressError: Timed out retrying after 4000ms: expected '' to have class 'foo' + AssertionError: Timed out retrying after 4000ms: expected '' to have class 'foo' [stack trace lines] diff --git a/system-tests/__snapshots__/vite_dev_server_fresh_spec.ts.js b/system-tests/__snapshots__/vite_dev_server_fresh_spec.ts.js index 80e7ae89096e..8e0d03dcf76a 100644 --- a/system-tests/__snapshots__/vite_dev_server_fresh_spec.ts.js +++ b/system-tests/__snapshots__/vite_dev_server_fresh_spec.ts.js @@ -153,7 +153,7 @@ https://on.cypress.io/uncaught-exception-from-application 4) Errors command failure: - CypressError: Timed out retrying after 50ms: Expected to find element: \`element-that-does-not-exist\`, but never found it. + AssertionError: Timed out retrying after 50ms: Expected to find element: \`element-that-does-not-exist\`, but never found it. [stack trace lines] @@ -575,7 +575,7 @@ https://on.cypress.io/uncaught-exception-from-application 4) Errors command failure: - CypressError: Timed out retrying after 50ms: Expected to find element: \`element-that-does-not-exist\`, but never found it. + AssertionError: Timed out retrying after 50ms: Expected to find element: \`element-that-does-not-exist\`, but never found it. [stack trace lines] @@ -997,7 +997,7 @@ https://on.cypress.io/uncaught-exception-from-application 4) Errors command failure: - CypressError: Timed out retrying after 50ms: Expected to find element: \`element-that-does-not-exist\`, but never found it. + AssertionError: Timed out retrying after 50ms: Expected to find element: \`element-that-does-not-exist\`, but never found it. [stack trace lines] diff --git a/system-tests/__snapshots__/web_security_spec.js b/system-tests/__snapshots__/web_security_spec.js index bcb84d1b0d83..0b9631948213 100644 --- a/system-tests/__snapshots__/web_security_spec.js +++ b/system-tests/__snapshots__/web_security_spec.js @@ -100,7 +100,7 @@ https://on.cypress.io/cross-origin-violation 4) web security fails when doing a CORS request cross-origin: - CypressError: Timed out retrying after 500ms: Expected to find content: 'success!' but never did. + AssertionError: Timed out retrying after 500ms: Expected to find content: 'success!' but never did. [stack trace lines] @@ -347,7 +347,7 @@ exports['e2e web security / when experimentalSessionAndOrigin is enabled / fails 4) web security fails when doing a CORS request cross-origin: - CypressError: Timed out retrying after 500ms: Expected to find content: 'success!' but never did. + AssertionError: Timed out retrying after 500ms: Expected to find content: 'success!' but never did. [stack trace lines] diff --git a/system-tests/__snapshots__/webpack_dev_server_fresh_spec.ts.js b/system-tests/__snapshots__/webpack_dev_server_fresh_spec.ts.js index deeac4211531..b6822aac2175 100644 --- a/system-tests/__snapshots__/webpack_dev_server_fresh_spec.ts.js +++ b/system-tests/__snapshots__/webpack_dev_server_fresh_spec.ts.js @@ -164,7 +164,7 @@ https://on.cypress.io/uncaught-exception-from-application 4) Errors command failure: - CypressError: Timed out retrying after 50ms: Expected to find element: \`element-that-does-not-exist\`, but never found it. + AssertionError: Timed out retrying after 50ms: Expected to find element: \`element-that-does-not-exist\`, but never found it. [stack trace lines] @@ -606,7 +606,7 @@ https://on.cypress.io/uncaught-exception-from-application 4) Errors command failure: - CypressError: Timed out retrying after 50ms: Expected to find element: \`element-that-does-not-exist\`, but never found it. + AssertionError: Timed out retrying after 50ms: Expected to find element: \`element-that-does-not-exist\`, but never found it. [stack trace lines] @@ -1039,7 +1039,7 @@ https://on.cypress.io/uncaught-exception-from-application 4) Errors command failure: - CypressError: Timed out retrying after 50ms: Expected to find element: \`element-that-does-not-exist\`, but never found it. + AssertionError: Timed out retrying after 50ms: Expected to find element: \`element-that-does-not-exist\`, but never found it. [stack trace lines] @@ -1484,7 +1484,7 @@ https://on.cypress.io/uncaught-exception-from-application 4) Errors command failure: - CypressError: Timed out retrying after 50ms: Expected to find element: \`element-that-does-not-exist\`, but never found it. + AssertionError: Timed out retrying after 50ms: Expected to find element: \`element-that-does-not-exist\`, but never found it. [stack trace lines] From 1f879676f0deeb85c0f151c9a2154fee8e6f2627 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Tue, 8 Nov 2022 08:17:09 -0600 Subject: [PATCH 47/52] fix --- .../e2e/commands/querying/querying.cy.js | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/driver/cypress/e2e/commands/querying/querying.cy.js b/packages/driver/cypress/e2e/commands/querying/querying.cy.js index 746d3e31562f..05266055a31e 100644 --- a/packages/driver/cypress/e2e/commands/querying/querying.cy.js +++ b/packages/driver/cypress/e2e/commands/querying/querying.cy.js @@ -789,7 +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('CypressError') + expect(err.name).to.include('AssertionError') expect(err.message).to.include('Expected to find element: `#missing-el`, but never found it.') done() @@ -800,7 +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('CypressError') + expect(err.name).to.include('AssertionError') expect(err.message).to.include('Expected to find element: `#missing-el`, but never found it.') done() @@ -811,7 +811,7 @@ describe('src/cy/commands/querying', () => { it('throws existence error without running assertions', (done) => { cy.on('fail', (err) => { - expect(err.name).to.include('CypressError') + expect(err.name).to.include('AssertionError') expect(err.message).to.include('Expected to find element: `#missing-el`, but never found it.') done() @@ -833,7 +833,7 @@ describe('src/cy/commands/querying', () => { it('throws after timing out after a .wait() alias reference', (done) => { cy.on('fail', (err) => { - expect(err.name).to.eq('CypressError') + expect(err.name).to.eq('AssertionError') expect(err.message).to.include('Expected to find element: `getJsonButton`, but never found it.') done() @@ -883,7 +883,7 @@ describe('src/cy/commands/querying', () => { cy.on('fail', (err) => { const { lastLog } = this - expect(err.name).to.eq('CypressError') + 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') @@ -901,7 +901,7 @@ describe('src/cy/commands/querying', () => { const { logs, lastLog } = this const getLog = logs[logs.length - 2] - expect(err.name).to.eq('CypressError') + 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') @@ -1795,7 +1795,7 @@ space it('throws when there is no filter and no subject', (done) => { cy.on('fail', (err) => { - expect(err.name).to.eq('CypressError') + expect(err.name).to.eq('AssertionError') expect(err.message).to.include('Expected to find content: \'brand new content\' but never did.') done() @@ -1806,7 +1806,7 @@ space it('throws when there is a filter', (done) => { cy.on('fail', (err) => { - expect(err.name).to.eq('CypressError') + 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() @@ -1817,7 +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('CypressError') + expect(err.name).to.eq('AssertionError') expect(err.message).to.include('Expected to find content: \'0\' within the element: but never did.') done() @@ -1828,7 +1828,7 @@ space it('throws when there is both a subject and a filter', (done) => { cy.on('fail', (err) => { - expect(err.name).to.eq('CypressError') + 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() From e1131049a3742ab7c7d4532f07031eeeeac8546b Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Tue, 8 Nov 2022 09:13:33 -0600 Subject: [PATCH 48/52] fix --- .../driver/cypress/e2e/commands/assertions.cy.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/driver/cypress/e2e/commands/assertions.cy.js b/packages/driver/cypress/e2e/commands/assertions.cy.js index d28fb2886627..fb7b43ad7f57 100644 --- a/packages/driver/cypress/e2e/commands/assertions.cy.js +++ b/packages/driver/cypress/e2e/commands/assertions.cy.js @@ -1023,14 +1023,13 @@ describe('src/cy/commands/assertions', () => { cy.on('log:added', (attrs, log) => { if (attrs.name === 'assert') { cy.removeAllListeners('log:added') + const consoleProps = log.invoke('consoleProps') - expect(log.invoke('consoleProps')).to.deep.eq({ - Command: 'assert', - expected: false, - actual: true, - Message: 'expected true to be false', - Error: log.get('error').message, - }) + expect(consoleProps.Command).to.eq('assert') + expect(consoleProps.expected).to.be.false + expect(consoleProps.actual).to.be.false + expect(consoleProps.Message).to.eq('expected true to be false') + expect(consoleProps.Error).to.include(log.get('error').message) done() } From d1eee3a14b2eca5eff25b955046a2e008b666b52 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Tue, 8 Nov 2022 09:18:42 -0600 Subject: [PATCH 49/52] fix test --- .../cypress/e2e/commands/assertions.cy.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/driver/cypress/e2e/commands/assertions.cy.js b/packages/driver/cypress/e2e/commands/assertions.cy.js index fb7b43ad7f57..b26e0024b485 100644 --- a/packages/driver/cypress/e2e/commands/assertions.cy.js +++ b/packages/driver/cypress/e2e/commands/assertions.cy.js @@ -1024,14 +1024,19 @@ describe('src/cy/commands/assertions', () => { if (attrs.name === 'assert') { cy.removeAllListeners('log:added') const consoleProps = log.invoke('consoleProps') + let err - expect(consoleProps.Command).to.eq('assert') - expect(consoleProps.expected).to.be.false - expect(consoleProps.actual).to.be.false - expect(consoleProps.Message).to.eq('expected true to be false') - expect(consoleProps.Error).to.include(log.get('error').message) - - done() + // FIXME: workaround for https://github.com/cypress-io/cypress/issues/4742 + try { + expect(consoleProps.Command).to.eq('assert') + expect(consoleProps.expected).to.be.false + expect(consoleProps.actual).to.be.true + expect(consoleProps.Message).to.eq('expected true to be false') + expect(consoleProps.Error).to.include(log.get('error').message) + } catch (e) { + err = e + } + done(err) } }) From 2a7bb06023212c5ef7537e2bd383fb21a2894c87 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Tue, 8 Nov 2022 09:58:02 -0600 Subject: [PATCH 50/52] correct types --- cli/types/cypress.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/types/cypress.d.ts b/cli/types/cypress.d.ts index 88e1d23f0444..48d28306029c 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 From 1d52b052299d911dfbdaa415f7a7053c306c1805 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Tue, 8 Nov 2022 11:42:43 -0600 Subject: [PATCH 51/52] fix test for webkit & firefox --- packages/app/cypress/e2e/runner/sessions.ui.cy.ts | 2 +- .../driver/cypress/e2e/commands/assertions.cy.js | 15 ++++++++------- packages/driver/src/cy/assertions.ts | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/app/cypress/e2e/runner/sessions.ui.cy.ts b/packages/app/cypress/e2e/runner/sessions.ui.cy.ts index 45fc975c8d93..46c2a797787f 100644 --- a/packages/app/cypress/e2e/runner/sessions.ui.cy.ts +++ b/packages/app/cypress/e2e/runner/sessions.ui.cy.ts @@ -236,7 +236,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() diff --git a/packages/driver/cypress/e2e/commands/assertions.cy.js b/packages/driver/cypress/e2e/commands/assertions.cy.js index b26e0024b485..a8bb3e80de67 100644 --- a/packages/driver/cypress/e2e/commands/assertions.cy.js +++ b/packages/driver/cypress/e2e/commands/assertions.cy.js @@ -1023,19 +1023,20 @@ describe('src/cy/commands/assertions', () => { cy.on('log:added', (attrs, log) => { if (attrs.name === 'assert') { cy.removeAllListeners('log:added') - const consoleProps = log.invoke('consoleProps') let err - // FIXME: workaround for https://github.com/cypress-io/cypress/issues/4742 try { - expect(consoleProps.Command).to.eq('assert') - expect(consoleProps.expected).to.be.false - expect(consoleProps.actual).to.be.true - expect(consoleProps.Message).to.eq('expected true to be false') - expect(consoleProps.Error).to.include(log.get('error').message) + expect(log.invoke('consoleProps')).to.deep.eq({ + Command: 'assert', + expected: false, + actual: true, + Message: 'expected true to be false', + Error: log.get('error').message, + }) } catch (e) { err = e } + done(err) } }) diff --git a/packages/driver/src/cy/assertions.ts b/packages/driver/src/cy/assertions.ts index e655000643ba..5a19adac5668 100644 --- a/packages/driver/src/cy/assertions.ts +++ b/packages/driver/src/cy/assertions.ts @@ -171,7 +171,7 @@ export const create = (Cypress: ICypress, cy: $Cy) => { } else { obj.end = true obj.snapshot = true - obj.error = _.clone(error) + obj.error = error } const isChildLike = (subject, current) => { From 919dd4aa18857d55e0ccdfa1651cb41815cb1535 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Tue, 8 Nov 2022 12:51:23 -0600 Subject: [PATCH 52/52] fix. --- packages/driver/cypress/e2e/commands/assertions.cy.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/driver/cypress/e2e/commands/assertions.cy.js b/packages/driver/cypress/e2e/commands/assertions.cy.js index a8bb3e80de67..b4048daab435 100644 --- a/packages/driver/cypress/e2e/commands/assertions.cy.js +++ b/packages/driver/cypress/e2e/commands/assertions.cy.js @@ -1026,12 +1026,12 @@ describe('src/cy/commands/assertions', () => { let err try { - expect(log.invoke('consoleProps')).to.deep.eq({ + expect(log.invoke('consoleProps')).to.deep.contain({ Command: 'assert', expected: false, actual: true, Message: 'expected true to be false', - Error: log.get('error').message, + Error: log.get('error').stack, }) } catch (e) { err = e