Skip to content

Commit af1c1d7

Browse files
authored
add recipe showing how to handle errors (#619)
1 parent ad803fa commit af1c1d7

File tree

14 files changed

+239
-0
lines changed

14 files changed

+239
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Recipe | Description
88
--- | ---
99
[Node Modules](./examples/fundamentals__node-modules) | Import your own node modules
1010
[Environment variables](./examples/server-communication__env-variables) | Passing environment variables to tests
11+
[Handling errors](./examples/fundamentals__errors) | Handling thrown errors and unhandled promise rejections
1112
[Dynamic tests](./examples/fundamentals__dynamic-tests) | Create tests dynamically from data
1213
[Fixtures](./examples/fundamentals__fixtures) | Loading single or multiple fixtures
1314
[Adding Custom Commands](./examples/fundamentals__add-custom-command) | Write your own custom commands using JavaScript with correct types for IntelliSense to work

circle.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,8 @@ jobs:
221221
<<: *defaults
222222
fundamentals__add-custom-command-ts:
223223
<<: *defaults
224+
fundamentals__errors:
225+
<<: *defaults
224226
logging-in__csrf-tokens:
225227
<<: *defaults
226228
logging-in__html-web-forms:
@@ -428,6 +430,9 @@ all_jobs: &all_jobs
428430
- fundamentals__add-custom-command-ts:
429431
requires:
430432
- build
433+
- fundamentals__errors:
434+
requires:
435+
- build
431436
- logging-in__csrf-tokens:
432437
requires:
433438
- build
@@ -610,6 +615,7 @@ all_jobs: &all_jobs
610615
- fundamentals__module-api-wrap
611616
- fundamentals__add-custom-command
612617
- fundamentals__add-custom-command-ts
618+
- fundamentals__errors
613619
- fundamentals__typescript
614620
- logging-in__csrf-tokens
615621
- logging-in__html-web-forms
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Handling application errors
2+
3+
For more details, see [Cypress catalogue of events](https://on.cypress.io/catalog-of-events)
4+
5+
## Exceptions
6+
7+
If an application throws an error, it fails the Cypress test automatically.
8+
9+
![Application error fails the test](./images/app-error.gif)
10+
11+
You can see how to ignore such errors in [cypress/integration/app-error.js](./cypress/integration/app-error.js) spec file
12+
13+
## Test fails
14+
15+
If a Cypress command fails, the test fails
16+
17+
![Test fails after it fails to find an element](./images/test-error.gif)
18+
19+
You can listen to the "fail" events and return false to NOT fail the test, as [cypress/integration/test-fails.js](./cypress/integration/test-fails.js) shows.
20+
21+
## Unhandled promise rejections
22+
23+
If the application code creates an unhandled rejected promise, Cypress does NOT see it by default and continues with the test. If you want to fail the test, listen to the unhandled promise event and throw an error.
24+
25+
![Test failing after an application has unhandled rejected promise](./images/unhandled-promise.gif)
26+
27+
You can register your own unhandled promise event listener during `cy.visit` as [cypress/integration/unhandled-promise.js](./cypress/integration/unhandled-promise.js) shows. Or you can register the window handler for all tests using `Cypress.on('window:before:load')` call, see [cypress/integration/unhandled-promise2.js](./cypress/integration/unhandled-promise2.js).

examples/fundamentals__errors/app.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/* eslint-env browser */
2+
3+
/* eslint-disable no-console */
4+
document.getElementById('error').addEventListener('click', () => {
5+
console.log('application will throw an error in 1 second')
6+
setTimeout(() => {
7+
console.log('application is about to throw an error')
8+
throw new Error('Things went bad')
9+
}, 1000)
10+
})
11+
12+
document.getElementById('promise').addEventListener('click', () => {
13+
console.log('application with NOT handle a rejected promise in 1 second')
14+
new Promise((resolve, reject) => {
15+
setTimeout(() => {
16+
console.log('application is about to reject a promise')
17+
reject('Did not handle this promise')
18+
}, 1000)
19+
})
20+
})
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"pluginsFile": false,
3+
"fixturesFolder": false,
4+
"supportFile": false
5+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/// <reference types="cypress" />
2+
3+
/* eslint-disable no-console */
4+
describe('App error', () => {
5+
// NOTE: run this test to see it fail on application error
6+
it.skip('fails the Cypress test', () => {
7+
cy.visit('index.html')
8+
cy.get('button#error').click()
9+
// the error happens after 1000ms
10+
cy.wait(1500)
11+
})
12+
13+
it('can be ignored', () => {
14+
/**
15+
* By using "cy.on()" we can ignore an exception in the current test only.
16+
* If you want to register exception handler for all tests using "Cypress.on()"
17+
* @see https://on.cypress.io/catalog-of-events
18+
* @param {Error} e The exception we caught
19+
* @param {Mocha.Runnable} runnable is the current test or hook during which the error is caught
20+
*/
21+
cy.on('uncaught:exception', (e, runnable) => {
22+
console.log('error', e)
23+
console.log('runnable', runnable)
24+
25+
// we can simply return false to avoid failing the test on uncaught error
26+
// return false
27+
// but a better strategy is to make sure the error is expected
28+
if (e.message.includes('Things went bad')) {
29+
// we expected this error, so let's ignore it
30+
// and let the test continue
31+
return false
32+
}
33+
// on any other error message the test fails
34+
})
35+
36+
cy.visit('index.html')
37+
cy.get('button#error').click()
38+
// the error happens after 1000ms
39+
cy.wait(1500)
40+
})
41+
})
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/// <reference types="cypress" />
2+
3+
/* eslint-disable no-console */
4+
describe('Test fails', () => {
5+
// NOTE: run this test to see it fail because it tries to get an non-existent button
6+
it.skip('when the command fails', () => {
7+
cy.visit('index.html')
8+
cy.get('button#does-not-exist', { timeout: 1000 }).click()
9+
})
10+
11+
it('can be ignored', () => {
12+
/**
13+
* By using "cy.on()" we can ignore the current test failing.
14+
* If you want to register this handler for all tests use "Cypress.on()"
15+
* @see https://on.cypress.io/catalog-of-events
16+
* @param {Error} e The exception we caught
17+
* @param {Mocha.Runnable} runnable is the current test or hook during which the error is caught
18+
*/
19+
cy.on('fail', (e, runnable) => {
20+
console.log('error', e)
21+
console.log('runnable', runnable)
22+
23+
// we can simply return false to avoid failing the test on uncaught error
24+
// return false
25+
// but a better strategy is to make sure the error is expected
26+
if (e.name === 'AssertionError' &&
27+
e.message.includes('Expected to find element: `button#does-not-exist`, but never found it')) {
28+
// we expected this error, so let's ignore it
29+
// and let the test continue
30+
return false
31+
}
32+
// on any other error message the test fails
33+
})
34+
35+
cy.visit('index.html')
36+
cy.get('button#does-not-exist', { timeout: 1000 }).click()
37+
38+
// note: after the cy.get fails and the test fails
39+
// the remaining commands are NOT executed
40+
// thus this failing assertion never gets to run
41+
cy.wrap(false).should('be.true')
42+
})
43+
})
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/// <reference types="cypress" />
2+
3+
/* eslint-disable no-console */
4+
describe('Unhandled promises', () => {
5+
it('does not affect the Cypress test', () => {
6+
cy.visit('index.html')
7+
cy.get('button#promise').click()
8+
// the unhandled promise happens after 1000ms
9+
cy.wait(1500)
10+
// but our test happily finishes
11+
})
12+
13+
// NOTE: skipping the test because it shows how to fail the test for real
14+
it.skip('fails Cypress test if we register our own handler', () => {
15+
// we can install our handler to listen for unhandled rejected promises
16+
// in the application code and fail the test
17+
cy.visit('index.html', {
18+
onBeforeLoad (win) {
19+
// https://developer.mozilla.org/en-US/docs/Web/API/Window/unhandledrejection_event
20+
win.addEventListener('unhandledrejection', (event) => {
21+
const msg = `UNHANDLED PROMISE REJECTION: ${event.reason}`
22+
23+
// fail the test
24+
throw new Error(msg)
25+
})
26+
},
27+
})
28+
29+
cy.get('button#promise').click()
30+
// the unhandled promise happens after 1000ms
31+
cy.wait(1500)
32+
})
33+
34+
afterEach(() => {
35+
console.log('afterEach')
36+
})
37+
})
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/// <reference types="cypress" />
2+
3+
/* eslint-disable no-console */
4+
describe('Unhandled promises using window event', () => {
5+
// we can bind to the events before each test using "cy.on()" call
6+
// or for all events using "Cypress.on()" call, usually placed into support file
7+
before(() => {
8+
Cypress.on('window:before:load', (win) => {
9+
// https://developer.mozilla.org/en-US/docs/Web/API/Window/unhandledrejection_event
10+
win.addEventListener('unhandledrejection', (event) => {
11+
const msg = `UNHANDLED PROMISE REJECTION: ${event.reason}`
12+
13+
// fail the test
14+
throw new Error(msg)
15+
})
16+
})
17+
})
18+
19+
// NOTE: skipping the test because it shows how to fail the test for real
20+
it.skip('fails Cypress test if we register our own handler', () => {
21+
cy.visit('index.html')
22+
cy.get('button#promise').click()
23+
// the unhandled promise happens after 1000ms
24+
cy.wait(1500)
25+
})
26+
27+
afterEach(() => {
28+
console.log('afterEach')
29+
})
30+
})
Loading
Loading
Loading
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<html lang="en">
2+
<body>
3+
<main>
4+
<h1>Handling application errors</h1>
5+
<p>
6+
Click on the button to cause application error
7+
<button id="error">Throw an error</button>
8+
</p>
9+
10+
<p>
11+
This button tells the application to create an unhandled rejected promise
12+
<button id="promise">Unhandle</button>
13+
</p>
14+
</main>
15+
<script src="app.js"></script>
16+
</body>
17+
</html>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"name": "handling-application-errors",
3+
"version": "1.0.0",
4+
"description": "Handling application errors example",
5+
"private": true,
6+
"scripts": {
7+
"cypress:open": "../../node_modules/.bin/cypress open",
8+
"cypress:run": "../../node_modules/.bin/cypress run",
9+
"test:ci": "../../node_modules/.bin/cypress run",
10+
"test:ci:record": "../../node_modules/.bin/cypress run --record"
11+
}
12+
}

0 commit comments

Comments
 (0)