From aec6034badffd76edf08a70bce4465eaac93e4b7 Mon Sep 17 00:00:00 2001 From: David Ortner Date: Mon, 18 Sep 2023 18:35:02 +0200 Subject: [PATCH] #1073@minor: Adds support for a new setting called disableErrorCapturing. Happy DOM will by default try to catch errors in functionality such as scripts, timers and event listeners. This setting makes it possible it possible to disable this behaviour. --- package-lock.json | 18 +++-- packages/happy-dom/src/event/EventTarget.ts | 18 +++-- .../HTMLLinkElementUtility.ts | 37 ++++++---- .../html-script-element/HTMLScriptElement.ts | 10 ++- .../HTMLScriptElementUtility.ts | 73 +++++++++++++------ .../happy-dom/src/window/IHappyDOMOptions.ts | 1 + .../happy-dom/src/window/IHappyDOMSettings.ts | 1 + packages/happy-dom/src/window/Window.ts | 28 +++++-- .../src/window/WindowErrorUtility.ts | 28 +------ .../HTMLScriptElement.test.ts | 12 +++ packages/happy-dom/test/window/Window.test.ts | 7 +- packages/jest-environment/src/index.ts | 21 ++---- .../src/UncaughtExceptionObserver.ts | 14 +++- .../test/UncaughtExceptionObserver.test.ts | 44 ++++++++++- 14 files changed, 201 insertions(+), 111 deletions(-) diff --git a/package-lock.json b/package-lock.json index 010c91e24..985ef6684 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12828,7 +12828,8 @@ "version": "14.4.3", "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.4.3.tgz", "integrity": "sha512-kCUc5MEwaEMakkO5x7aoD+DLi02ehmEM2QCGWvNqAS1dV/fAvORWEjnjsEIvml59M7Y5kCkWN6fCCyPOe8OL6Q==", - "dev": true + "dev": true, + "requires": {} }, "@types/aria-query": { "version": "5.0.1", @@ -13482,7 +13483,8 @@ "version": "6.6.2", "resolved": "https://registry.npmjs.org/@webref/css/-/css-6.6.2.tgz", "integrity": "sha512-BtX/sMtWl6eJfspxTQfmINpSEP/BvwQz1OL1FEiPffRDRZVEP9s4Nf/pyf16o9j/1SEj1LmevUCHABzSsktDvQ==", - "dev": true + "dev": true, + "requires": {} }, "accepts": { "version": "1.3.8", @@ -13504,7 +13506,8 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true + "dev": true, + "requires": {} }, "acorn-walk": { "version": "8.2.0", @@ -14968,7 +14971,8 @@ "version": "8.5.0", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz", "integrity": "sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==", - "dev": true + "dev": true, + "requires": {} }, "eslint-import-resolver-node": { "version": "0.3.7", @@ -15121,7 +15125,8 @@ "version": "0.0.7", "resolved": "https://registry.npmjs.org/eslint-plugin-turbo/-/eslint-plugin-turbo-0.0.7.tgz", "integrity": "sha512-iajOH8eD4jha3duztGVBD1BEmvNrQBaA/y3HFHf91vMDRYRwH7BpHSDFtxydDpk5ghlhRxG299SFxz7D6z4MBQ==", - "dev": true + "dev": true, + "requires": {} }, "eslint-scope": { "version": "5.1.1", @@ -17051,7 +17056,8 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "dev": true + "dev": true, + "requires": {} }, "jest-regex-util": { "version": "29.4.3", diff --git a/packages/happy-dom/src/event/EventTarget.ts b/packages/happy-dom/src/event/EventTarget.ts index 7a57a1f62..608f62928 100644 --- a/packages/happy-dom/src/event/EventTarget.ts +++ b/packages/happy-dom/src/event/EventTarget.ts @@ -140,8 +140,12 @@ export default abstract class EventTarget implements IEventTarget { if (typeof this[onEventName] === 'function') { // We can end up in a never ending loop if the listener for the error event on Window also throws an error. - if (window && (this !== window || event.type !== 'error')) { - WindowErrorUtility.captureErrorSync(window, this[onEventName].bind(this, event)); + if ( + window && + (this !== window || event.type !== 'error') && + !window.happyDOM.settings.disableErrorCapturing + ) { + WindowErrorUtility.captureError(window, this[onEventName].bind(this, event)); } else { this[onEventName].call(this, event); } @@ -169,14 +173,18 @@ export default abstract class EventTarget implements IEventTarget { } // We can end up in a never ending loop if the listener for the error event on Window also throws an error. - if (window && (this !== window || event.type !== 'error')) { + if ( + window && + (this !== window || event.type !== 'error') && + !window.happyDOM.settings.disableErrorCapturing + ) { if ((listener).handleEvent) { - WindowErrorUtility.captureErrorSync( + WindowErrorUtility.captureError( window, (listener).handleEvent.bind(this, event) ); } else { - WindowErrorUtility.captureErrorSync( + WindowErrorUtility.captureError( window, (<(event: Event) => void>listener).bind(this, event) ); diff --git a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementUtility.ts b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementUtility.ts index 808122390..687635ab7 100644 --- a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementUtility.ts +++ b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementUtility.ts @@ -24,32 +24,41 @@ export default class HTMLLinkElementUtility { if (href !== null && rel && rel.toLowerCase() === 'stylesheet' && element.isConnected) { if (element.ownerDocument.defaultView.happyDOM.settings.disableCSSFileLoading) { - WindowErrorUtility.dispatchError( - element, - new DOMException( - `Failed to load external stylesheet "${href}". CSS file loading is disabled.`, - DOMExceptionNameEnum.notSupportedError - ) + const error = new DOMException( + `Failed to load external stylesheet "${href}". CSS file loading is disabled.`, + DOMExceptionNameEnum.notSupportedError ); - + WindowErrorUtility.dispatchError(element, error); + if (element.ownerDocument.defaultView.happyDOM.settings.disableErrorCapturing) { + throw error; + } return; } (element.ownerDocument)._readyStateManager.startTask(); - const code: string | null = await WindowErrorUtility.captureErrorAsync( - element, - async () => await ResourceFetch.fetch(element.ownerDocument, href) - ); + let code: string | null = null; + let error: Error | null = null; + + try { + code = await ResourceFetch.fetch(element.ownerDocument, href); + } catch (e) { + error = e; + } - if (code) { + (element.ownerDocument)._readyStateManager.endTask(); + + if (error) { + WindowErrorUtility.dispatchError(element, error); + if (element.ownerDocument.defaultView.happyDOM.settings.disableErrorCapturing) { + throw error; + } + } else { const styleSheet = new CSSStyleSheet(); styleSheet.replaceSync(code); (element.sheet) = styleSheet; element.dispatchEvent(new Event('load')); } - - (element.ownerDocument)._readyStateManager.endTask(); } } } diff --git a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts index 15601c8dc..b505886cb 100644 --- a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts +++ b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts @@ -189,9 +189,13 @@ export default class HTMLScriptElement extends HTMLElement implements IHTMLScrip type === 'application/x-javascript' || type.startsWith('text/javascript')) ) { - WindowErrorUtility.captureErrorSync(this.ownerDocument.defaultView, () => - this.ownerDocument.defaultView.eval(textContent) - ); + if (this.ownerDocument.defaultView.happyDOM.settings.disableErrorCapturing) { + this.ownerDocument.defaultView.eval(textContent); + } else { + WindowErrorUtility.captureError(this.ownerDocument.defaultView, () => + this.ownerDocument.defaultView.eval(textContent) + ); + } } } } diff --git a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementUtility.ts b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementUtility.ts index fe4a5e0b6..d0e813b4c 100644 --- a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementUtility.ts +++ b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementUtility.ts @@ -25,40 +25,69 @@ export default class HTMLScriptElementUtility { element.ownerDocument.defaultView.happyDOM.settings.disableJavaScriptFileLoading || element.ownerDocument.defaultView.happyDOM.settings.disableJavaScriptEvaluation ) { - WindowErrorUtility.dispatchError( - element, - new DOMException( - `Failed to load external script "${src}". JavaScript file loading is disabled.`, - DOMExceptionNameEnum.notSupportedError - ) + const error = new DOMException( + `Failed to load external script "${src}". JavaScript file loading is disabled.`, + DOMExceptionNameEnum.notSupportedError ); + WindowErrorUtility.dispatchError(element, error); + if (element.ownerDocument.defaultView.happyDOM.settings.disableErrorCapturing) { + throw error; + } return; } if (async) { (element.ownerDocument)._readyStateManager.startTask(); - const code = await WindowErrorUtility.captureErrorAsync( - element, - async () => await ResourceFetch.fetch(element.ownerDocument, src) - ); + let code: string | null = null; + let error: Error | null = null; - if (code) { - WindowErrorUtility.captureErrorSync(element.ownerDocument.defaultView, () => - element.ownerDocument.defaultView.eval(code) - ); - element.dispatchEvent(new Event('load')); + try { + code = await ResourceFetch.fetch(element.ownerDocument, src); + } catch (e) { + error = e; } + (element.ownerDocument)._readyStateManager.endTask(); + + if (error) { + WindowErrorUtility.dispatchError(element, error); + if (element.ownerDocument.defaultView.happyDOM.settings.disableErrorCapturing) { + throw error; + } + } else { + if (element.ownerDocument.defaultView.happyDOM.settings.disableErrorCapturing) { + element.ownerDocument.defaultView.eval(code); + } else { + WindowErrorUtility.captureError(element.ownerDocument.defaultView, () => + element.ownerDocument.defaultView.eval(code) + ); + } + element.dispatchEvent(new Event('load')); + } } else { - const code = WindowErrorUtility.captureErrorSync(element, () => - ResourceFetch.fetchSync(element.ownerDocument, src) - ); + let code: string | null = null; + let error: Error | null = null; + + try { + code = ResourceFetch.fetchSync(element.ownerDocument, src); + } catch (e) { + error = e; + } - if (code) { - WindowErrorUtility.captureErrorSync(element.ownerDocument.defaultView, () => - element.ownerDocument.defaultView.eval(code) - ); + if (error) { + WindowErrorUtility.dispatchError(element, error); + if (element.ownerDocument.defaultView.happyDOM.settings.disableErrorCapturing) { + throw error; + } + } else { + if (element.ownerDocument.defaultView.happyDOM.settings.disableErrorCapturing) { + element.ownerDocument.defaultView.eval(code); + } else { + WindowErrorUtility.captureError(element.ownerDocument.defaultView, () => + element.ownerDocument.defaultView.eval(code) + ); + } element.dispatchEvent(new Event('load')); } } diff --git a/packages/happy-dom/src/window/IHappyDOMOptions.ts b/packages/happy-dom/src/window/IHappyDOMOptions.ts index b005c4db2..92ebb25bd 100644 --- a/packages/happy-dom/src/window/IHappyDOMOptions.ts +++ b/packages/happy-dom/src/window/IHappyDOMOptions.ts @@ -12,6 +12,7 @@ export default interface IHappyDOMOptions { disableCSSFileLoading?: boolean; disableIframePageLoading?: boolean; disableComputedStyleRendering?: boolean; + disableErrorCapturing?: boolean; enableFileSystemHttpRequests?: boolean; navigator?: { userAgent?: string; diff --git a/packages/happy-dom/src/window/IHappyDOMSettings.ts b/packages/happy-dom/src/window/IHappyDOMSettings.ts index 3212c13b5..1b2afeb00 100644 --- a/packages/happy-dom/src/window/IHappyDOMSettings.ts +++ b/packages/happy-dom/src/window/IHappyDOMSettings.ts @@ -7,6 +7,7 @@ export default interface IHappyDOMSettings { disableCSSFileLoading: boolean; disableIframePageLoading: boolean; disableComputedStyleRendering: boolean; + disableErrorCapturing: boolean; enableFileSystemHttpRequests: boolean; navigator: { userAgent: string; diff --git a/packages/happy-dom/src/window/Window.ts b/packages/happy-dom/src/window/Window.ts index 93ea64236..67562cb04 100644 --- a/packages/happy-dom/src/window/Window.ts +++ b/packages/happy-dom/src/window/Window.ts @@ -210,6 +210,7 @@ export default class Window extends EventTarget implements IWindow { disableCSSFileLoading: false, disableIframePageLoading: false, disableComputedStyleRendering: false, + disableErrorCapturing: false, enableFileSystemHttpRequests: false, navigator: { userAgent: `Mozilla/5.0 (X11; ${ @@ -736,7 +737,11 @@ export default class Window extends EventTarget implements IWindow { public setTimeout(callback: Function, delay = 0, ...args: unknown[]): NodeJS.Timeout { const id = this._setTimeout(() => { this.happyDOM.asyncTaskManager.endTimer(id); - WindowErrorUtility.captureErrorSync(this, () => callback(...args)); + if (this.happyDOM.settings.disableErrorCapturing) { + callback(...args); + } else { + WindowErrorUtility.captureError(this, () => callback(...args)); + } }, delay); this.happyDOM.asyncTaskManager.startTimer(id); return id; @@ -762,11 +767,15 @@ export default class Window extends EventTarget implements IWindow { */ public setInterval(callback: Function, delay = 0, ...args: unknown[]): NodeJS.Timeout { const id = this._setInterval(() => { - WindowErrorUtility.captureErrorSync( - this, - () => callback(...args), - () => this.clearInterval(id) - ); + if (this.happyDOM.settings.disableErrorCapturing) { + callback(...args); + } else { + WindowErrorUtility.captureError( + this, + () => callback(...args), + () => this.clearInterval(id) + ); + } }, delay); this.happyDOM.asyncTaskManager.startTimer(id); return id; @@ -811,8 +820,13 @@ export default class Window extends EventTarget implements IWindow { const taskId = this.happyDOM.asyncTaskManager.startTask(() => (isAborted = true)); this._queueMicrotask(() => { if (!isAborted) { - WindowErrorUtility.captureErrorSync(this, <() => unknown>callback); this.happyDOM.asyncTaskManager.endTask(taskId); + + if (this.happyDOM.settings.disableErrorCapturing) { + callback(); + } else { + WindowErrorUtility.captureError(this, <() => unknown>callback); + } } }); } diff --git a/packages/happy-dom/src/window/WindowErrorUtility.ts b/packages/happy-dom/src/window/WindowErrorUtility.ts index b831370d2..7c76b5482 100644 --- a/packages/happy-dom/src/window/WindowErrorUtility.ts +++ b/packages/happy-dom/src/window/WindowErrorUtility.ts @@ -6,32 +6,6 @@ import { IElement } from '../index.js'; * Error utility. */ export default class WindowErrorUtility { - /** - * Calls a function asynchronously wrapped in a try/catch block to capture errors and dispatch error events. - * - * It will also output the errors to the console. - * - * @param elementOrWindow Element or Window. - * @param callback Callback. - * @param [cleanup] Cleanup callback on error. - * @returns Promise. - */ - public static async captureErrorAsync( - elementOrWindow: IWindow | IElement, - callback: () => Promise, - cleanup?: () => void - ): Promise { - try { - return await callback(); - } catch (error) { - this.dispatchError(elementOrWindow, error); - if (cleanup) { - cleanup(); - } - } - return null; - } - /** * Calls a function synchronously wrapped in a try/catch block to capture errors and dispatch error events. * If the callback returns a Promise, it will catch errors from the promise. @@ -43,7 +17,7 @@ export default class WindowErrorUtility { * @param [cleanup] Cleanup callback on error. * @returns Result. */ - public static captureErrorSync( + public static captureError( elementOrWindow: IWindow | IElement, callback: () => T, cleanup?: () => void diff --git a/packages/happy-dom/test/nodes/html-script-element/HTMLScriptElement.test.ts b/packages/happy-dom/test/nodes/html-script-element/HTMLScriptElement.test.ts index eacf6efdd..588c33553 100644 --- a/packages/happy-dom/test/nodes/html-script-element/HTMLScriptElement.test.ts +++ b/packages/happy-dom/test/nodes/html-script-element/HTMLScriptElement.test.ts @@ -485,5 +485,17 @@ describe('HTMLScriptElement', () => { true ); }); + + it('Throws an exception when appending an element that contains invalid Javascript and Window.happyDOM.settings.disableErrorCapturing is set to true.', () => { + const element = document.createElement('script'); + + window.happyDOM.settings.disableErrorCapturing = true; + + element.text = 'globalThis.test = /;'; + + expect(() => { + document.body.appendChild(element); + }).toThrow(new TypeError('Invalid regular expression: missing /')); + }); }); }); diff --git a/packages/happy-dom/test/window/Window.test.ts b/packages/happy-dom/test/window/Window.test.ts index c5d80563e..5a14d1451 100644 --- a/packages/happy-dom/test/window/Window.test.ts +++ b/packages/happy-dom/test/window/Window.test.ts @@ -132,6 +132,7 @@ describe('Window', () => { expect(windowWithOptions.happyDOM.settings.disableJavaScriptFileLoading).toBe(false); expect(windowWithOptions.happyDOM.settings.disableCSSFileLoading).toBe(false); expect(windowWithOptions.happyDOM.settings.disableIframePageLoading).toBe(false); + expect(windowWithOptions.happyDOM.settings.disableErrorCapturing).toBe(false); expect(windowWithOptions.happyDOM.settings.enableFileSystemHttpRequests).toBe(false); expect(windowWithOptions.happyDOM.settings.navigator.userAgent).toBe('test'); expect(windowWithOptions.happyDOM.settings.device.prefersColorScheme).toBe('dark'); @@ -150,6 +151,7 @@ describe('Window', () => { expect(windowWithoutOptions.happyDOM.settings.disableJavaScriptFileLoading).toBe(false); expect(windowWithoutOptions.happyDOM.settings.disableCSSFileLoading).toBe(false); expect(windowWithoutOptions.happyDOM.settings.disableIframePageLoading).toBe(false); + expect(windowWithoutOptions.happyDOM.settings.disableErrorCapturing).toBe(false); expect(windowWithoutOptions.happyDOM.settings.enableFileSystemHttpRequests).toBe(false); expect(windowWithoutOptions.happyDOM.settings.navigator.userAgent).toBe( `Mozilla/5.0 (${GET_NAVIGATOR_PLATFORM()}) AppleWebKit/537.36 (KHTML, like Gecko) HappyDOM/${ @@ -821,11 +823,6 @@ describe('Window', () => { const result = <() => number>window.eval('() => 5'); expect(result()).toBe(5); }); - - it('Captures errors and triggers an error event.', () => { - const result = <() => number>window.eval('() => 5'); - expect(result()).toBe(5); - }); }); describe('setTimeout()', () => { diff --git a/packages/jest-environment/src/index.ts b/packages/jest-environment/src/index.ts index b2e7aa4b3..4f7b8783a 100644 --- a/packages/jest-environment/src/index.ts +++ b/packages/jest-environment/src/index.ts @@ -5,7 +5,7 @@ import * as JestUtil from 'jest-util'; import { ModuleMocker } from 'jest-mock'; import { LegacyFakeTimers, ModernFakeTimers } from '@jest/fake-timers'; import { JestEnvironment, EnvironmentContext } from '@jest/environment'; -import { Window, IWindow, ErrorEvent } from 'happy-dom'; +import { Window, IWindow } from 'happy-dom'; import { Script } from 'vm'; import { Global, Config } from '@jest/types'; @@ -15,10 +15,12 @@ import { Global, Config } from '@jest/types'; export default class HappyDOMEnvironment implements JestEnvironment { public fakeTimers: LegacyFakeTimers = null; public fakeTimersModern: ModernFakeTimers = null; - public window: IWindow = new Window({ console: globalThis.console }); + public window: IWindow = new Window({ + console: globalThis.console, + settings: { disableErrorCapturing: true } + }); public global: Global.Global = (this.window); public moduleMocker: ModuleMocker = new ModuleMocker((this.window)); - private errorEventListener: (event: ErrorEvent) => void | null = null; /** * Constructor. @@ -73,14 +75,6 @@ export default class HappyDOMEnvironment implements JestEnvironment { this.window.happyDOM.setURL('http://localhost/'); } - // Report uncaught errors. - this.errorEventListener = (event: ErrorEvent) => { - if (this.window['_listeners']?.['error']?.length === 0 && event.error !== null) { - process.emit('uncaughtException', event.error); - } - }; - this.window.addEventListener('error', this.errorEventListener); - this.fakeTimers = new LegacyFakeTimers({ config: projectConfig, global: (this.window), @@ -125,11 +119,6 @@ export default class HappyDOMEnvironment implements JestEnvironment { ((this.global)).happyDOM.cancelAsync(); - if (this.errorEventListener) { - this.window.removeEventListener('error', this.errorEventListener); - this.errorEventListener = null; - } - this.global = null; this.moduleMocker = null; this.fakeTimers = null; diff --git a/packages/uncaught-exception-observer/src/UncaughtExceptionObserver.ts b/packages/uncaught-exception-observer/src/UncaughtExceptionObserver.ts index 78ce81533..2e4933054 100644 --- a/packages/uncaught-exception-observer/src/UncaughtExceptionObserver.ts +++ b/packages/uncaught-exception-observer/src/UncaughtExceptionObserver.ts @@ -27,14 +27,17 @@ export default class UncaughtExceptionObserver { (this.constructor).listenerCount++; this.uncaughtExceptionListener = ( - error: Error, + error: unknown, origin: 'uncaughtException' | 'unhandledRejection' ) => { if (origin === 'unhandledRejection') { return; } - if (error instanceof this.window.Error && error.stack?.includes('/happy-dom/')) { + if ( + (error instanceof this.window.Error || error instanceof this.window.DOMException) && + error.stack?.includes('/happy-dom/') + ) { this.window.console.error(error); this.window.dispatchEvent( new this.window.ErrorEvent('error', { error, message: error.message }) @@ -52,8 +55,11 @@ export default class UncaughtExceptionObserver { // The "uncaughtException" event is not always triggered for unhandled rejections. // Therefore we want to use the "unhandledRejection" event as well. - this.uncaughtRejectionListener = (error: Error) => { - if (error instanceof this.window.Error && error.stack?.includes('/happy-dom/')) { + this.uncaughtRejectionListener = (error: unknown) => { + if ( + (error instanceof this.window.Error || error instanceof this.window.DOMException) && + error.stack?.includes('/happy-dom/') + ) { this.window.console.error(error); this.window.dispatchEvent( new this.window.ErrorEvent('error', { error, message: error.message }) diff --git a/packages/uncaught-exception-observer/test/UncaughtExceptionObserver.test.ts b/packages/uncaught-exception-observer/test/UncaughtExceptionObserver.test.ts index a361f7e01..a3b42305f 100644 --- a/packages/uncaught-exception-observer/test/UncaughtExceptionObserver.test.ts +++ b/packages/uncaught-exception-observer/test/UncaughtExceptionObserver.test.ts @@ -1,7 +1,7 @@ import { Window, ErrorEvent, IResponse } from 'happy-dom'; import UncaughtExceptionObserver from '../lib/UncaughtExceptionObserver.js'; -async function itObservesUnhandledRejections(): Promise { +async function itObservesUnhandledFetchRejections(): Promise { const window = new Window(); const document = window.document; const observer = new UncaughtExceptionObserver(); @@ -45,6 +45,45 @@ async function itObservesUnhandledRejections(): Promise { } } +async function itObservesUnhandledJavaScriptFetchRejections(): Promise { + const window = new Window(); + const document = window.document; + const observer = new UncaughtExceptionObserver(); + let errorEvent: ErrorEvent | null = null; + + window.happyDOM.settings.disableErrorCapturing = true; + + observer.observe(window); + + window.addEventListener('error', (event) => (errorEvent = event)); + + document.write(` + + `); + + await new Promise((resolve) => setTimeout(resolve, 10)); + + observer.disconnect(); + + if (!(errorEvent instanceof window.ErrorEvent)) { + throw new Error('Error event not dispatched.'); + } + + if ( + errorEvent.error.message !== + 'Fetch to "https://localhost:3000/404.js" failed. Error: connect ECONNREFUSED 127.0.0.1:3000' + ) { + throw new Error('Error message not correct.'); + } + + if ( + errorEvent.message !== + 'Fetch to "https://localhost:3000/404.js" failed. Error: connect ECONNREFUSED 127.0.0.1:3000' + ) { + throw new Error('Error message not correct.'); + } +} + async function itObservesUncaughtExceptions(): Promise { const window = new Window(); const document = window.document; @@ -96,7 +135,8 @@ async function itObservesUncaughtExceptions(): Promise { async function main(): Promise { try { - await itObservesUnhandledRejections(); + await itObservesUnhandledFetchRejections(); + await itObservesUnhandledJavaScriptFetchRejections(); await itObservesUncaughtExceptions(); } catch (error) { // eslint-disable-next-line no-console