I'm trying to test that a particular element's event listener successfully throws an error. I want to catch the error so that my test does not automatically fail, but I'm not able to for some reason.
In the example below, I've got a div .other-container
and a function addClickListener
, which adds a click event handler on .other-container
. That handler simply throws an error. In my test, I dispatch a 'click' event on the div element and try to catch it. If it's caught, I save the error message to assert that the error was received.
The click handler is successfully called and throws the error, but my test is not able to catch it. In addition, the test automatically fails as a result of the error, regardless of the assertion that's made. The assertion could be expect(true).toBe(true)
, and the test will still fail.
I'm guessing the inability to catch the error has to do with the asynchronous nature of event handlers. But in my case, I'm wrapping my call to page.evaluate
in the try..catch block. I'm awaiting page.evaluate
to finish inside of try
, and I would think that the error would get thrown during that wait time. So why doesn't the error get caught?
I also tried adding a 1 second delay before exiting the try
block in case the event handler needed extra time to throw the error. Oddly, an error does get caught, but it's the failed assertion I make at the end of the test. Not the thrown error from the click handler.
my-test.test.js...
test('other container', async () => {
await page.goto('http://localhost:3333/index.html')
await page.setViewport({ width: 1280, height: 800 })
await page.waitForFunction('window.addClickListener')
await page.waitForSelector('.other-container')
page.on('console', message =>
console.log(`${message.type().substring(0, 3).toUpperCase()} ${message.text()}`))
await page.evaluate(() => { addClickListener() })
let errorMessage = ""
try {
await page.evaluate(() => {
const clickEvent = new MouseEvent('click', { bubbles: true });
const otherContainer = document.querySelector('.other-container')
otherContainer.dispatchEvent(clickEvent)
})
// Wait a little while so that the click handler has enough time to throw the error.
// await page.evaluate(() => {
// return new Promise((resolve) => setTimeout(resolve, 1000))
// })
} catch (error) {
errorMessage = error.message
}
expect(!!errorMessage).toBe(true)
})
index.js...
export function addClickListener () {
document.querySelector('.other-container').addEventListener('click', () => {
throw new Error('other container error')
})
}
index.html...
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Random Page</title>
<link rel="stylesheet" href="http://localhost:3333/index.css">
<script type="module">
import { addClickListener } from 'http://localhost:3333/index.js'
window.addClickListener = addClickListener
</script>
</head>
<body>
<div class="other-container"></div>
</body>
</html>
index.css...
.other-container {
width: 50px;
height: 50px;
background-color: blue;
}
Dependencies...
"@babel/preset-env": "^7.28.0",
"http-server": "^14.1.1",
"jest": "^30.0.4",
"jest-puppeteer": "^11.0.0",
"puppeteer": "^24.17.0"
jest-puppeteer config...
module.exports = {
launch: {
headless: true,
product: 'chrome',
},
browserContext: 'default',
server: {
command: 'http-server -p 3333',
port: 3333,
launchTimeout: 10000,
debug: true
}
}
More background info:
I'm developing a data visualization widget that listens for a custom init
event. The widget has a couple of properties that must be configured by the user. When the init
event is dispatched, the widget checks whether those required properties have been configured. If not, an error is thrown. My example above is a simplified version of this behavior, except it's throwing an error within a click
event handler.
Inability to catch the error is normal JS behavior; the error that's being triggered runs asynchronously in a different call stack:
document.querySelector("button").addEventListener("click", () => {
throw Error("this will throw uncaught!");
});
try {
document.querySelector("button").click();
} catch (err) {
console.error("this never runs");
}
<button>x</button>
...and even if this worked, it's a cumbersome way to go about dealing with this scenario.
The main issue is that jest-puppeteer automatically installs an error listener that fails tests whenever an error is thrown by the page under test.
You can remove the listener for it entirely, or add your own listener to ignore a specific error by message.
To silence the default error listener, you can set exitOnPageError: false
in jest-puppeteer.config.js
:
module.exports = {
exitOnPageError: false // default: true
};
Or use page.removeAllListeners("pageerror");
as shown below.
test.test.js:
const html = `<!DOCTYPE html><html><body><script>
// change this to something else to see the test fail
throw Error("hello world");
</script></body></html>`;
beforeAll(() => {
page.removeAllListeners("pageerror"); // basically exitOnPageError: false
page.on("pageerror", err => {
if (err.message !== "hello world") {
throw err;
}
});
});
test("does not throw just because the page throws", async () => {
await page.setContent(html);
});
jest.config.js:
module.exports = {
preset: "jest-puppeteer",
};
Deps:
{
"jest": "^30.0.5",
"jest-puppeteer": "^11.0.0",
"puppeteer": "^24.11.1"
}
Output:
npx jest test.test.js
PASS ./test.test.js
✓ does not throw just because the page throws (181 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.386 s
Ran all test suites matching test.test.js
If you throw a different error than "hello world"
:
npx jest test.test.js
FAIL ./test.test.js
✕ does not throw just because the page throws (16 ms)
● does not throw just because the page throws
goodbye world
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 total
Snapshots: 0 total
Time: 0.879 s, estimated 2 s
Ran all test suites matching test.test.js.
As an aside, Playwright does not fail tests when the page under test throws like this by default. I'd generally recommend using it over jest-puppeteer for testing these days.