Skip to content

Commit

Permalink
#1073@minor: Adds support for a new setting called disableErrorCaptur…
Browse files Browse the repository at this point in the history
…ing. 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.
  • Loading branch information
capricorn86 committed Sep 18, 2023
1 parent 89e4f76 commit aec6034
Show file tree
Hide file tree
Showing 14 changed files with 201 additions and 111 deletions.
18 changes: 12 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 13 additions & 5 deletions packages/happy-dom/src/event/EventTarget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 !== <IEventTarget>window || event.type !== 'error')) {
WindowErrorUtility.captureErrorSync(window, this[onEventName].bind(this, event));
if (
window &&
(this !== <IEventTarget>window || event.type !== 'error') &&
!window.happyDOM.settings.disableErrorCapturing
) {
WindowErrorUtility.captureError(window, this[onEventName].bind(this, event));
} else {
this[onEventName].call(this, event);
}
Expand Down Expand Up @@ -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 !== <IEventTarget>window || event.type !== 'error')) {
if (
window &&
(this !== <IEventTarget>window || event.type !== 'error') &&
!window.happyDOM.settings.disableErrorCapturing
) {
if ((<IEventListener>listener).handleEvent) {
WindowErrorUtility.captureErrorSync(
WindowErrorUtility.captureError(
window,
(<IEventListener>listener).handleEvent.bind(this, event)
);
} else {
WindowErrorUtility.captureErrorSync(
WindowErrorUtility.captureError(
window,
(<(event: Event) => void>listener).bind(this, event)
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

(<Document>element.ownerDocument)._readyStateManager.startTask();

const code: string | null = await WindowErrorUtility.captureErrorAsync<string>(
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) {
(<Document>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);
(<CSSStyleSheet>element.sheet) = styleSheet;
element.dispatchEvent(new Event('load'));
}

(<Document>element.ownerDocument)._readyStateManager.endTask();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
);
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
(<Document>element.ownerDocument)._readyStateManager.startTask();

const code = await WindowErrorUtility.captureErrorAsync<string>(
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;
}

(<Document>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<string>(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'));
}
}
Expand Down
1 change: 1 addition & 0 deletions packages/happy-dom/src/window/IHappyDOMOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export default interface IHappyDOMOptions {
disableCSSFileLoading?: boolean;
disableIframePageLoading?: boolean;
disableComputedStyleRendering?: boolean;
disableErrorCapturing?: boolean;
enableFileSystemHttpRequests?: boolean;
navigator?: {
userAgent?: string;
Expand Down
1 change: 1 addition & 0 deletions packages/happy-dom/src/window/IHappyDOMSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export default interface IHappyDOMSettings {
disableCSSFileLoading: boolean;
disableIframePageLoading: boolean;
disableComputedStyleRendering: boolean;
disableErrorCapturing: boolean;
enableFileSystemHttpRequests: boolean;
navigator: {
userAgent: string;
Expand Down
28 changes: 21 additions & 7 deletions packages/happy-dom/src/window/Window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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; ${
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
}
}
});
}
Expand Down
28 changes: 1 addition & 27 deletions packages/happy-dom/src/window/WindowErrorUtility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(
elementOrWindow: IWindow | IElement,
callback: () => Promise<T>,
cleanup?: () => void
): Promise<T | null> {
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.
Expand All @@ -43,7 +17,7 @@ export default class WindowErrorUtility {
* @param [cleanup] Cleanup callback on error.
* @returns Result.
*/
public static captureErrorSync<T>(
public static captureError<T>(
elementOrWindow: IWindow | IElement,
callback: () => T,
cleanup?: () => void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <IHTMLScriptElement>document.createElement('script');

window.happyDOM.settings.disableErrorCapturing = true;

element.text = 'globalThis.test = /;';

expect(() => {
document.body.appendChild(element);
}).toThrow(new TypeError('Invalid regular expression: missing /'));
});
});
});
Loading

0 comments on commit aec6034

Please sign in to comment.