使用Jest和Puppeteer测试事件处理器抛错时无法捕获的问题
问题:捕获事件监听器抛出的错误失败
问题描述
我需要测试元素的事件监听器是否会抛出错误,希望捕获该错误避免测试自动失败,但始终无法实现。简化场景如下:
- 有一个
.other-container元素,addClickListener函数会为其添加点击事件处理器,处理器直接抛出错误 - 测试中触发该元素的click事件,尝试捕获错误并断言,但点击处理器的错误始终无法被捕获,测试会因该错误直接失败
尝试过将page.evaluate包裹在try..catch中,甚至添加延迟,但要么抓不到错误,要么抓到的是断言失败的错误。
相关代码及配置
测试代码(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) }) // 尝试添加延迟等待错误抛出 // 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.js module.exports = { launch: { headless: true, product: 'chrome', }, browserContext: 'default', server: { command: 'http-server -p 3333', port: 3333, launchTimeout: 10000, debug: true } }
补充背景:实际是开发数据可视化组件,监听自定义init事件,未配置必填属性时触发init会抛出错误,上述是简化场景。
原因分析
事件处理器抛出的错误不会中断dispatchEvent的执行,也不会让dispatchEvent调用本身抛出异常。这类错误会被浏览器视为全局未捕获错误,触发window.onerror事件,但不会被page.evaluate外部的try/catch捕获。Puppeteer会检测到浏览器中的全局未处理错误,直接导致测试失败。
解决方案
方案一:在浏览器上下文内部捕获错误
在page.evaluate中,主动监听全局错误事件,捕获事件处理器抛出的错误并返回:
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') await page.evaluate(() => { addClickListener() }) // 在浏览器上下文内部捕获错误 const errorMessage = await page.evaluate(() => { return new Promise((resolve) => { // 监听全局错误,只触发一次 window.addEventListener('error', (event) => { resolve(event.error.message); }, { once: true }); // 触发点击事件 const clickEvent = new MouseEvent('click', { bubbles: true }); const otherContainer = document.querySelector('.other-container') otherContainer.dispatchEvent(clickEvent) }) }) expect(errorMessage).toBe('other container error') })
方案二:监听Puppeteer的page.error事件
通过监听Puppeteer页面的error事件,捕获浏览器中的全局错误:
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') await page.evaluate(() => { addClickListener() }) let errorMessage = ""; // 监听page的error事件,只触发一次 const errorPromise = new Promise((resolve) => { page.once('error', (error) => { resolve(error.message); }); }); // 触发点击事件 await page.evaluate(() => { const clickEvent = new MouseEvent('click', { bubbles: true }); const otherContainer = document.querySelector('.other-container') otherContainer.dispatchEvent(clickEvent) }) // 等待错误被捕获 errorMessage = await errorPromise; expect(errorMessage).toBe('other container error') })
注意事项
- 方案一中使用
{ once: true }确保错误监听器只触发一次,避免内存泄漏 - 方案二中使用
page.once而不是page.on,防止后续测试受到影响 - 两种方案都能避免测试因全局未处理错误而直接失败,同时可以捕获错误信息用于断言
内容的提问来源于stack exchange,提问作者Eric
相关产品推荐
相关产品推荐

