Skip to content

Commit

Permalink
fix: log error on reject with string content (#25059)
Browse files Browse the repository at this point in the history
Co-authored-by: Chris Breiding <chrisbreiding@users.noreply.github.com>
Co-authored-by: Chris Breiding <chrisbreiding@gmail.com>
Closes undefined
  • Loading branch information
geritol authored Dec 19, 2022
1 parent 166b694 commit 470b94b
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 17 deletions.
12 changes: 12 additions & 0 deletions packages/app/cypress/e2e/runner/reporter.errors.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,18 @@ describe('errors ui', {
],
})

verify('spec unhandled rejection with string content', {
uncaught: true,
column: 20,
originalMessage: 'Unhandled promise rejection with string content from the spec',
message: [
'The following error originated from your test code',
'It was caused by an unhandled promise rejection',
],
stackRegex: /.*/,
hasCodeFrame: false,
})

verify('spec unhandled rejection with done', {
uncaught: true,
column: 20,
Expand Down
27 changes: 26 additions & 1 deletion packages/driver/cypress/component/spec.cy.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const { sinon } = Cypress

describe('component testing', () => {
/** @type {Cypress.Agent<sinon.SinonSpy>} */
let uncaughtExceptionStub
Expand All @@ -12,17 +14,40 @@ describe('component testing', () => {
})
})

beforeEach(() => {
uncaughtExceptionStub.resetHistory()
document.querySelector('[data-cy-root]').innerHTML = ''
})

it('fails and shows an error', () => {
cy.spy(Cypress, 'log').log(false)
const $el = document.createElement('button')

$el.innerText = `Don't click it!`
$el.addEventListener('click', () => {
throw Error('An error!')
throw new Error('An error!')
})

document.querySelector('[data-cy-root]').appendChild($el)
cy.get('button').click().then(() => {
expect(uncaughtExceptionStub).to.have.been.calledOnceWithExactly(null)
expect(Cypress.log).to.be.calledWithMatch(sinon.match({ 'message': `Error: An error!`, name: 'uncaught exception' }))
})
})

it('fails and shows when a promise rejects with a string', () => {
cy.spy(Cypress, 'log').log(false)
const $el = document.createElement('button')

$el.innerText = `Don't click it!`
$el.addEventListener('click', new Promise((_, reject) => {
reject('Promise rejected with a string!')
}))

document.querySelector('[data-cy-root]').appendChild($el)
cy.get('button').click().then(() => {
expect(uncaughtExceptionStub).to.have.been.calledOnceWithExactly(null)
expect(Cypress.log).to.be.calledWithMatch(sinon.match({ 'message': `Error: "Promise rejected with a string!"`, name: 'uncaught exception' }))
})
})
})
36 changes: 34 additions & 2 deletions packages/driver/cypress/e2e/cypress/error_utils.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import $stackUtils from '@packages/driver/src/cypress/stack_utils'
import $errUtils, { CypressError } from '@packages/driver/src/cypress/error_utils'
import $errorMessages from '@packages/driver/src/cypress/error_messages'

const { sinon } = Cypress

describe('driver/src/cypress/error_utils', () => {
context('.modifyErrMsg', () => {
let originalErr
Expand Down Expand Up @@ -90,7 +92,7 @@ describe('driver/src/cypress/error_utils', () => {
})

it('attaches onFail to the error when it is a function', () => {
const onFail = function () {}
const onFail = function () { }
const fn = () => $errUtils.throwErr(new Error('foo'), { onFail })

expect(fn).throw().and.satisfy((err) => {
Expand Down Expand Up @@ -561,7 +563,7 @@ describe('driver/src/cypress/error_utils', () => {

it('does not error if no last log', () => {
state.returns({
getLastLog: () => {},
getLastLog: () => { },
})

const result = $errUtils.createUncaughtException({
Expand Down Expand Up @@ -660,4 +662,34 @@ describe('driver/src/cypress/error_utils', () => {
expect(unsupportedPlugin).to.eq(null)
})
})

context('.logError', () => {
let cypressMock

beforeEach(() => {
cypressMock = {
log: cy.stub(),
}
})

it('calls Cypress.log with error name and message when error is instance of Error', () => {
$errUtils.logError(cypressMock, 'error', new Error('Some error'))
expect(cypressMock.log).to.have.been.calledWithMatch(sinon.match.has('message', `Error: Some error`))
})

it('calls Cypress.log with error name and message when error a string', () => {
$errUtils.logError(cypressMock, 'error', 'Some string error')
expect(cypressMock.log).to.have.been.calledWithMatch(sinon.match.has('message', `Error: \"Some string error\"`))
})

it('calls Cypress.log with default error name and provided message message when error is an object with a message', () => {
$errUtils.logError(cypressMock, 'error', { message: 'Some object error with message' })
expect(cypressMock.log).to.have.been.calledWithMatch(sinon.match.has('message', `Error: Some object error with message`))
})

it('calls Cypress.log with error name and message when error is an object', () => {
$errUtils.logError(cypressMock, 'error', { err: 'Error details' })
expect(cypressMock.log).to.have.been.calledWithMatch(sinon.match.has('message', `Error: {"err":"Error details"}`))
})
})
})
25 changes: 13 additions & 12 deletions packages/driver/src/cypress/cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,8 @@ const setTopOnError = function (Cypress, cy: $Cy) {

// prevent Mocha from setting top.onerror
Object.defineProperty(top, 'onerror', {
set () {},
get () {},
set () { },
get () { },
configurable: false,
enumerable: true,
})
Expand All @@ -131,12 +131,12 @@ const ensureRunnable = (cy, cmd) => {
interface ICyFocused extends Omit<
IFocused,
'documentHasFocus' | 'interceptFocus' | 'interceptBlur'
> {}
> { }

interface ICySnapshots extends Omit<
ISnapshots,
'onCssModified' | 'onBeforeWindowLoad'
> {}
> { }

export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILocation, ITimer, IChai, IXhr, IAliases, ICySnapshots, ICyFocused {
id: string
Expand Down Expand Up @@ -505,16 +505,16 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert
// If the runner can communicate, we should setup all events, otherwise just setup the window and fire the load event.
if (isRunnerAbleToCommunicateWithAUT) {
if (this.Cypress.isBrowser('webkit')) {
// WebKit's unhandledrejection event will sometimes not fire within the AUT
// due to a documented bug: https://bugs.webkit.org/show_bug.cgi?id=187822
// To ensure that the event will always fire (and always report these
// unhandled rejections to the user), we patch the AUT's Error constructor
// to enqueue a no-op microtask when executed, which ensures that the unhandledrejection
// event handler will be executed if this Error is uncaught.
// WebKit's unhandledrejection event will sometimes not fire within the AUT
// due to a documented bug: https://bugs.webkit.org/show_bug.cgi?id=187822
// To ensure that the event will always fire (and always report these
// unhandled rejections to the user), we patch the AUT's Error constructor
// to enqueue a no-op microtask when executed, which ensures that the unhandledrejection
// event handler will be executed if this Error is uncaught.
const originalError = autWindow.Error

autWindow.Error = function __CyWebKitError (...args) {
autWindow.queueMicrotask(() => {})
autWindow.queueMicrotask(() => { })

return originalError.apply(this, args)
}
Expand Down Expand Up @@ -1059,6 +1059,7 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert
// eslint-disable-next-line @cypress/dev/arrow-body-multiline-braces
onError: (handlerType) => (event) => {
const { originalErr, err, promise } = $errUtils.errorFromUncaughtEvent(handlerType, event) as ErrorFromProjectRejectionEvent

const handled = cy.onUncaughtException({
err,
promise,
Expand All @@ -1080,7 +1081,7 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert
onSubmit (e) {
return cy.Cypress.action('app:form:submitted', e)
},
onLoad () {},
onLoad () { },
onBeforeUnload (e) {
cy.isStable(false, 'beforeunload')

Expand Down
29 changes: 27 additions & 2 deletions packages/driver/src/cypress/error_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,10 @@ const appendErrMsg = (err, errMsg) => {
}

const makeErrFromObj = (obj) => {
if (_.isString(obj)) {
return new Error(obj)
}

const err2 = new Error(obj.message)

err2.name = obj.name
Expand Down Expand Up @@ -549,9 +553,11 @@ const errorFromUncaughtEvent = (handlerType: HandlerType, event) => {
errorFromProjectRejectionEvent(event)
}

const logError = (Cypress, handlerType: HandlerType, err, handled = false) => {
const logError = (Cypress, handlerType: HandlerType, err: unknown, handled = false) => {
const error = toLoggableError(err)

Cypress.log({
message: `${err.name}: ${err.message}`,
message: `${error.name || 'Error'}: ${error.message}`,
name: 'uncaught exception',
type: 'parent',
// specifying the error causes the log to be red/failed
Expand All @@ -572,6 +578,25 @@ const logError = (Cypress, handlerType: HandlerType, err, handled = false) => {
})
}

interface LoggableError { name?: string, message: string }

const isLoggableError = (error: unknown): error is LoggableError => {
return (
typeof error === 'object' &&
error !== null &&
'message' in error)
}

const toLoggableError = (maybeError: unknown): LoggableError => {
if (isLoggableError(maybeError)) return maybeError

try {
return { message: JSON.stringify(maybeError) }
} catch {
return { message: String(maybeError) }
}
}

const getUnsupportedPlugin = (runnable) => {
if (!(runnable.invocationDetails && runnable.invocationDetails.originalFile && runnable.err && runnable.err.message)) {
return null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ describe('uncaught errors', { defaultCommandTimeout: 0 }, () => {
cy.wait(10000)
})

it('spec unhandled rejection with string content', () => {
Promise.reject('Unhandled promise rejection with string content from the spec')

cy.wait(10000)
})

// eslint-disable-next-line mocha/handle-done-callback
it('spec unhandled rejection with done', (done) => {
Promise.reject(new Error('Unhandled promise rejection from the spec'))
Expand Down

5 comments on commit 470b94b

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 470b94b Dec 19, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the linux arm64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/12.2.0/linux-arm64/develop-470b94b8fa57635c0863251d1bd2dae2562f7a05/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 470b94b Dec 19, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the linux x64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/12.2.0/linux-x64/develop-470b94b8fa57635c0863251d1bd2dae2562f7a05/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 470b94b Dec 19, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the darwin arm64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/12.2.0/darwin-arm64/develop-470b94b8fa57635c0863251d1bd2dae2562f7a05/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 470b94b Dec 19, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the darwin x64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/12.2.0/darwin-x64/develop-470b94b8fa57635c0863251d1bd2dae2562f7a05/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 470b94b Dec 19, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the win32 x64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/12.2.0/win32-x64/develop-470b94b8fa57635c0863251d1bd2dae2562f7a05/cypress.tgz

Please sign in to comment.