From 9a64097d31c9d542bbd17c37b7e166b301554787 Mon Sep 17 00:00:00 2001 From: David Ortner Date: Wed, 28 Aug 2024 02:13:32 +0200 Subject: [PATCH] chore: [#1508] Fixes problem where the release build failed --- .../src/GlobalRegistrator.ts | 2 +- packages/happy-dom/src/ClassMethodBinder.ts | 17 +- packages/happy-dom/src/PropertySymbol.ts | 17 +- .../async-task-manager/AsyncTaskManager.ts | 110 +-- packages/happy-dom/src/browser/Browser.ts | 7 + .../happy-dom/src/browser/BrowserFrame.ts | 10 +- .../src/browser/DefaultBrowserSettings.ts | 3 +- .../detached-browser/DetachedBrowser.ts | 7 + .../detached-browser/DetachedBrowserFrame.ts | 10 +- .../happy-dom/src/browser/types/IBrowser.ts | 3 + .../src/browser/types/IBrowserFrame.ts | 3 - .../src/browser/types/IBrowserSettings.ts | 1 + .../utilities/BrowserExceptionObserver.ts | 125 ++++ .../BrowserFrameExceptionObserver.ts | 111 --- .../browser/utilities/BrowserFrameFactory.ts | 68 +- .../utilities/BrowserFrameNavigator.ts | 98 +-- packages/happy-dom/src/clipboard/Clipboard.ts | 34 +- .../AbstractCSSStyleDeclaration.ts | 8 +- .../CSSStyleDeclarationElementStyle.ts | 13 +- .../CSSMeasurementConverter.ts | 14 +- .../custom-element/CustomElementRegistry.ts | 62 +- .../dom-implementation/DOMImplementation.ts | 25 +- .../happy-dom/src/dom-parser/DOMParser.ts | 27 +- packages/happy-dom/src/event/Event.ts | 103 ++- packages/happy-dom/src/event/EventTarget.ts | 385 ++++++----- .../happy-dom/src/event/IEventListener.ts | 13 - packages/happy-dom/src/event/IMessagePort.ts | 26 - packages/happy-dom/src/event/MessagePort.ts | 3 +- .../happy-dom/src/event/TEventListener.ts | 6 + .../src/event/TEventListenerFunction.ts | 5 + .../src/event/TEventListenerObject.ts | 7 + .../happy-dom/src/event/events/CustomEvent.ts | 22 +- .../src/event/events/IErrorEventInit.ts | 1 - .../src/event/events/IMessageEventInit.ts | 4 +- .../src/event/events/MessageEvent.ts | 4 +- .../happy-dom/src/fetch/AbortController.ts | 12 +- packages/happy-dom/src/fetch/AbortSignal.ts | 34 +- packages/happy-dom/src/fetch/Fetch.ts | 60 +- packages/happy-dom/src/fetch/Request.ts | 138 ++-- packages/happy-dom/src/fetch/ResourceFetch.ts | 5 +- packages/happy-dom/src/fetch/Response.ts | 125 ++-- packages/happy-dom/src/fetch/SyncFetch.ts | 34 +- .../multipart/MultipartFormDataParser.ts | 21 +- .../src/fetch/multipart/MultipartReader.ts | 9 +- .../src/fetch/utilities/FetchBodyUtility.ts | 31 +- packages/happy-dom/src/file/FileReader.ts | 28 +- packages/happy-dom/src/history/History.ts | 45 +- packages/happy-dom/src/index.ts | 4 +- packages/happy-dom/src/location/Location.ts | 68 +- .../src/match-media/MediaQueryItem.ts | 57 +- .../src/match-media/MediaQueryList.ts | 32 +- .../src/match-media/MediaQueryParser.ts | 10 +- .../src/mutation-observer/MutationObserver.ts | 42 +- packages/happy-dom/src/navigator/Navigator.ts | 26 +- packages/happy-dom/src/nodes/NodeFactory.ts | 2 +- .../happy-dom/src/nodes/document/Document.ts | 118 ++-- .../document/DocumentReadyStateManager.ts | 23 +- .../happy-dom/src/nodes/element/Element.ts | 73 +- .../src/nodes/element/NamedNodeMap.ts | 4 + .../nodes/element/NamedNodeMapProxyFactory.ts | 6 - .../html-anchor-element/HTMLAnchorElement.ts | 12 +- .../html-area-element/HTMLAreaElement.ts | 18 +- .../src/nodes/html-audio-element/Audio.ts | 6 + .../html-button-element/HTMLButtonElement.ts | 60 +- .../html-canvas-element/HTMLCanvasElement.ts | 8 +- .../src/nodes/html-document/HTMLDocument.ts | 10 +- .../src/nodes/html-element/HTMLElement.ts | 233 +++---- .../html-form-element/HTMLFormElement.ts | 16 +- .../html-iframe-element/HTMLIFrameElement.ts | 73 +- .../src/nodes/html-image-element/Image.ts | 6 + .../html-input-element/HTMLInputElement.ts | 113 ++-- .../html-label-element/HTMLLabelElement.ts | 27 +- .../html-link-element/HTMLLinkElement.ts | 31 +- .../html-media-element/HTMLMediaElement.ts | 27 +- .../nodes/html-media-element/MediaStream.ts | 6 + .../html-media-element/MediaStreamTrack.ts | 6 + .../src/nodes/html-media-element/TextTrack.ts | 6 + .../nodes/html-media-element/TextTrackCue.ts | 6 + .../src/nodes/html-media-element/VTTCue.ts | 19 +- .../html-meter-element/HTMLMeterElement.ts | 14 +- .../HTMLProgressElement.ts | 6 +- .../html-script-element/HTMLScriptElement.ts | 70 +- .../html-select-element/HTMLSelectElement.ts | 11 +- .../html-table-element/HTMLTableElement.ts | 17 +- .../HTMLTableRowElement.ts | 11 +- .../HTMLTextAreaElement.ts | 3 +- .../html-track-element/HTMLTrackElement.ts | 2 +- packages/happy-dom/src/nodes/node/Node.ts | 54 +- packages/happy-dom/src/nodes/node/NodeList.ts | 18 +- packages/happy-dom/src/nodes/text/Text.ts | 3 +- .../happy-dom/src/permissions/Permissions.ts | 18 +- .../src/query-selector/QuerySelector.ts | 25 +- packages/happy-dom/src/range/Range.ts | 62 +- packages/happy-dom/src/selection/Selection.ts | 44 +- .../happy-dom/src/window/BrowserWindow.ts | 640 +++++++++--------- .../src/window/VMGlobalPropertyScript.ts | 1 - .../src/window/WindowBrowserContext.ts | 114 ++++ .../src/window/WindowBrowserSettingsReader.ts | 55 -- .../src/window/WindowClassExtender.ts | 312 +++++++++ .../src/window/WindowPageOpenUtility.ts | 12 +- .../src/xml-http-request/XMLHttpRequest.ts | 75 +- packages/happy-dom/test/event/Event.test.ts | 41 +- .../happy-dom/test/event/EventTarget.test.ts | 67 +- .../test/fetch/AbortController.test.ts | 12 +- .../happy-dom/test/fetch/AbortSignal.test.ts | 19 +- packages/happy-dom/test/fetch/Fetch.test.ts | 27 +- packages/happy-dom/test/fetch/Request.test.ts | 8 +- .../happy-dom/test/fetch/SyncFetch.test.ts | 6 +- .../happy-dom/test/history/History.test.ts | 72 +- .../test/match-media/MediaQueryList.test.ts | 398 +++++------ .../MutationObserver.test.ts | 34 +- .../test/nodes/document/Document.test.ts | 45 +- .../HTMLButtonElement.test.ts | 11 +- .../CanvasCaptureMediaStreamTrack.test.ts | 26 +- .../nodes/html-element/HTMLElement.test.ts | 33 +- .../html-element/HTMLElementUtility.test.ts | 99 ++- .../nodes/html-image-element/Image.test.ts | 8 + .../HTMLInputElement.test.ts | 20 + .../HTMLLabelElement.test.ts | 26 +- .../html-link-element/HTMLLinkElement.test.ts | 17 +- .../HTMLMediaElement.test.ts | 11 + .../html-media-element/MediaStream.test.ts | 63 +- .../MediaStreamTrack.test.ts | 33 +- .../html-media-element/RemotePlayback.test.ts | 17 +- .../html-media-element/TextTrack.test.ts | 42 +- .../html-media-element/TextTrackCue.test.ts | 27 +- .../html-media-element/TextTrackList.test.ts | 40 +- .../HTMLScriptElement.test.ts | 17 +- .../happy-dom/test/nodes/node/Node.test.ts | 61 +- .../test/window/BrowserWindow.test.ts | 143 +++- packages/integration-test/test/index.js | 9 +- .../test/tests/Browser.test.js | 11 +- ...st.js => BrowserExceptionObserver.test.js} | 6 +- 133 files changed, 3597 insertions(+), 2463 deletions(-) create mode 100644 packages/happy-dom/src/browser/utilities/BrowserExceptionObserver.ts delete mode 100644 packages/happy-dom/src/browser/utilities/BrowserFrameExceptionObserver.ts delete mode 100644 packages/happy-dom/src/event/IEventListener.ts delete mode 100644 packages/happy-dom/src/event/IMessagePort.ts create mode 100644 packages/happy-dom/src/event/TEventListener.ts create mode 100644 packages/happy-dom/src/event/TEventListenerFunction.ts create mode 100644 packages/happy-dom/src/event/TEventListenerObject.ts create mode 100644 packages/happy-dom/src/window/WindowBrowserContext.ts delete mode 100644 packages/happy-dom/src/window/WindowBrowserSettingsReader.ts create mode 100644 packages/happy-dom/src/window/WindowClassExtender.ts rename packages/integration-test/test/tests/{BrowserFrameExceptionObserver.test.js => BrowserExceptionObserver.test.js} (94%) diff --git a/packages/global-registrator/src/GlobalRegistrator.ts b/packages/global-registrator/src/GlobalRegistrator.ts index 5c9afce9d..3da895d9a 100644 --- a/packages/global-registrator/src/GlobalRegistrator.ts +++ b/packages/global-registrator/src/GlobalRegistrator.ts @@ -80,7 +80,7 @@ export default class GlobalRegistrator { } // Set owner window on document to global - global.document[PropertySymbol.ownerWindow] = global; + global.document[PropertySymbol.window] = global; global.document[PropertySymbol.defaultView] = global; } diff --git a/packages/happy-dom/src/ClassMethodBinder.ts b/packages/happy-dom/src/ClassMethodBinder.ts index 96f84b2c4..8bbc3ac40 100644 --- a/packages/happy-dom/src/ClassMethodBinder.ts +++ b/packages/happy-dom/src/ClassMethodBinder.ts @@ -10,11 +10,12 @@ export default class ClassMethodBinder { * @param [options] Options. * @param [options.bindSymbols] Bind symbol methods. * @param [options.forwardToPrototype] Forwards the method calls to the prototype. This makes it possible for test tools to override methods on the prototype (e.g. Object.defineProperty(HTMLCollection.prototype, 'item', {})). + * @param [options.proxy] Bind methods using a proxy. */ public static bindMethods( target: Object, classes: any[], - options?: { bindSymbols?: boolean; forwardToPrototype?: boolean } + options?: { bindSymbols?: boolean; forwardToPrototype?: boolean; proxy?: any } ): void { for (const _class of classes) { const propertyDescriptors = Object.getOwnPropertyDescriptors(_class.prototype); @@ -26,6 +27,8 @@ export default class ClassMethodBinder { } } + const scope = options?.proxy ? options.proxy : target; + if (options?.forwardToPrototype) { for (const key of keys) { const descriptor = propertyDescriptors[key]; @@ -34,11 +37,11 @@ export default class ClassMethodBinder { ...descriptor, get: descriptor.get && - (() => Object.getOwnPropertyDescriptor(_class.prototype, key).get.call(target)), + (() => Object.getOwnPropertyDescriptor(_class.prototype, key).get.call(scope)), set: descriptor.set && ((newValue) => - Object.getOwnPropertyDescriptor(_class.prototype, key).set.call(target, newValue)) + Object.getOwnPropertyDescriptor(_class.prototype, key).set.call(scope, newValue)) }); } else if ( key !== 'constructor' && @@ -47,7 +50,7 @@ export default class ClassMethodBinder { ) { Object.defineProperty(target, key, { ...descriptor, - value: (...args) => _class.prototype[key].apply(target, args) + value: (...args) => _class.prototype[key].apply(scope, args) }); } } @@ -57,8 +60,8 @@ export default class ClassMethodBinder { if (descriptor.get || descriptor.set) { Object.defineProperty(target, key, { ...descriptor, - get: descriptor.get?.bind(target), - set: descriptor.set?.bind(target) + get: descriptor.get?.bind(scope), + set: descriptor.set?.bind(scope) }); } else if ( key !== 'constructor' && @@ -67,7 +70,7 @@ export default class ClassMethodBinder { ) { Object.defineProperty(target, key, { ...descriptor, - value: descriptor.value.bind(target) + value: descriptor.value.bind(scope) }); } } diff --git a/packages/happy-dom/src/PropertySymbol.ts b/packages/happy-dom/src/PropertySymbol.ts index d258e1f11..c92dbb57e 100644 --- a/packages/happy-dom/src/PropertySymbol.ts +++ b/packages/happy-dom/src/PropertySymbol.ts @@ -5,7 +5,6 @@ export const bodyBuffer = Symbol('bodyBuffer'); export const buffer = Symbol('buffer'); export const cachedResponse = Symbol('cachedResponse'); export const callbacks = Symbol('callbacks'); -export const captureEventListenerCount = Symbol('captureEventListenerCount'); export const checked = Symbol('checked'); export const childNodes = Symbol('childNodes'); export const children = Symbol('children'); @@ -29,7 +28,7 @@ export const evaluateCSS = Symbol('evaluateCSS'); export const evaluateScript = Symbol('evaluateScript'); export const exceptionObserver = Symbol('exceptionObserver'); export const formNode = Symbol('formNode'); -export const happyDOMSettingsID = Symbol('happyDOMSettingsID'); +export const internalId = Symbol('internalId'); export const height = Symbol('height'); export const immediatePropagationStopped = Symbol('immediatePropagationStopped'); export const isFirstWrite = Symbol('isFirstWrite'); @@ -102,7 +101,6 @@ export const specified = Symbol('specified'); export const adoptedStyleSheets = Symbol('adoptedStyleSheets'); export const implementation = Symbol('implementation'); export const readyState = Symbol('readyState'); -export const ownerWindow = Symbol('ownerWindow'); export const publicId = Symbol('publicId'); export const systemId = Symbol('systemId'); export const validationMessage = Symbol('validationMessage'); @@ -220,3 +218,16 @@ export const illegalConstructor = Symbol('illegalConstructor'); export const state = Symbol('state'); export const canvas = Symbol('canvas'); export const popoverTargetElement = Symbol('popoverTargetElement'); +export const composed = Symbol('composed'); +export const bubbles = Symbol('bubbles'); +export const cancelable = Symbol('cancelable'); +export const defaultPrevented = Symbol('defaultPrevented'); +export const eventPhase = Symbol('eventPhase'); +export const timeStamp = Symbol('timeStamp'); +export const type = Symbol('type'); +export const detail = Symbol('detail'); +export const globalObject = Symbol('globalObject'); +export const destroyed = Symbol('destroyed'); +export const aborted = Symbol('aborted'); +export const browserFrames = Symbol('browserFrames'); +export const windowInternalId = Symbol('windowInternalId'); diff --git a/packages/happy-dom/src/async-task-manager/AsyncTaskManager.ts b/packages/happy-dom/src/async-task-manager/AsyncTaskManager.ts index e3a8c686d..c5cf4ba0d 100644 --- a/packages/happy-dom/src/async-task-manager/AsyncTaskManager.ts +++ b/packages/happy-dom/src/async-task-manager/AsyncTaskManager.ts @@ -1,3 +1,5 @@ +import IBrowserFrame from '../browser/types/IBrowserFrame.js'; + // We need to set this as a global constant, so that using fake timers in Jest and Vitest won't override this on the global object. const TIMER = { setTimeout: globalThis.setTimeout.bind(globalThis), @@ -10,12 +12,24 @@ const TIMER = { */ export default class AsyncTaskManager { private static taskID = 0; - private runningTasks: { [k: string]: (destroy: boolean) => void | Promise } = {}; + private runningTasks: { [k: string]: (destroy: boolean) => void } = {}; private runningTaskCount = 0; private runningTimers: NodeJS.Timeout[] = []; private runningImmediates: NodeJS.Immediate[] = []; private waitUntilCompleteTimer: NodeJS.Immediate | null = null; private waitUntilCompleteResolvers: Array<() => void> = []; + private aborted = false; + private destroyed = false; + #browserFrame: IBrowserFrame; + + /** + * Constructor. + * + * @param browserFrame Browser frame. + */ + constructor(browserFrame: IBrowserFrame) { + this.#browserFrame = browserFrame; + } /** * Returns a promise that is resolved when async tasks are complete. @@ -25,7 +39,7 @@ export default class AsyncTaskManager { public waitUntilComplete(): Promise { return new Promise((resolve) => { this.waitUntilCompleteResolvers.push(resolve); - this.endTask(this.startTask()); + this.resolveWhenComplete(); }); } @@ -33,6 +47,12 @@ export default class AsyncTaskManager { * Aborts all tasks. */ public abort(): Promise { + if (this.aborted) { + return new Promise((resolve) => { + this.waitUntilCompleteResolvers.push(resolve); + this.resolveWhenComplete(); + }); + } return this.abortAll(false); } @@ -40,6 +60,12 @@ export default class AsyncTaskManager { * Destroys the manager. */ public destroy(): Promise { + if (this.aborted) { + return new Promise((resolve) => { + this.waitUntilCompleteResolvers.push(resolve); + this.resolveWhenComplete(); + }); + } return this.abortAll(true); } @@ -49,6 +75,10 @@ export default class AsyncTaskManager { * @param timerID Timer ID. */ public startTimer(timerID: NodeJS.Timeout): void { + if (this.aborted) { + TIMER.clearTimeout(timerID); + return; + } if (this.waitUntilCompleteTimer) { TIMER.clearTimeout(this.waitUntilCompleteTimer); this.waitUntilCompleteTimer = null; @@ -62,16 +92,16 @@ export default class AsyncTaskManager { * @param timerID Timer ID. */ public endTimer(timerID: NodeJS.Timeout): void { - if (this.waitUntilCompleteTimer) { - TIMER.clearTimeout(this.waitUntilCompleteTimer); - this.waitUntilCompleteTimer = null; + if (this.aborted) { + TIMER.clearTimeout(timerID); + return; } const index = this.runningTimers.indexOf(timerID); if (index !== -1) { this.runningTimers.splice(index, 1); - } - if (!this.runningTaskCount && !this.runningTimers.length && !this.runningImmediates.length) { - this.resolveWhenComplete(); + if (!this.runningTaskCount && !this.runningTimers.length && !this.runningImmediates.length) { + this.resolveWhenComplete(); + } } } @@ -81,6 +111,10 @@ export default class AsyncTaskManager { * @param immediateID Immediate ID. */ public startImmediate(immediateID: NodeJS.Immediate): void { + if (this.aborted) { + TIMER.clearImmediate(immediateID); + return; + } if (this.waitUntilCompleteTimer) { TIMER.clearTimeout(this.waitUntilCompleteTimer); this.waitUntilCompleteTimer = null; @@ -94,16 +128,16 @@ export default class AsyncTaskManager { * @param immediateID Immediate ID. */ public endImmediate(immediateID: NodeJS.Immediate): void { - if (this.waitUntilCompleteTimer) { - TIMER.clearTimeout(this.waitUntilCompleteTimer); - this.waitUntilCompleteTimer = null; + if (this.aborted) { + TIMER.clearImmediate(immediateID); + return; } const index = this.runningImmediates.indexOf(immediateID); if (index !== -1) { this.runningImmediates.splice(index, 1); - } - if (!this.runningTaskCount && !this.runningTimers.length && !this.runningImmediates.length) { - this.resolveWhenComplete(); + if (!this.runningTaskCount && !this.runningTimers.length && !this.runningImmediates.length) { + this.resolveWhenComplete(); + } } } @@ -113,7 +147,15 @@ export default class AsyncTaskManager { * @param abortHandler Abort handler. * @returns Task ID. */ - public startTask(abortHandler?: () => void): number { + public startTask(abortHandler?: (destroy?: boolean) => void): number { + if (this.aborted) { + if (abortHandler) { + abortHandler(this.destroyed); + } + throw new this.#browserFrame.window.Error( + `Failed to execute 'startTask()' on 'AsyncTaskManager': The asynchrounous task manager has been aborted.` + ); + } if (this.waitUntilCompleteTimer) { TIMER.clearTimeout(this.waitUntilCompleteTimer); this.waitUntilCompleteTimer = null; @@ -130,16 +172,15 @@ export default class AsyncTaskManager { * @param taskID Task ID. */ public endTask(taskID: number): void { - if (this.waitUntilCompleteTimer) { - TIMER.clearTimeout(this.waitUntilCompleteTimer); - this.waitUntilCompleteTimer = null; + if (this.aborted) { + return; } if (this.runningTasks[taskID]) { delete this.runningTasks[taskID]; this.runningTaskCount--; - } - if (!this.runningTaskCount && !this.runningTimers.length && !this.runningImmediates.length) { - this.resolveWhenComplete(); + if (!this.runningTaskCount && !this.runningTimers.length && !this.runningImmediates.length) { + this.resolveWhenComplete(); + } } } @@ -186,8 +227,9 @@ export default class AsyncTaskManager { for (const resolver of resolvers) { resolver(); } + this.aborted = false; } - }); + }, 1); } /** @@ -200,6 +242,8 @@ export default class AsyncTaskManager { const runningImmediates = this.runningImmediates; const runningTasks = this.runningTasks; + this.aborted = true; + this.destroyed = destroy; this.runningTasks = {}; this.runningTaskCount = 0; this.runningImmediates = []; @@ -218,26 +262,14 @@ export default class AsyncTaskManager { TIMER.clearTimeout(timer); } - const taskPromises = []; - for (const key of Object.keys(runningTasks)) { - const returnValue = runningTasks[key](destroy); - if (returnValue instanceof Promise) { - taskPromises.push(returnValue); - } - } - - if (taskPromises.length) { - return Promise.all(taskPromises) - .then(() => this.waitUntilComplete()) - .catch((error) => { - /* eslint-disable-next-line no-console */ - console.error(error); - throw error; - }); + runningTasks[key](destroy); } // We need to wait for microtasks to complete before resolving. - return this.waitUntilComplete(); + return new Promise((resolve) => { + this.waitUntilCompleteResolvers.push(resolve); + this.resolveWhenComplete(); + }); } } diff --git a/packages/happy-dom/src/browser/Browser.ts b/packages/happy-dom/src/browser/Browser.ts index 39bafbb9b..6dfffd80a 100644 --- a/packages/happy-dom/src/browser/Browser.ts +++ b/packages/happy-dom/src/browser/Browser.ts @@ -4,6 +4,9 @@ import IOptionalBrowserSettings from './types/IOptionalBrowserSettings.js'; import BrowserSettingsFactory from './BrowserSettingsFactory.js'; import BrowserPage from './BrowserPage.js'; import IBrowser from './types/IBrowser.js'; +import BrowserExceptionObserver from './utilities/BrowserExceptionObserver.js'; +import * as PropertySymbol from '../PropertySymbol.js'; +import BrowserErrorCaptureEnum from './enums/BrowserErrorCaptureEnum.js'; /** * Browser. @@ -14,6 +17,7 @@ export default class Browser implements IBrowser { public readonly contexts: BrowserContext[]; public readonly settings: IBrowserSettings; public readonly console: Console | null; + public [PropertySymbol.exceptionObserver]: BrowserExceptionObserver | null = null; /** * Constructor. @@ -25,6 +29,9 @@ export default class Browser implements IBrowser { constructor(options?: { settings?: IOptionalBrowserSettings; console?: Console }) { this.console = options?.console || null; this.settings = BrowserSettingsFactory.createSettings(options?.settings); + if (this.settings.errorCapture === BrowserErrorCaptureEnum.processLevel) { + this[PropertySymbol.exceptionObserver] = new BrowserExceptionObserver(); + } this.contexts = [new BrowserContext(this)]; } diff --git a/packages/happy-dom/src/browser/BrowserFrame.ts b/packages/happy-dom/src/browser/BrowserFrame.ts index 6dab8732c..0561f5d29 100644 --- a/packages/happy-dom/src/browser/BrowserFrame.ts +++ b/packages/happy-dom/src/browser/BrowserFrame.ts @@ -11,8 +11,6 @@ import BrowserFrameURL from './utilities/BrowserFrameURL.js'; import BrowserFrameScriptEvaluator from './utilities/BrowserFrameScriptEvaluator.js'; import BrowserFrameNavigator from './utilities/BrowserFrameNavigator.js'; import IReloadOptions from './types/IReloadOptions.js'; -import BrowserFrameExceptionObserver from './utilities/BrowserFrameExceptionObserver.js'; -import BrowserErrorCaptureEnum from './enums/BrowserErrorCaptureEnum.js'; import Document from '../nodes/document/Document.js'; import IHistoryItem from '../history/IHistoryItem.js'; import HistoryScrollRestorationEnum from '../history/HistoryScrollRestorationEnum.js'; @@ -25,8 +23,7 @@ export default class BrowserFrame implements IBrowserFrame { public readonly parentFrame: BrowserFrame | null = null; public readonly page: BrowserPage; public readonly window: BrowserWindow; - public [PropertySymbol.asyncTaskManager] = new AsyncTaskManager(); - public [PropertySymbol.exceptionObserver]: BrowserFrameExceptionObserver | null = null; + public [PropertySymbol.asyncTaskManager] = new AsyncTaskManager(this); public [PropertySymbol.listeners]: { navigation: Array<() => void> } = { navigation: [] }; public [PropertySymbol.openerFrame]: IBrowserFrame | null = null; public [PropertySymbol.openerWindow]: BrowserWindow | CrossOriginBrowserWindow | null = null; @@ -53,9 +50,8 @@ export default class BrowserFrame implements IBrowserFrame { this.window = new BrowserWindow(this); // Attach process level error capturing. - if (page.context.browser.settings.errorCapture === BrowserErrorCaptureEnum.processLevel) { - this[PropertySymbol.exceptionObserver] = new BrowserFrameExceptionObserver(); - this[PropertySymbol.exceptionObserver].observe(this); + if (page.context.browser[PropertySymbol.exceptionObserver]) { + page.context.browser[PropertySymbol.exceptionObserver].observe(this.window); } } diff --git a/packages/happy-dom/src/browser/DefaultBrowserSettings.ts b/packages/happy-dom/src/browser/DefaultBrowserSettings.ts index 02a372ad4..31e53e97a 100644 --- a/packages/happy-dom/src/browser/DefaultBrowserSettings.ts +++ b/packages/happy-dom/src/browser/DefaultBrowserSettings.ts @@ -15,7 +15,8 @@ export default { timer: { maxTimeout: -1, maxIntervalTime: -1, - maxIntervalIterations: -1 + maxIntervalIterations: -1, + preventTimerLoops: false }, navigation: { disableMainFrameNavigation: false, diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowser.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowser.ts index faf744b80..679c47097 100644 --- a/packages/happy-dom/src/browser/detached-browser/DetachedBrowser.ts +++ b/packages/happy-dom/src/browser/detached-browser/DetachedBrowser.ts @@ -6,6 +6,9 @@ import DetachedBrowserPage from './DetachedBrowserPage.js'; import IBrowser from '../types/IBrowser.js'; import IBrowserFrame from '../types/IBrowserFrame.js'; import BrowserWindow from '../../window/BrowserWindow.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; +import BrowserErrorCaptureEnum from '../enums/BrowserErrorCaptureEnum.js'; +import BrowserExceptionObserver from '../utilities/BrowserExceptionObserver.js'; /** * Detached browser used when constructing a Window instance without a browser. @@ -20,6 +23,7 @@ export default class DetachedBrowser implements IBrowser { browserFrame: IBrowserFrame, options?: { url?: string; width?: number; height?: number } ) => BrowserWindow | null; + public [PropertySymbol.exceptionObserver]: BrowserExceptionObserver | null = null; /** * Constructor. @@ -39,6 +43,9 @@ export default class DetachedBrowser implements IBrowser { this.windowClass = windowClass; this.console = options?.console || null; this.settings = BrowserSettingsFactory.createSettings(options?.settings); + if (this.settings.errorCapture === BrowserErrorCaptureEnum.processLevel) { + this[PropertySymbol.exceptionObserver] = new BrowserExceptionObserver(); + } this.contexts = []; this.contexts.push(new DetachedBrowserContext(this)); } diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts index 67d4955c6..1f476cff6 100644 --- a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts +++ b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts @@ -10,8 +10,6 @@ import BrowserFrameScriptEvaluator from '../utilities/BrowserFrameScriptEvaluato import BrowserFrameNavigator from '../utilities/BrowserFrameNavigator.js'; import BrowserWindow from '../../window/BrowserWindow.js'; import IReloadOptions from '../types/IReloadOptions.js'; -import BrowserErrorCaptureEnum from '../enums/BrowserErrorCaptureEnum.js'; -import BrowserFrameExceptionObserver from '../utilities/BrowserFrameExceptionObserver.js'; import Document from '../../nodes/document/Document.js'; import CrossOriginBrowserWindow from '../../window/CrossOriginBrowserWindow.js'; import IHistoryItem from '../../history/IHistoryItem.js'; @@ -26,8 +24,7 @@ export default class DetachedBrowserFrame implements IBrowserFrame { public readonly page: DetachedBrowserPage; // Needs to be injected from the outside when the browser frame is constructed. public window: BrowserWindow; - public [PropertySymbol.asyncTaskManager] = new AsyncTaskManager(); - public [PropertySymbol.exceptionObserver]: BrowserFrameExceptionObserver | null = null; + public [PropertySymbol.asyncTaskManager] = new AsyncTaskManager(this); public [PropertySymbol.listeners]: { navigation: Array<() => void> } = { navigation: [] }; public [PropertySymbol.openerFrame]: IBrowserFrame | null = null; public [PropertySymbol.openerWindow]: BrowserWindow | CrossOriginBrowserWindow | null = null; @@ -57,9 +54,8 @@ export default class DetachedBrowserFrame implements IBrowserFrame { } // Attach process level error capturing. - if (page.context.browser.settings.errorCapture === BrowserErrorCaptureEnum.processLevel) { - this[PropertySymbol.exceptionObserver] = new BrowserFrameExceptionObserver(); - this[PropertySymbol.exceptionObserver].observe(this); + if (page.context.browser[PropertySymbol.exceptionObserver]) { + page.context.browser[PropertySymbol.exceptionObserver].observe(this.window); } } diff --git a/packages/happy-dom/src/browser/types/IBrowser.ts b/packages/happy-dom/src/browser/types/IBrowser.ts index 34fd479c4..bba211de3 100644 --- a/packages/happy-dom/src/browser/types/IBrowser.ts +++ b/packages/happy-dom/src/browser/types/IBrowser.ts @@ -1,6 +1,8 @@ import IBrowserContext from './IBrowserContext.js'; import IBrowserPage from './IBrowserPage.js'; import IBrowserSettings from './IBrowserSettings.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; +import BrowserExceptionObserver from '../utilities/BrowserExceptionObserver.js'; /** * Browser. @@ -12,6 +14,7 @@ export default interface IBrowser { readonly contexts: IBrowserContext[]; readonly settings: IBrowserSettings; readonly console: Console | null; + readonly [PropertySymbol.exceptionObserver]: BrowserExceptionObserver | null; /** * Aborts all ongoing operations and destroys the browser. diff --git a/packages/happy-dom/src/browser/types/IBrowserFrame.ts b/packages/happy-dom/src/browser/types/IBrowserFrame.ts index 09bfb893f..0e80bc603 100644 --- a/packages/happy-dom/src/browser/types/IBrowserFrame.ts +++ b/packages/happy-dom/src/browser/types/IBrowserFrame.ts @@ -7,7 +7,6 @@ import Response from '../../fetch/Response.js'; import IGoToOptions from './IGoToOptions.js'; import { Script } from 'vm'; import IReloadOptions from './IReloadOptions.js'; -import BrowserFrameExceptionObserver from '../utilities/BrowserFrameExceptionObserver.js'; import CrossOriginBrowserWindow from '../../window/CrossOriginBrowserWindow.js'; import IHistoryItem from '../../history/IHistoryItem.js'; @@ -24,7 +23,6 @@ export default interface IBrowserFrame { url: string; [PropertySymbol.history]: IHistoryItem[]; [PropertySymbol.asyncTaskManager]: AsyncTaskManager; - [PropertySymbol.exceptionObserver]: BrowserFrameExceptionObserver | null; [PropertySymbol.listeners]: { navigation: Array<() => void> }; [PropertySymbol.openerFrame]: IBrowserFrame | null; [PropertySymbol.openerWindow]: BrowserWindow | CrossOriginBrowserWindow | null; @@ -46,7 +44,6 @@ export default interface IBrowserFrame { abort(): Promise; /** - * Evaluates code or a VM Script in the page's context. * * @param script Script. * @returns Result. diff --git a/packages/happy-dom/src/browser/types/IBrowserSettings.ts b/packages/happy-dom/src/browser/types/IBrowserSettings.ts index 7d5ddf2c1..ed8bb17ee 100644 --- a/packages/happy-dom/src/browser/types/IBrowserSettings.ts +++ b/packages/happy-dom/src/browser/types/IBrowserSettings.ts @@ -25,6 +25,7 @@ export default interface IBrowserSettings { maxTimeout: number; maxIntervalTime: number; maxIntervalIterations: number; + preventTimerLoops: boolean; }; /** diff --git a/packages/happy-dom/src/browser/utilities/BrowserExceptionObserver.ts b/packages/happy-dom/src/browser/utilities/BrowserExceptionObserver.ts new file mode 100644 index 000000000..fbc68bb03 --- /dev/null +++ b/packages/happy-dom/src/browser/utilities/BrowserExceptionObserver.ts @@ -0,0 +1,125 @@ +import BrowserWindow from '../../window/BrowserWindow.js'; + +/** + * Listens for uncaught exceptions coming from Happy DOM on the running Node process and dispatches error events on the Window instance. + */ +export default class BrowserExceptionObserver { + private static listenerCount = 0; + private observedWindows: BrowserWindow[] = []; + private uncaughtExceptionListener: ( + error: Error, + origin: 'uncaughtException' | 'unhandledRejection' + ) => void | null; + private uncaughtRejectionListener: (error: Error) => void | null; + + /** + * Observes the Node process for uncaught exceptions. + * + * @param window Browser window. + */ + public observe(window: BrowserWindow): void { + if (this.observedWindows.includes(window)) { + throw new Error('Browser window is already being observed.'); + } + + this.observedWindows.push(window); + + if (this.uncaughtExceptionListener) { + return; + } + + this.uncaughtExceptionListener = ( + error: unknown, + origin: 'uncaughtException' | 'unhandledRejection' + ): void => { + if (origin === 'unhandledRejection') { + return; + } + + let targetWindow: BrowserWindow | null = null; + + for (const window of this.observedWindows) { + if (error instanceof window.Error || error instanceof window.DOMException) { + targetWindow = window; + break; + } + } + + if (targetWindow) { + targetWindow.console.error(error); + targetWindow.dispatchEvent( + new targetWindow.ErrorEvent('error', { + error: error, + message: (error).message + }) + ); + } else if ( + process.listenerCount('uncaughtException') === + (this.constructor).listenerCount + ) { + // eslint-disable-next-line no-console + console.error(error); + // Exit if there are no other listeners handling the error. + process.exit(1); + } + }; + + // The "uncaughtException" event is not always triggered for unhandled rejections. + // Therefore we want to use the "unhandledRejection" event as well. + this.uncaughtRejectionListener = (error: unknown): void => { + let targetWindow: BrowserWindow | null = null; + + for (const window of this.observedWindows) { + if (error instanceof window.Error || error instanceof window.DOMException) { + targetWindow = window; + break; + } + } + + if (targetWindow) { + targetWindow.console.error(error); + targetWindow.dispatchEvent( + new targetWindow.ErrorEvent('error', { + error: error, + message: (error).message + }) + ); + } else if ( + process.listenerCount('unhandledRejection') === + (this.constructor).listenerCount + ) { + // eslint-disable-next-line no-console + console.error(error); + // Exit if there are no other listeners handling the error. + process.exit(1); + } + }; + + (this.constructor).listenerCount++; + process.on('uncaughtException', this.uncaughtExceptionListener); + process.on('unhandledRejection', this.uncaughtRejectionListener); + } + + /** + * Disconnects observer. + * + * @param window Browser window. + */ + public disconnect(window: BrowserWindow): void { + const index = this.observedWindows.indexOf(window); + + if (index === -1) { + return; + } + + this.observedWindows.splice(index, 1); + + if (this.observedWindows.length === 0 && this.uncaughtExceptionListener) { + (this.constructor).listenerCount--; + process.off('uncaughtException', this.uncaughtExceptionListener); + process.off('unhandledRejection', this.uncaughtRejectionListener); + this.uncaughtExceptionListener = null; + this.uncaughtRejectionListener = null; + } + } +} diff --git a/packages/happy-dom/src/browser/utilities/BrowserFrameExceptionObserver.ts b/packages/happy-dom/src/browser/utilities/BrowserFrameExceptionObserver.ts deleted file mode 100644 index 8ffb4d325..000000000 --- a/packages/happy-dom/src/browser/utilities/BrowserFrameExceptionObserver.ts +++ /dev/null @@ -1,111 +0,0 @@ -import IBrowserFrame from '../types/IBrowserFrame.js'; - -/** - * Listens for uncaught exceptions coming from Happy DOM on the running Node process and dispatches error events on the Window instance. - */ -export default class BrowserFrameExceptionObserver { - private static listenerCount = 0; - private browserFrame: IBrowserFrame | null = null; - private uncaughtExceptionListener: ( - error: Error, - origin: 'uncaughtException' | 'unhandledRejection' - ) => void | null = null; - private uncaughtRejectionListener: (error: Error) => void | null = null; - - /** - * Observes the Node process for uncaught exceptions. - * - * @param browserFrame Browser frame. - */ - public observe(browserFrame: IBrowserFrame): void { - if (this.browserFrame) { - throw new Error('Already observing.'); - } - - this.browserFrame = browserFrame; - - (this.constructor).listenerCount++; - - this.uncaughtExceptionListener = ( - error: unknown, - origin: 'uncaughtException' | 'unhandledRejection' - ) => { - if (origin === 'unhandledRejection') { - return; - } - - if (!this.browserFrame.window) { - throw new Error( - 'Browser frame was not closed correctly. Window is undefined on browser frame, but exception observer is still watching.' - ); - } - - if ( - error instanceof this.browserFrame.window.Error || - error instanceof this.browserFrame.window.DOMException - ) { - this.browserFrame.window.console.error(error); - this.browserFrame.window.dispatchEvent( - new this.browserFrame.window.ErrorEvent('error', { error, message: error.message }) - ); - } else if ( - process.listenerCount('uncaughtException') === - (this.constructor).listenerCount - ) { - // eslint-disable-next-line no-console - console.error(error); - // Exit if there are no other listeners handling the error. - process.exit(1); - } - }; - - // The "uncaughtException" event is not always triggered for unhandled rejections. - // Therefore we want to use the "unhandledRejection" event as well. - this.uncaughtRejectionListener = (error: unknown) => { - if (!this.browserFrame.window) { - throw new Error( - 'Browser frame was not closed correctly. Window is undefined on browser frame, but exception observer is still watching.' - ); - } - - if ( - error instanceof this.browserFrame.window.Error || - error instanceof this.browserFrame.window.DOMException - ) { - this.browserFrame.window.console.error(error); - this.browserFrame.window.dispatchEvent( - new this.browserFrame.window.ErrorEvent('error', { error, message: error.message }) - ); - } else if ( - process.listenerCount('unhandledRejection') === - (this.constructor).listenerCount - ) { - // eslint-disable-next-line no-console - console.error(error); - // Exit if there are no other listeners handling the error. - process.exit(1); - } - }; - - process.on('uncaughtException', this.uncaughtExceptionListener); - process.on('unhandledRejection', this.uncaughtRejectionListener); - } - - /** - * Disconnects observer. - */ - public disconnect(): void { - if (!this.browserFrame) { - return; - } - - (this.constructor).listenerCount--; - - process.off('uncaughtException', this.uncaughtExceptionListener); - process.off('unhandledRejection', this.uncaughtRejectionListener); - - this.uncaughtExceptionListener = null; - this.uncaughtRejectionListener = null; - this.browserFrame = null; - } -} diff --git a/packages/happy-dom/src/browser/utilities/BrowserFrameFactory.ts b/packages/happy-dom/src/browser/utilities/BrowserFrameFactory.ts index 7400b8ee9..c1ccc9caf 100644 --- a/packages/happy-dom/src/browser/utilities/BrowserFrameFactory.ts +++ b/packages/happy-dom/src/browser/utilities/BrowserFrameFactory.ts @@ -2,6 +2,7 @@ import IBrowserFrame from '../types/IBrowserFrame.js'; import * as PropertySymbol from '../../PropertySymbol.js'; import BrowserWindow from '../../window/BrowserWindow.js'; import IBrowserPage from '../types/IBrowserPage.js'; + /** * Browser frame factory. */ @@ -27,7 +28,9 @@ export default class BrowserFrameFactory { * @param frame Frame. */ public static destroyFrame(frame: IBrowserFrame): Promise { - // Using Promise instead of async/await to prevent microtask + const exceptionObserver = frame.page.context.browser[PropertySymbol.exceptionObserver]; + + // Using Promise instead of async/await to prevent usage of a microtask return new Promise((resolve, reject) => { if (!frame.window) { resolve(); @@ -42,54 +45,43 @@ export default class BrowserFrameFactory { } if (!frame.childFrames.length) { - if (frame.window && frame.window[PropertySymbol.mutationObservers]) { - for (const mutationObserver of frame.window[PropertySymbol.mutationObservers]) { - mutationObserver.disconnect(); - } - frame.window[PropertySymbol.mutationObservers] = []; - } - return frame[PropertySymbol.asyncTaskManager] + frame[PropertySymbol.asyncTaskManager] .destroy() .then(() => { - frame[PropertySymbol.exceptionObserver]?.disconnect(); - if (frame.window) { - frame.window[PropertySymbol.destroy](); - (frame.page) = null; - (frame.window) = null; - frame[PropertySymbol.openerFrame] = null; - frame[PropertySymbol.openerWindow] = null; + if (exceptionObserver && frame.window) { + exceptionObserver.disconnect(frame.window); } resolve(); }) .catch((error) => reject(error)); + if (frame.window) { + frame.window[PropertySymbol.destroy](); + (frame.page) = null; + (frame.window) = null; + frame[PropertySymbol.openerFrame] = null; + frame[PropertySymbol.openerWindow] = null; + } + return; } Promise.all(frame.childFrames.slice().map((childFrame) => this.destroyFrame(childFrame))) .then(() => { - if (frame.window && frame.window[PropertySymbol.mutationObservers]) { - for (const mutationObserver of frame.window[PropertySymbol.mutationObservers]) { - mutationObserver.disconnect(); - } - frame.window[PropertySymbol.mutationObservers] = []; - } - return frame[PropertySymbol.asyncTaskManager].destroy().then(() => { - frame[PropertySymbol.exceptionObserver]?.disconnect(); - if (frame.window) { - const listeners = frame[PropertySymbol.listeners]; - - frame.window[PropertySymbol.destroy](); - (frame.page) = null; - (frame.window) = null; - frame[PropertySymbol.listeners] = null; - frame[PropertySymbol.openerFrame] = null; - frame[PropertySymbol.openerWindow] = null; - - for (const listener of listeners.navigation) { - listener(); + frame[PropertySymbol.asyncTaskManager] + .destroy() + .then(() => { + if (exceptionObserver && frame.window) { + exceptionObserver.disconnect(frame.window); } - } - resolve(); - }); + resolve(); + }) + .catch((error) => reject(error)); + if (frame.window) { + frame.window[PropertySymbol.destroy](); + (frame.page) = null; + (frame.window) = null; + frame[PropertySymbol.openerFrame] = null; + frame[PropertySymbol.openerWindow] = null; + } }) .catch((error) => reject(error)); }); diff --git a/packages/happy-dom/src/browser/utilities/BrowserFrameNavigator.ts b/packages/happy-dom/src/browser/utilities/BrowserFrameNavigator.ts index bb1156bb6..658a8e268 100644 --- a/packages/happy-dom/src/browser/utilities/BrowserFrameNavigator.ts +++ b/packages/happy-dom/src/browser/utilities/BrowserFrameNavigator.ts @@ -4,13 +4,10 @@ import IGoToOptions from '../types/IGoToOptions.js'; import Response from '../../fetch/Response.js'; import DocumentReadyStateManager from '../../nodes/document/DocumentReadyStateManager.js'; import BrowserWindow from '../../window/BrowserWindow.js'; -import WindowErrorUtility from '../../window/WindowErrorUtility.js'; -import AbortController from '../../fetch/AbortController.js'; import BrowserFrameFactory from './BrowserFrameFactory.js'; import BrowserFrameURL from './BrowserFrameURL.js'; import BrowserFrameValidator from './BrowserFrameValidator.js'; import AsyncTaskManager from '../../async-task-manager/AsyncTaskManager.js'; -import BrowserErrorCaptureEnum from '../enums/BrowserErrorCaptureEnum.js'; import FormData from '../../form-data/FormData.js'; import HistoryScrollRestorationEnum from '../../history/HistoryScrollRestorationEnum.js'; import IHistoryItem from '../../history/IHistoryItem.js'; @@ -46,6 +43,7 @@ export default class BrowserFrameNavigator { disableHistory?: boolean; }): Promise { const { windowClass, frame, url, formData, method, goToOptions, disableHistory } = options; + const exceptionObserver = frame.page.context.browser[PropertySymbol.exceptionObserver]; const referrer = goToOptions?.referrer || frame.window.location.origin; const targetURL = BrowserFrameURL.getRelativeURL(frame, url); const resolveNavigationListeners = (): void => { @@ -60,6 +58,7 @@ export default class BrowserFrameNavigator { throw new Error('The frame has been destroyed, the "window" property is not set.'); } + // Javascript protocol if (targetURL.protocol === 'javascript:') { if (frame && !frame.page.context.browser.settings.disableJavaScriptEvaluation) { const readyStateManager = (< @@ -67,30 +66,26 @@ export default class BrowserFrameNavigator { >(frame.window))[PropertySymbol.readyStateManager]; readyStateManager.startTask(); - - // The browser will wait for the next tick before executing the script. - await new Promise((resolve) => frame.page.mainFrame.window.setTimeout(resolve)); - const code = '//# sourceURL=' + frame.url + '\n' + targetURL.href.replace('javascript:', ''); - if ( - frame.page.context.browser.settings.disableErrorCapturing || - frame.page.context.browser.settings.errorCapture !== BrowserErrorCaptureEnum.tryAndCatch - ) { - frame.window.eval(code); - } else { - WindowErrorUtility.captureError(frame.window, () => frame.window.eval(code)); - } + // The browser will wait for the next tick before executing the script. + // Fixes issue where evaluating the response can throw an error. + // By using requestAnimationFrame() the error will not reject the promise. + // The error will be caught by process error level listener or a try and catch in the requestAnimationFrame(). + frame.window.requestAnimationFrame(() => frame.window.eval(code)); - readyStateManager.endTask(); + // We need to wait for the next tick before resolving navigation listeners and ending the ready state task. + await new Promise((resolve) => frame.window.setTimeout(() => resolve(null))); + readyStateManager.endTask(); resolveNavigationListeners(); } return null; } + // Validate navigation if (!BrowserFrameValidator.validateCrossOriginPolicy(frame, targetURL)) { return null; } @@ -103,16 +98,6 @@ export default class BrowserFrameNavigator { return null; } - const width = frame.window.innerWidth; - const height = frame.window.innerHeight; - const devicePixelRatio = frame.window.devicePixelRatio; - const parentWindow = frame.parentFrame ? frame.parentFrame.window : frame.page.mainFrame.window; - const topWindow = frame.page.mainFrame.window; - - for (const childFrame of frame.childFrames) { - BrowserFrameFactory.destroyFrame(childFrame); - } - // History management. if (!disableHistory) { const history = frame[PropertySymbol.history]; @@ -138,36 +123,63 @@ export default class BrowserFrameNavigator { }); } - (frame.childFrames) = []; - frame.window[PropertySymbol.destroy](); - frame[PropertySymbol.asyncTaskManager].destroy(); - frame[PropertySymbol.asyncTaskManager] = new AsyncTaskManager(); + // Store current Window state + const previousWindow = frame.window; + const previousAsyncTaskManager = frame[PropertySymbol.asyncTaskManager]; + const width = previousWindow.innerWidth; + const height = previousWindow.innerHeight; + const devicePixelRatio = previousWindow.devicePixelRatio; + const parentWindow = frame.parentFrame ? frame.parentFrame.window : frame.page.mainFrame.window; + const topWindow = frame.page.mainFrame.window; + // Create new Window + frame[PropertySymbol.asyncTaskManager] = new AsyncTaskManager(frame); (frame.window) = new windowClass(frame, { url: targetURL.href, width, height }); frame.window[PropertySymbol.parent] = parentWindow; frame.window[PropertySymbol.top] = topWindow; (frame.window.devicePixelRatio) = devicePixelRatio; + if (exceptionObserver) { + exceptionObserver.observe(frame.window); + } + if (referrer) { frame.window.document[PropertySymbol.referrer] = referrer; } + // Destroy child frames and Window + const destroyTaskID = frame[PropertySymbol.asyncTaskManager].startTask(); + const destroyWindowAndAsyncTaskManager = (): void => { + previousAsyncTaskManager.destroy().then(() => { + if (exceptionObserver) { + exceptionObserver.disconnect(previousWindow); + } + frame[PropertySymbol.asyncTaskManager].endTask(destroyTaskID); + }); + + previousWindow[PropertySymbol.destroy](); + }; + + if (frame.childFrames.length) { + Promise.all( + frame.childFrames.map((childFrame) => BrowserFrameFactory.destroyFrame(childFrame)) + ).then(destroyWindowAndAsyncTaskManager); + } else { + destroyWindowAndAsyncTaskManager(); + } + + // About protocol if (targetURL.protocol === 'about:') { await new Promise((resolve) => frame.page.mainFrame.window.requestAnimationFrame(resolve)); resolveNavigationListeners(); return null; } + // Start navigation const readyStateManager = (<{ [PropertySymbol.readyStateManager]: DocumentReadyStateManager }>( (frame.window) ))[PropertySymbol.readyStateManager]; - - readyStateManager.startTask(); - - const abortController = new AbortController(); - let response: Response; - let responseText: string; - + const abortController = new frame.window.AbortController(); const timeout = frame.window.setTimeout( () => abortController.abort(new Error('Request timed out.')), goToOptions?.timeout ?? 30000 @@ -177,6 +189,10 @@ export default class BrowserFrameNavigator { readyStateManager.endTask(); resolveNavigationListeners(); }; + let response: Response; + let responseText: string; + + readyStateManager.startTask(); try { response = await frame.window.fetch(targetURL.href, { @@ -211,13 +227,19 @@ export default class BrowserFrameNavigator { frame.page.console.error(`GET ${targetURL.href} ${response.status} (${response.statusText})`); } + // The frame may be destroyed during teardown. + if (!frame.window) { + return; + } + // Fixes issue where evaluating the response can throw an error. // By using requestAnimationFrame() the error will not reject the promise. // The error will be caught by process error level listener or a try and catch in the requestAnimationFrame(). frame.window.requestAnimationFrame(() => (frame.content = responseText)); + // Finalize the navigation await new Promise((resolve) => - frame.window.requestAnimationFrame(() => { + frame.window.setTimeout(() => { finalize(); resolve(null); }) diff --git a/packages/happy-dom/src/clipboard/Clipboard.ts b/packages/happy-dom/src/clipboard/Clipboard.ts index c69455372..da30cd8fc 100644 --- a/packages/happy-dom/src/clipboard/Clipboard.ts +++ b/packages/happy-dom/src/clipboard/Clipboard.ts @@ -1,4 +1,3 @@ -import DOMException from '../exception/DOMException.js'; import BrowserWindow from '../window/BrowserWindow.js'; import ClipboardItem from './ClipboardItem.js'; import Blob from '../file/Blob.js'; @@ -10,16 +9,19 @@ import Blob from '../file/Blob.js'; * https://developer.mozilla.org/en-US/docs/Web/API/Clipboard. */ export default class Clipboard { - #ownerWindow: BrowserWindow; + #window: BrowserWindow; #data: ClipboardItem[] = []; /** * Constructor. * - * @param ownerWindow Owner window. + * @param window Owner window. */ - constructor(ownerWindow: BrowserWindow) { - this.#ownerWindow = ownerWindow; + constructor(window: BrowserWindow) { + if (!window) { + throw new TypeError('Illegal constructor'); + } + this.#window = window; } /** @@ -28,11 +30,13 @@ export default class Clipboard { * @returns Data. */ public async read(): Promise { - const permissionStatus = await this.#ownerWindow.navigator.permissions.query({ + const permissionStatus = await this.#window.navigator.permissions.query({ name: 'clipboard-read' }); if (permissionStatus.state === 'denied') { - throw new DOMException(`Failed to execute 'read' on 'Clipboard': The request is not allowed`); + throw new this.#window.DOMException( + `Failed to execute 'read' on 'Clipboard': The request is not allowed` + ); } return this.#data; } @@ -43,11 +47,11 @@ export default class Clipboard { * @returns Text. */ public async readText(): Promise { - const permissionStatus = await this.#ownerWindow.navigator.permissions.query({ + const permissionStatus = await this.#window.navigator.permissions.query({ name: 'clipboard-read' }); if (permissionStatus.state === 'denied') { - throw new DOMException( + throw new this.#window.DOMException( `Failed to execute 'readText' on 'Clipboard': The request is not allowed` ); } @@ -72,11 +76,11 @@ export default class Clipboard { * @param data Data. */ public async write(data: ClipboardItem[]): Promise { - const permissionStatus = await this.#ownerWindow.navigator.permissions.query({ + const permissionStatus = await this.#window.navigator.permissions.query({ name: 'clipboard-write' }); if (permissionStatus.state === 'denied') { - throw new DOMException( + throw new this.#window.DOMException( `Failed to execute 'write' on 'Clipboard': The request is not allowed` ); } @@ -89,14 +93,16 @@ export default class Clipboard { * @param text Text. */ public async writeText(text: string): Promise { - const permissionStatus = await this.#ownerWindow.navigator.permissions.query({ + const permissionStatus = await this.#window.navigator.permissions.query({ name: 'clipboard-write' }); if (permissionStatus.state === 'denied') { - throw new DOMException( + throw new this.#window.DOMException( `Failed to execute 'writeText' on 'Clipboard': The request is not allowed` ); } - this.#data = [new ClipboardItem({ 'text/plain': new Blob([text], { type: 'text/plain' }) })]; + this.#data = [ + new this.#window.ClipboardItem({ 'text/plain': new Blob([text], { type: 'text/plain' }) }) + ]; } } diff --git a/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts b/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts index e5e2cf105..4d4a45c14 100644 --- a/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts +++ b/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts @@ -1,9 +1,9 @@ import Element from '../../nodes/element/Element.js'; import CSSRule from '../CSSRule.js'; import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; -import DOMException from '../../exception/DOMException.js'; import CSSStyleDeclarationElementStyle from './element-style/CSSStyleDeclarationElementStyle.js'; import CSSStyleDeclarationPropertyManager from './property-manager/CSSStyleDeclarationPropertyManager.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; /** * CSS Style Declaration. @@ -68,7 +68,7 @@ export default abstract class AbstractCSSStyleDeclaration { */ public set cssText(cssText: string) { if (this.#computed) { - throw new DOMException( + throw new this[PropertySymbol.window].DOMException( `Failed to execute 'cssText' on 'CSSStyleDeclaration': These styles are computed, and the properties are therefore read-only.`, DOMExceptionNameEnum.domException ); @@ -106,7 +106,7 @@ export default abstract class AbstractCSSStyleDeclaration { */ public setProperty(name: string, value: string, priority?: 'important' | '' | undefined): void { if (this.#computed) { - throw new DOMException( + throw new this[PropertySymbol.window].DOMException( `Failed to execute 'setProperty' on 'CSSStyleDeclaration': These styles are computed, and therefore the '${name}' property is read-only.`, DOMExceptionNameEnum.domException ); @@ -139,7 +139,7 @@ export default abstract class AbstractCSSStyleDeclaration { */ public removeProperty(name: string): void { if (this.#computed) { - throw new DOMException( + throw new this[PropertySymbol.window].DOMException( `Failed to execute 'removeProperty' on 'CSSStyleDeclaration': These styles are computed, and therefore the '${name}' property is read-only.`, DOMExceptionNameEnum.domException ); diff --git a/packages/happy-dom/src/css/declaration/element-style/CSSStyleDeclarationElementStyle.ts b/packages/happy-dom/src/css/declaration/element-style/CSSStyleDeclarationElementStyle.ts index 84c86f555..224d2bc7a 100644 --- a/packages/happy-dom/src/css/declaration/element-style/CSSStyleDeclarationElementStyle.ts +++ b/packages/happy-dom/src/css/declaration/element-style/CSSStyleDeclarationElementStyle.ts @@ -17,7 +17,7 @@ import CSSStyleDeclarationCSSParser from '../css-parser/CSSStyleDeclarationCSSPa import QuerySelector from '../../../query-selector/QuerySelector.js'; import CSSMeasurementConverter from '../measurement-converter/CSSMeasurementConverter.js'; import MediaQueryList from '../../../match-media/MediaQueryList.js'; -import WindowBrowserSettingsReader from '../../../window/WindowBrowserSettingsReader.js'; +import WindowBrowserContext from '../../../window/WindowBrowserContext.js'; const CSS_MEASUREMENT_REGEXP = /[0-9.]+(px|rem|em|vw|vh|%|vmin|vmax|cm|mm|in|pt|pc|Q)/g; const CSS_VARIABLE_REGEXP = /var\( *(--[^), ]+)\)|var\( *(--[^), ]+), *(.+)\)/; @@ -333,7 +333,7 @@ export default class CSSStyleDeclarationElementStyle { return; } - const ownerWindow = this.element[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow]; + const window = this.element[PropertySymbol.window]; for (const rule of options.cssRules) { if (rule.type === CSSRuleTypeEnum.styleRule) { @@ -371,7 +371,7 @@ export default class CSSStyleDeclarationElementStyle { rule.type === CSSRuleTypeEnum.mediaRule && // TODO: We need to send in a predfined root font size as it will otherwise be calculated using Window.getComputedStyle(), which will cause a never ending loop. Is there another solution? new MediaQueryList({ - ownerWindow, + window, media: (rule).conditionText, rootFontSize: this.element[PropertySymbol.tagName] === 'HTML' ? 16 : null }).matches @@ -425,9 +425,8 @@ export default class CSSStyleDeclarationElementStyle { parentSize: string | number | null; }): string { if ( - WindowBrowserSettingsReader.getSettings( - this.element[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow] - ).disableComputedStyleRendering + new WindowBrowserContext(this.element[PropertySymbol.window]).getSettings() + ?.disableComputedStyleRendering ) { return options.value; } @@ -439,7 +438,7 @@ export default class CSSStyleDeclarationElementStyle { while ((match = regexp.exec(options.value)) !== null) { if (match[1] !== 'px') { const valueInPixels = CSSMeasurementConverter.toPixels({ - ownerWindow: this.element[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow], + window: this.element[PropertySymbol.window], value: match[0], rootFontSize: options.rootFontSize, parentFontSize: options.parentFontSize, diff --git a/packages/happy-dom/src/css/declaration/measurement-converter/CSSMeasurementConverter.ts b/packages/happy-dom/src/css/declaration/measurement-converter/CSSMeasurementConverter.ts index d3fd2b28e..da0061e56 100644 --- a/packages/happy-dom/src/css/declaration/measurement-converter/CSSMeasurementConverter.ts +++ b/packages/happy-dom/src/css/declaration/measurement-converter/CSSMeasurementConverter.ts @@ -8,7 +8,7 @@ export default class CSSMeasurementConverter { * Returns measurement in pixels. * * @param options Options. - * @param options.ownerWindow Owner window. + * @param options.window Owner window. * @param options.value Measurement (e.g. "10px", "10rem" or "10em"). * @param options.rootFontSize Root font size in pixels. * @param options.parentFontSize Parent font size in pixels. @@ -16,7 +16,7 @@ export default class CSSMeasurementConverter { * @returns Measurement in pixels. */ public static toPixels(options: { - ownerWindow: BrowserWindow; + window: BrowserWindow; value: string; rootFontSize: string | number; parentFontSize: string | number; @@ -37,21 +37,19 @@ export default class CSSMeasurementConverter { case 'em': return this.round(value * parseFloat(options.parentFontSize)); case 'vw': - return this.round((value * options.ownerWindow.innerWidth) / 100); + return this.round((value * options.window.innerWidth) / 100); case 'vh': - return this.round((value * options.ownerWindow.innerHeight) / 100); + return this.round((value * options.window.innerHeight) / 100); case '%': return options.parentSize !== undefined && options.parentSize !== null ? this.round((value * parseFloat(options.parentSize)) / 100) : null; case 'vmin': return this.round( - (value * Math.min(options.ownerWindow.innerWidth, options.ownerWindow.innerHeight)) / 100 + (value * Math.min(options.window.innerWidth, options.window.innerHeight)) / 100 ); case 'vmax': - return ( - (value * Math.max(options.ownerWindow.innerWidth, options.ownerWindow.innerHeight)) / 100 - ); + return (value * Math.max(options.window.innerWidth, options.window.innerHeight)) / 100; case 'cm': return this.round(value * 37.7812); case 'mm': diff --git a/packages/happy-dom/src/custom-element/CustomElementRegistry.ts b/packages/happy-dom/src/custom-element/CustomElementRegistry.ts index bb07a6bc8..8aa4a2698 100644 --- a/packages/happy-dom/src/custom-element/CustomElementRegistry.ts +++ b/packages/happy-dom/src/custom-element/CustomElementRegistry.ts @@ -1,4 +1,3 @@ -import DOMException from '../exception/DOMException.js'; import * as PropertySymbol from '../PropertySymbol.js'; import HTMLElement from '../nodes/html-element/HTMLElement.js'; import Node from '../nodes/node/Node.js'; @@ -13,7 +12,8 @@ export default class CustomElementRegistry { [k: string]: { elementClass: typeof HTMLElement; extends: string }; } = {}; public [PropertySymbol.registedClass]: Map = new Map(); - public [PropertySymbol.callbacks]: { [k: string]: (() => void)[] } = {}; + public [PropertySymbol.callbacks]: Map void>> = new Map(); + public [PropertySymbol.destroyed]: boolean = false; #window: BrowserWindow; /** @@ -22,6 +22,9 @@ export default class CustomElementRegistry { * @param window Window. */ constructor(window: BrowserWindow) { + if (!window) { + throw new TypeError('Illegal constructor'); + } this.#window = window; } @@ -38,30 +41,35 @@ export default class CustomElementRegistry { elementClass: typeof HTMLElement, options?: { extends?: string } ): void { + if (this[PropertySymbol.destroyed]) { + return; + } + if (!this.#isValidCustomElementName(name)) { - throw new DOMException( + throw new this.#window.DOMException( `Failed to execute 'define' on 'CustomElementRegistry': "${name}" is not a valid custom element name` ); } if (this[PropertySymbol.registry][name]) { - throw new DOMException( + throw new this.#window.DOMException( `Failed to execute 'define' on 'CustomElementRegistry': the name "${name}" has already been used with this registry` ); } if (this[PropertySymbol.registedClass].has(elementClass)) { - throw new DOMException( + throw new this.#window.DOMException( "Failed to execute 'define' on 'CustomElementRegistry': this constructor has already been used with this registry" ); } const tagName = name.toUpperCase(); - elementClass[PropertySymbol.ownerDocument] = this.#window.document; - elementClass[PropertySymbol.tagName] = tagName; - elementClass[PropertySymbol.localName] = name; - elementClass[PropertySymbol.namespaceURI] = NamespaceURI.html; + elementClass.prototype[PropertySymbol.window] = this.#window; + elementClass.prototype[PropertySymbol.ownerDocument] = this.#window.document; + elementClass.prototype[PropertySymbol.tagName] = tagName; + elementClass.prototype[PropertySymbol.localName] = name; + elementClass.prototype[PropertySymbol.namespaceURI] = NamespaceURI.html; this[PropertySymbol.registry][name] = { elementClass, @@ -74,9 +82,9 @@ export default class CustomElementRegistry { (name) => String(name).toLowerCase() ); - if (this[PropertySymbol.callbacks][name]) { - const callbacks = this[PropertySymbol.callbacks][name]; - delete this[PropertySymbol.callbacks][name]; + const callbacks = this[PropertySymbol.callbacks].get(name); + if (callbacks) { + this[PropertySymbol.callbacks].delete(name); for (const callback of callbacks) { callback(); } @@ -110,15 +118,30 @@ export default class CustomElementRegistry { * @param name Tag name of element. */ public whenDefined(name: string): Promise { + if (this[PropertySymbol.destroyed]) { + return Promise.reject( + new this.#window.DOMException( + `Failed to execute 'whenDefined' on 'CustomElementRegistry': The custom element registry has been destroyed.` + ) + ); + } if (!this.#isValidCustomElementName(name)) { - return Promise.reject(new DOMException(`Invalid custom element name: "${name}"`)); + return Promise.reject( + new this.#window.DOMException( + `Failed to execute 'whenDefined' on 'CustomElementRegistry': Invalid custom element name: "${name}"` + ) + ); } if (this.get(name)) { return Promise.resolve(); } return new Promise((resolve) => { - this[PropertySymbol.callbacks][name] = this[PropertySymbol.callbacks][name] || []; - this[PropertySymbol.callbacks][name].push(resolve); + const callbacks: Array<() => void> = this[PropertySymbol.callbacks].get(name); + if (callbacks) { + callbacks.push(resolve); + } else { + this[PropertySymbol.callbacks].set(name, [resolve]); + } }); } @@ -136,12 +159,17 @@ export default class CustomElementRegistry { * Destroys the registry. */ public [PropertySymbol.destroy](): void { + this[PropertySymbol.destroyed] = true; for (const entity of Object.values(this[PropertySymbol.registry])) { - entity.elementClass[PropertySymbol.ownerDocument] = null; + entity.elementClass.prototype[PropertySymbol.window] = null; + entity.elementClass.prototype[PropertySymbol.ownerDocument] = null; + entity.elementClass.prototype[PropertySymbol.tagName] = null; + entity.elementClass.prototype[PropertySymbol.localName] = null; + entity.elementClass.prototype[PropertySymbol.namespaceURI] = null; } this[PropertySymbol.registry] = {}; this[PropertySymbol.registedClass] = new Map(); - this[PropertySymbol.callbacks] = {}; + this[PropertySymbol.callbacks] = new Map(); } /** diff --git a/packages/happy-dom/src/dom-implementation/DOMImplementation.ts b/packages/happy-dom/src/dom-implementation/DOMImplementation.ts index dbf9870b9..32c70d64d 100644 --- a/packages/happy-dom/src/dom-implementation/DOMImplementation.ts +++ b/packages/happy-dom/src/dom-implementation/DOMImplementation.ts @@ -12,26 +12,34 @@ export default class DOMImplementation { /** * Constructor. * - * @param window Window. + * @param document Document. */ - constructor(window: Document) { - this.#document = window; + constructor(document: Document) { + this.#document = document; } /** * Creates and returns an XML Document. * * TODO: Not fully implemented. + * + * @param _namespaceURI Namespace URI. + * @param _qualifiedName Qualified name. + * @param [_docType] Document type. */ - public createDocument(): Document { - return new this.#document[PropertySymbol.ownerWindow].HTMLDocument(); + public createDocument( + _namespaceURI: string, + _qualifiedName: string, + _docType?: string + ): Document { + return new this.#document[PropertySymbol.window].HTMLDocument(); } /** * Creates and returns an HTML Document. */ public createHTMLDocument(): Document { - return new this.#document[PropertySymbol.ownerWindow].HTMLDocument(); + return new this.#document[PropertySymbol.window].HTMLDocument(); } /** @@ -46,10 +54,7 @@ export default class DOMImplementation { publicId: string, systemId: string ): DocumentType { - const documentType = NodeFactory.createNode( - this.#document, - this.#document[PropertySymbol.ownerWindow].DocumentType - ); + const documentType = NodeFactory.createNode(this.#document, DocumentType); documentType[PropertySymbol.name] = qualifiedName; documentType[PropertySymbol.publicId] = publicId; diff --git a/packages/happy-dom/src/dom-parser/DOMParser.ts b/packages/happy-dom/src/dom-parser/DOMParser.ts index 871da462d..a176d0c6d 100644 --- a/packages/happy-dom/src/dom-parser/DOMParser.ts +++ b/packages/happy-dom/src/dom-parser/DOMParser.ts @@ -1,7 +1,6 @@ import Document from '../nodes/document/Document.js'; import * as PropertySymbol from '../PropertySymbol.js'; import XMLParser from '../xml-parser/XMLParser.js'; -import DOMException from '../exception/DOMException.js'; import DocumentFragment from '../nodes/document-fragment/DocumentFragment.js'; import BrowserWindow from '../window/BrowserWindow.js'; import NodeTypeEnum from '../nodes/node/NodeTypeEnum.js'; @@ -13,16 +12,8 @@ import NodeTypeEnum from '../nodes/node/NodeTypeEnum.js'; * https://developer.mozilla.org/en-US/docs/Web/API/DOMParser. */ export default class DOMParser { - readonly #window: BrowserWindow; - - /** - * Constructor. - * - * @param window Window. - */ - constructor(window: BrowserWindow) { - this.#window = window; - } + // Injected by WindowClassExtender + protected declare [PropertySymbol.window]: BrowserWindow; /** * Parses HTML and returns a root element. @@ -33,7 +24,9 @@ export default class DOMParser { */ public parseFromString(string: string, mimeType: string): Document { if (!mimeType) { - throw new DOMException('Second parameter "mimeType" is mandatory.'); + throw new this[PropertySymbol.window].DOMException( + 'Second parameter "mimeType" is mandatory.' + ); } const newDocument = this.#createDocument(mimeType); @@ -107,17 +100,19 @@ export default class DOMParser { * @returns Document. */ #createDocument(mimeType: string): Document { + const window = this[PropertySymbol.window]; + switch (mimeType) { case 'text/html': - return new this.#window.HTMLDocument(); + return new window.HTMLDocument(); case 'image/svg+xml': - return new this.#window.SVGDocument(); + return new window.SVGDocument(); case 'text/xml': case 'application/xml': case 'application/xhtml+xml': - return new this.#window.XMLDocument(); + return new window.XMLDocument(); default: - throw new DOMException(`Unknown mime type "${mimeType}".`); + throw new window.DOMException(`Unknown mime type "${mimeType}".`); } } } diff --git a/packages/happy-dom/src/event/Event.ts b/packages/happy-dom/src/event/Event.ts index d62a84331..ba18a159c 100644 --- a/packages/happy-dom/src/event/Event.ts +++ b/packages/happy-dom/src/event/Event.ts @@ -12,18 +12,19 @@ import Document from '../nodes/document/Document.js'; * Event. */ export default class Event { - public composed: boolean; - public bubbles: boolean; - public cancelable: boolean; - public defaultPrevented = false; - public eventPhase: EventPhaseEnum = EventPhaseEnum.none; - public timeStamp: number = performance.now(); - public type: string; public NONE = EventPhaseEnum.none; public CAPTURING_PHASE = EventPhaseEnum.capturing; public AT_TARGET = EventPhaseEnum.atTarget; public BUBBLING_PHASE = EventPhaseEnum.bubbling; + public [PropertySymbol.composed] = false; + public [PropertySymbol.bubbles] = false; + public [PropertySymbol.cancelable] = false; + public [PropertySymbol.defaultPrevented] = false; + public [PropertySymbol.eventPhase] = EventPhaseEnum.none; + public [PropertySymbol.timeStamp] = performance.now(); + public [PropertySymbol.type]: string; + public [PropertySymbol.immediatePropagationStopped] = false; public [PropertySymbol.propagationStopped] = false; public [PropertySymbol.target]: EventTarget = null; @@ -37,11 +38,73 @@ export default class Event { * @param [eventInit] Event init. */ constructor(type: string, eventInit: IEventInit | null = null) { - this.type = type; + this[PropertySymbol.type] = type; + this[PropertySymbol.bubbles] = eventInit?.bubbles ?? false; + this[PropertySymbol.cancelable] = eventInit?.cancelable ?? false; + this[PropertySymbol.composed] = eventInit?.composed ?? false; + } + + /** + * Returns composed. + * + * @returns Composed. + */ + public get composed(): boolean { + return this[PropertySymbol.composed]; + } - this.bubbles = eventInit?.bubbles ?? false; - this.cancelable = eventInit?.cancelable ?? false; - this.composed = eventInit?.composed ?? false; + /** + * Returns bubbles. + * + * @returns Bubbles. + */ + public get bubbles(): boolean { + return this[PropertySymbol.bubbles]; + } + + /** + * Returns cancelable. + * + * @returns Cancelable. + */ + public get cancelable(): boolean { + return this[PropertySymbol.cancelable]; + } + + /** + * Returns defaultPrevented. + * + * @returns Default prevented. + */ + public get defaultPrevented(): boolean { + return this[PropertySymbol.defaultPrevented]; + } + + /** + * Returns eventPhase. + * + * @returns Event phase. + */ + public get eventPhase(): EventPhaseEnum { + return this[PropertySymbol.eventPhase]; + } + + /** + * Returns timeStamp. + * + * @returns Time stamp. + */ + public get timeStamp(): number { + return this[PropertySymbol.timeStamp]; + } + + /** + * Returns type. + * + * @returns Type. + */ + public get type(): string { + return this[PropertySymbol.type]; } /** @@ -92,13 +155,17 @@ export default class Event { if (((eventTarget)).parentNode) { eventTarget = ((eventTarget)).parentNode; } else if ( - this.composed && + this[PropertySymbol.composed] && (eventTarget)[PropertySymbol.nodeType] === NodeTypeEnum.documentFragmentNode && (eventTarget).host ) { eventTarget = (eventTarget).host; - } else if ((eventTarget)[PropertySymbol.nodeType] === NodeTypeEnum.documentNode) { - eventTarget = ((eventTarget))[PropertySymbol.ownerWindow]; + } else if ( + (eventTarget)[PropertySymbol.nodeType] === NodeTypeEnum.documentNode && + // The "load" event is a special case. It should not bubble up to the window. + this[PropertySymbol.type] !== 'load' + ) { + eventTarget = ((eventTarget))[PropertySymbol.window]; } else { break; } @@ -116,9 +183,9 @@ export default class Event { * @param [cancelable=false] "true" if it cancelable. */ public initEvent(type: string, bubbles = false, cancelable = false): void { - this.type = type; - this.bubbles = bubbles; - this.cancelable = cancelable; + this[PropertySymbol.type] = type; + this[PropertySymbol.bubbles] = bubbles; + this[PropertySymbol.cancelable] = cancelable; } /** @@ -126,7 +193,7 @@ export default class Event { */ public preventDefault(): void { if (!this[PropertySymbol.isInPassiveEventListener] && this.cancelable) { - this.defaultPrevented = true; + this[PropertySymbol.defaultPrevented] = true; } } diff --git a/packages/happy-dom/src/event/EventTarget.ts b/packages/happy-dom/src/event/EventTarget.ts index ea9a43a6d..c2cf28a43 100644 --- a/packages/happy-dom/src/event/EventTarget.ts +++ b/packages/happy-dom/src/event/EventTarget.ts @@ -1,25 +1,36 @@ -import IEventListener from './IEventListener.js'; import * as PropertySymbol from '../PropertySymbol.js'; import Event from './Event.js'; import IEventListenerOptions from './IEventListenerOptions.js'; import EventPhaseEnum from './EventPhaseEnum.js'; -import Node from '../nodes/node/Node.js'; -import Document from '../nodes/document/Document.js'; -import BrowserWindow from '../window/BrowserWindow.js'; import WindowErrorUtility from '../window/WindowErrorUtility.js'; -import WindowBrowserSettingsReader from '../window/WindowBrowserSettingsReader.js'; +import WindowBrowserContext from '../window/WindowBrowserContext.js'; import BrowserErrorCaptureEnum from '../browser/enums/BrowserErrorCaptureEnum.js'; +import TEventListener from './TEventListener.js'; +import TEventListenerObject from './TEventListenerObject.js'; +import TEventListenerFunction from './TEventListenerFunction.js'; +import BrowserWindow from '../window/BrowserWindow.js'; /** * Handles events. */ -export default abstract class EventTarget { +export default class EventTarget { + // Injected by WindowClassExtender + protected declare [PropertySymbol.window]: BrowserWindow; + public readonly [PropertySymbol.listeners]: { - [k: string]: (((event: Event) => void) | IEventListener)[]; - } = {}; + capturing: Map; + bubbling: Map; + } = { + capturing: new Map(), + bubbling: new Map() + }; public readonly [PropertySymbol.listenerOptions]: { - [k: string]: (IEventListenerOptions | null)[]; - } = {}; + capturing: Map; + bubbling: Map; + } = { + capturing: new Map(), + bubbling: new Map() + }; /** * Return a default description for the EventTarget class. @@ -38,28 +49,29 @@ export default abstract class EventTarget { */ public addEventListener( type: string, - listener: ((event: Event) => void) | IEventListener, + listener: TEventListener, options?: boolean | IEventListenerOptions ): void { - const listenerOptions = typeof options === 'boolean' ? { capture: options } : options || null; + options = typeof options === 'boolean' ? { capture: options } : options || {}; + const eventPhase = options.capture ? 'capturing' : 'bubbling'; + + let listeners: TEventListener[] = this[PropertySymbol.listeners][eventPhase].get(type); + let listenerOptions: IEventListenerOptions[] = + this[PropertySymbol.listenerOptions][eventPhase].get(type); + + if (!listeners) { + listeners = []; + listenerOptions = []; + this[PropertySymbol.listeners][eventPhase].set(type, listeners); + this[PropertySymbol.listenerOptions][eventPhase].set(type, listenerOptions); + } - this[PropertySymbol.listeners][type] = this[PropertySymbol.listeners][type] || []; - this[PropertySymbol.listenerOptions][type] = this[PropertySymbol.listenerOptions][type] || []; - if (this[PropertySymbol.listeners][type].includes(listener)) { + if (listeners.includes(listener)) { return; } - this[PropertySymbol.listeners][type].push(listener); - this[PropertySymbol.listenerOptions][type].push(listenerOptions); - - // Tracks the amount of capture event listeners to improve performance when they are not used. - if (listenerOptions && listenerOptions.capture) { - const window = this.#getWindow(); - if (window) { - window[PropertySymbol.captureEventListenerCount][type] = - window[PropertySymbol.captureEventListenerCount][type] ?? 0; - window[PropertySymbol.captureEventListenerCount][type]++; - } - } + + listeners.push(listener); + listenerOptions.push(options); } /** @@ -68,26 +80,23 @@ export default abstract class EventTarget { * @param type Event type. * @param listener Listener. */ - public removeEventListener( - type: string, - listener: ((event: Event) => void) | IEventListener - ): void { - if (this[PropertySymbol.listeners][type]) { - const index = this[PropertySymbol.listeners][type].indexOf(listener); + public removeEventListener(type: string, listener: TEventListener): void { + const bubblingListeners = this[PropertySymbol.listeners].bubbling.get(type); + if (bubblingListeners) { + const index = bubblingListeners.indexOf(listener); if (index !== -1) { - // Tracks the amount of capture event listeners to improve performance when they are not used. - if ( - this[PropertySymbol.listenerOptions][type][index] && - this[PropertySymbol.listenerOptions][type][index].capture - ) { - const window = this.#getWindow(); - if (window && window[PropertySymbol.captureEventListenerCount][type]) { - window[PropertySymbol.captureEventListenerCount][type]--; - } - } + bubblingListeners.splice(index, 1); + this[PropertySymbol.listenerOptions].bubbling.get(type).splice(index, 1); + return; + } + } - this[PropertySymbol.listeners][type].splice(index, 1); - this[PropertySymbol.listenerOptions][type].splice(index, 1); + const capturingListeners = this[PropertySymbol.listeners].capturing.get(type); + if (capturingListeners) { + const index = capturingListeners.indexOf(listener); + if (index !== -1) { + capturingListeners.splice(index, 1); + this[PropertySymbol.listenerOptions].capturing.get(type).splice(index, 1); } } } @@ -101,63 +110,119 @@ export default abstract class EventTarget { * @returns The return value is false if event is cancelable and at least one of the event handlers which handled this event called Event.preventDefault(). */ public dispatchEvent(event: Event): boolean { - const window = this.#getWindow(); - - if (event.eventPhase === EventPhaseEnum.none) { + if (!event[PropertySymbol.target]) { event[PropertySymbol.target] = this; - const composedPath = event.composedPath(); + this.#goThroughDispatchEventPhases(event); - // Capturing phase + return !(event[PropertySymbol.cancelable] && event[PropertySymbol.defaultPrevented]); + } - // We only need to iterate over the composed path if there are capture event listeners. - if (window && window[PropertySymbol.captureEventListenerCount][event.type]) { - event.eventPhase = EventPhaseEnum.capturing; + this.#callDispatchEventListeners(event); - for (let i = composedPath.length - 1; i >= 0; i--) { - composedPath[i].dispatchEvent(event); - if ( - event[PropertySymbol.propagationStopped] || - event[PropertySymbol.immediatePropagationStopped] - ) { - break; - } - } - } + return !(event[PropertySymbol.cancelable] && event[PropertySymbol.defaultPrevented]); + } + + /** + * Adds an event listener. + * + * TODO: + * Was used by with IE8- and Opera. React believed Happy DOM was a legacy browser and used them, but that is no longer the case, so we should remove this method after that this is verified. + * + * @deprecated + * @param type Event type. + * @param listener Listener. + */ + public attachEvent(type: string, listener: TEventListener): void { + this.addEventListener(type.replace('on', ''), listener); + } - // At target phase - event.eventPhase = EventPhaseEnum.atTarget; + /** + * Removes an event listener. + * + * TODO: + * Was used by IE8- and Opera. React believed Happy DOM was a legacy browser and used them, but that is no longer the case, so we should remove this method after that this is verified. + * + * @deprecated + * @param type Event type. + * @param listener Listener. + */ + public detachEvent(type: string, listener: TEventListener): void { + this.removeEventListener(type.replace('on', ''), listener); + } - this.dispatchEvent(event); + /** + * Goes through dispatch event phases. + * + * @param event Event. + */ + #goThroughDispatchEventPhases(event: Event): void { + const composedPath = event.composedPath(); + + // Capturing phase + event[PropertySymbol.eventPhase] = EventPhaseEnum.capturing; + + for (let i = composedPath.length - 1; i >= 0; i--) { + event[PropertySymbol.currentTarget] = composedPath[i]; + + composedPath[i].dispatchEvent(event); - // Bubbling phase if ( - event.bubbles && - !event[PropertySymbol.propagationStopped] && - !event[PropertySymbol.immediatePropagationStopped] + event[PropertySymbol.propagationStopped] || + event[PropertySymbol.immediatePropagationStopped] ) { - event.eventPhase = EventPhaseEnum.bubbling; - - for (let i = 1; i < composedPath.length; i++) { - composedPath[i].dispatchEvent(event); - if ( - event[PropertySymbol.propagationStopped] || - event[PropertySymbol.immediatePropagationStopped] - ) { - break; - } - } + event[PropertySymbol.eventPhase] = EventPhaseEnum.none; + event[PropertySymbol.target] = null; + event[PropertySymbol.currentTarget] = null; + return; } + } - // None phase (completed) - event.eventPhase = EventPhaseEnum.none; + // At target phase + event[PropertySymbol.eventPhase] = EventPhaseEnum.atTarget; + event[PropertySymbol.currentTarget] = this; + event[PropertySymbol.target].dispatchEvent(event); - return !(event.cancelable && event.defaultPrevented); + // Bubbling phase + event[PropertySymbol.eventPhase] = EventPhaseEnum.bubbling; + + if ( + event[PropertySymbol.bubbles] && + !event[PropertySymbol.propagationStopped] && + !event[PropertySymbol.immediatePropagationStopped] + ) { + for (let i = 1, max = composedPath.length; i < max; i++) { + event[PropertySymbol.currentTarget] = composedPath[i]; + + composedPath[i].dispatchEvent(event); + + if ( + event[PropertySymbol.propagationStopped] || + event[PropertySymbol.immediatePropagationStopped] + ) { + event[PropertySymbol.eventPhase] = EventPhaseEnum.none; + event[PropertySymbol.target] = null; + event[PropertySymbol.currentTarget] = null; + return; + } + } } - event[PropertySymbol.currentTarget] = this; + // None phase (done) + event[PropertySymbol.eventPhase] = EventPhaseEnum.none; + event[PropertySymbol.target] = null; + event[PropertySymbol.currentTarget] = null; + } - const browserSettings = window ? WindowBrowserSettingsReader.getSettings(window) : null; + /** + * Handles dispatch event listeners. + * + * @param event Event. + */ + #callDispatchEventListeners(event: Event): void { + const window = this[PropertySymbol.window]; + const browserSettings = window ? new WindowBrowserContext(window).getSettings() : null; + const eventPhase = event.eventPhase === EventPhaseEnum.capturing ? 'capturing' : 'bubbling'; if (event.eventPhase !== EventPhaseEnum.capturing) { const onEventName = 'on' + event.type.toLowerCase(); @@ -177,116 +242,66 @@ export default abstract class EventTarget { } } - if (this[PropertySymbol.listeners][event.type]) { - // We need to clone the arrays because the listeners may remove themselves while we are iterating. - const listeners = this[PropertySymbol.listeners][event.type].slice(); - const listenerOptions = this[PropertySymbol.listenerOptions][event.type].slice(); + // We need to clone the arrays because the listeners may remove themselves while we are iterating. + const listeners = this[PropertySymbol.listeners][eventPhase].get(event.type)?.slice(); - for (let i = 0, max = listeners.length; i < max; i++) { - const listener = listeners[i]; - const options = listenerOptions[i]; + if (!listeners) { + return; + } - if ( - (options?.capture && event.eventPhase !== EventPhaseEnum.capturing) || - (!options?.capture && event.eventPhase === EventPhaseEnum.capturing) - ) { - continue; - } + const listenerOptions = this[PropertySymbol.listenerOptions][eventPhase] + .get(event.type) + ?.slice(); - if (options?.passive) { - event[PropertySymbol.isInPassiveEventListener] = true; - } + for (let i = 0, max = listeners.length; i < max; i++) { + const listener = listeners[i]; + const options = listenerOptions[i]; - // 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') && - !browserSettings?.disableErrorCapturing && - browserSettings?.errorCapture === BrowserErrorCaptureEnum.tryAndCatch - ) { - if ((listener).handleEvent) { - WindowErrorUtility.captureError( - window, - (listener).handleEvent.bind(listener, event) - ); - } else { - WindowErrorUtility.captureError( - window, - (<(event: Event) => void>listener).bind(this, event) - ); - } - } else { - if ((listener).handleEvent) { - (listener).handleEvent(event); - } else { - (<(event: Event) => void>listener).call(this, event); - } - } - - event[PropertySymbol.isInPassiveEventListener] = false; + if (options?.passive) { + event[PropertySymbol.isInPassiveEventListener] = true; + } - if (options?.once) { - // At this time, listeners and listenersOptions are cloned arrays. When the original value is deleted, - // The value corresponding to the cloned array is not deleted. So we need to delete the value in the cloned array. - listeners.splice(i, 1); - listenerOptions.splice(i, 1); - this.removeEventListener(event.type, listener); - i--; - max--; + // 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') && + !browserSettings?.disableErrorCapturing && + browserSettings?.errorCapture === BrowserErrorCaptureEnum.tryAndCatch + ) { + if ((listener).handleEvent) { + WindowErrorUtility.captureError( + window, + (listener).handleEvent.bind(listener, event) + ); + } else { + WindowErrorUtility.captureError( + window, + (listener).bind(this, event) + ); } - - if (event[PropertySymbol.immediatePropagationStopped]) { - return !(event.cancelable && event.defaultPrevented); + } else { + if ((listener).handleEvent) { + (listener).handleEvent(event); + } else { + (listener).call(this, event); } } - } - - return !(event.cancelable && event.defaultPrevented); - } - /** - * Adds an event listener. - * - * TODO: - * Was used by with IE8- and Opera. React believed Happy DOM was a legacy browser and used them, but that is no longer the case, so we should remove this method after that this is verified. - * - * @deprecated - * @param type Event type. - * @param listener Listener. - */ - public attachEvent(type: string, listener: ((event: Event) => void) | IEventListener): void { - this.addEventListener(type.replace('on', ''), listener); - } + event[PropertySymbol.isInPassiveEventListener] = false; - /** - * Removes an event listener. - * - * TODO: - * Was used by IE8- and Opera. React believed Happy DOM was a legacy browser and used them, but that is no longer the case, so we should remove this method after that this is verified. - * - * @deprecated - * @param type Event type. - * @param listener Listener. - */ - public detachEvent(type: string, listener: ((event: Event) => void) | IEventListener): void { - this.removeEventListener(type.replace('on', ''), listener); - } + if (options?.once) { + // At this time, listeners and listenersOptions are cloned arrays. When the original value is deleted, + // The value corresponding to the cloned array is not deleted. So we need to delete the value in the cloned array. + listeners.splice(i, 1); + listenerOptions.splice(i, 1); + this.removeEventListener(event.type, listener); + i--; + max--; + } - /** - * Finds and returns window if possible. - * - * @returns Window. - */ - #getWindow(): BrowserWindow | null { - if (((this))[PropertySymbol.ownerDocument]) { - return ((this))[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow]; - } - if (((this))[PropertySymbol.ownerWindow]) { - return ((this))[PropertySymbol.ownerWindow]; - } - if (((this)).document) { - return (this); + if (event[PropertySymbol.immediatePropagationStopped]) { + return; + } } - return null; } } diff --git a/packages/happy-dom/src/event/IEventListener.ts b/packages/happy-dom/src/event/IEventListener.ts deleted file mode 100644 index 59b151e82..000000000 --- a/packages/happy-dom/src/event/IEventListener.ts +++ /dev/null @@ -1,13 +0,0 @@ -import Event from './Event.js'; - -/** - * Event listener. - */ -export default interface IEventListener { - /** - * Handles event. - * - * @param event Event. - */ - handleEvent(event: Event): void; -} diff --git a/packages/happy-dom/src/event/IMessagePort.ts b/packages/happy-dom/src/event/IMessagePort.ts deleted file mode 100644 index af9f9a85a..000000000 --- a/packages/happy-dom/src/event/IMessagePort.ts +++ /dev/null @@ -1,26 +0,0 @@ -import EventTarget from './EventTarget.js'; - -/** - * Message port. - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/MessagePort - */ -export default interface IMessagePort extends EventTarget { - /** - * Sends a message from the port, and optionally, transfers ownership of objects to other browsing contexts. - * - * @param type Event type. - * @param listener Listener. - */ - postMessage(message: unknown, transerList: unknown[]): void; - - /** - * Starts the sending of messages queued on the port. - */ - start(): void; - - /** - * Disconnects the port, so it is no longer active. This stops the flow of messages to that port. - */ - close(): void; -} diff --git a/packages/happy-dom/src/event/MessagePort.ts b/packages/happy-dom/src/event/MessagePort.ts index 9b4c12c34..35d378159 100644 --- a/packages/happy-dom/src/event/MessagePort.ts +++ b/packages/happy-dom/src/event/MessagePort.ts @@ -1,12 +1,11 @@ import EventTarget from './EventTarget.js'; -import IMessagePort from './IMessagePort.js'; /** * Message port. * * @see https://developer.mozilla.org/en-US/docs/Web/API/MessagePort */ -export default abstract class MessagePort extends EventTarget implements IMessagePort { +export default abstract class MessagePort extends EventTarget { /** * Sends a message from the port, and optionally, transfers ownership of objects to other browsing contexts. * diff --git a/packages/happy-dom/src/event/TEventListener.ts b/packages/happy-dom/src/event/TEventListener.ts new file mode 100644 index 000000000..8bb0a9234 --- /dev/null +++ b/packages/happy-dom/src/event/TEventListener.ts @@ -0,0 +1,6 @@ +import TEventListenerFunction from './TEventListenerFunction.js'; +import TEventListenerObject from './TEventListenerObject.js'; + +type TEventListener = TEventListenerFunction | TEventListenerObject; + +export default TEventListener; diff --git a/packages/happy-dom/src/event/TEventListenerFunction.ts b/packages/happy-dom/src/event/TEventListenerFunction.ts new file mode 100644 index 000000000..900f4ea8e --- /dev/null +++ b/packages/happy-dom/src/event/TEventListenerFunction.ts @@ -0,0 +1,5 @@ +import Event from './Event.js'; + +type TEventListenerFunction = (event: Event) => void; + +export default TEventListenerFunction; diff --git a/packages/happy-dom/src/event/TEventListenerObject.ts b/packages/happy-dom/src/event/TEventListenerObject.ts new file mode 100644 index 000000000..6e5ac8e48 --- /dev/null +++ b/packages/happy-dom/src/event/TEventListenerObject.ts @@ -0,0 +1,7 @@ +import Event from './Event.js'; + +type TEventListenerObject = { + handleEvent(event: Event): void; +}; + +export default TEventListenerObject; diff --git a/packages/happy-dom/src/event/events/CustomEvent.ts b/packages/happy-dom/src/event/events/CustomEvent.ts index 0393d9709..c819a53a9 100644 --- a/packages/happy-dom/src/event/events/CustomEvent.ts +++ b/packages/happy-dom/src/event/events/CustomEvent.ts @@ -1,12 +1,13 @@ import Event from '../Event.js'; import ICustomEventInit from './ICustomEventInit.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; /** * */ export default class CustomEvent extends Event { // eslint-disable-next-line @typescript-eslint/no-explicit-any - public detail: any; + public [PropertySymbol.detail]: any; /** * Constructor. @@ -17,7 +18,16 @@ export default class CustomEvent extends Event { constructor(type: string, eventInit: ICustomEventInit | null = null) { super(type, eventInit); - this.detail = eventInit?.detail ?? null; + this[PropertySymbol.detail] = eventInit?.detail ?? null; + } + + /** + * Returns detail. + * + * @returns Detail. + */ + public get detail(): any { + return this[PropertySymbol.detail]; } /** @@ -35,9 +45,9 @@ export default class CustomEvent extends Event { cancelable = false, detail: object = null ): void { - this.type = type; - this.bubbles = bubbles; - this.cancelable = cancelable; - this.detail = detail; + this[PropertySymbol.type] = type; + this[PropertySymbol.bubbles] = bubbles; + this[PropertySymbol.cancelable] = cancelable; + this[PropertySymbol.detail] = detail; } } diff --git a/packages/happy-dom/src/event/events/IErrorEventInit.ts b/packages/happy-dom/src/event/events/IErrorEventInit.ts index 1fb205736..7b8201792 100644 --- a/packages/happy-dom/src/event/events/IErrorEventInit.ts +++ b/packages/happy-dom/src/event/events/IErrorEventInit.ts @@ -1,5 +1,4 @@ import IEventInit from '../IEventInit.js'; - export default interface IErrorEventInit extends IEventInit { message?: string; filename?: string; diff --git a/packages/happy-dom/src/event/events/IMessageEventInit.ts b/packages/happy-dom/src/event/events/IMessageEventInit.ts index b0028037b..5a2ce3986 100644 --- a/packages/happy-dom/src/event/events/IMessageEventInit.ts +++ b/packages/happy-dom/src/event/events/IMessageEventInit.ts @@ -1,11 +1,11 @@ import IEventInit from '../IEventInit.js'; import BrowserWindow from '../../window/BrowserWindow.js'; -import IMessagePort from '../IMessagePort.js'; +import MessagePort from '../MessagePort.js'; export default interface IMessageEventInit extends IEventInit { data?: unknown | null; origin?: string; lastEventId?: string; source?: BrowserWindow | null; - ports?: IMessagePort[]; + ports?: MessagePort[]; } diff --git a/packages/happy-dom/src/event/events/MessageEvent.ts b/packages/happy-dom/src/event/events/MessageEvent.ts index d23edb0e8..1bda0a137 100644 --- a/packages/happy-dom/src/event/events/MessageEvent.ts +++ b/packages/happy-dom/src/event/events/MessageEvent.ts @@ -1,6 +1,6 @@ import BrowserWindow from '../../window/BrowserWindow.js'; import Event from '../Event.js'; -import IMessagePort from '../IMessagePort.js'; +import MessagePort from '../MessagePort.js'; import IMessageEventInit from './IMessageEventInit.js'; /** @@ -13,7 +13,7 @@ export default class MessageEvent extends Event { public readonly origin: string; public readonly lastEventId: string; public readonly source: BrowserWindow | null; - public readonly ports: IMessagePort[]; + public readonly ports: MessagePort[]; /** * Constructor. diff --git a/packages/happy-dom/src/fetch/AbortController.ts b/packages/happy-dom/src/fetch/AbortController.ts index 2aa64f91a..9db7ab04b 100644 --- a/packages/happy-dom/src/fetch/AbortController.ts +++ b/packages/happy-dom/src/fetch/AbortController.ts @@ -1,5 +1,6 @@ import AbortSignal from './AbortSignal.js'; import * as PropertySymbol from '../PropertySymbol.js'; +import BrowserWindow from '../window/BrowserWindow.js'; /** * AbortController. @@ -7,14 +8,11 @@ import * as PropertySymbol from '../PropertySymbol.js'; * @see https://developer.mozilla.org/en-US/docs/Web/API/AbortController */ export default class AbortController { - public readonly signal: AbortSignal = new AbortSignal(); + // Injected by WindowClassExtender + protected declare [PropertySymbol.window]: BrowserWindow; - /** - * Constructor. - */ - constructor() { - this.signal = new AbortSignal(); - } + // Public properties + public readonly signal: AbortSignal = new this[PropertySymbol.window].AbortSignal(); /** * Aborts the signal. diff --git a/packages/happy-dom/src/fetch/AbortSignal.ts b/packages/happy-dom/src/fetch/AbortSignal.ts index 8d78712da..2d8421894 100644 --- a/packages/happy-dom/src/fetch/AbortSignal.ts +++ b/packages/happy-dom/src/fetch/AbortSignal.ts @@ -2,7 +2,7 @@ import EventTarget from '../event/EventTarget.js'; import * as PropertySymbol from '../PropertySymbol.js'; import Event from '../event/Event.js'; import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum.js'; -import DOMException from '../exception/DOMException.js'; +import BrowserWindow from '../window/BrowserWindow.js'; /** * AbortSignal. @@ -10,10 +10,30 @@ import DOMException from '../exception/DOMException.js'; * @see https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal */ export default class AbortSignal extends EventTarget { + // Injected by WindowClassExtender + protected declare static [PropertySymbol.window]: BrowserWindow; + protected declare [PropertySymbol.window]: BrowserWindow; + + // Public properties public readonly aborted: boolean = false; public readonly reason: Error | null = null; + + // Events public onabort: ((this: AbortSignal, event: Event) => void) | null = null; + /** + * Constructor. + */ + constructor() { + super(); + + if (!this[PropertySymbol.window]) { + throw new TypeError( + `Failed to construct '${this.constructor.name}': '${this.constructor.name}' was constructed outside a Window context.` + ); + } + } + /** * Return a default description for the AbortSignal class. */ @@ -32,7 +52,10 @@ export default class AbortSignal extends EventTarget { } (this.reason) = reason || - new DOMException('signal is aborted without reason', DOMExceptionNameEnum.abortError); + new this[PropertySymbol.window].DOMException( + 'signal is aborted without reason', + DOMExceptionNameEnum.abortError + ); (this.aborted) = true; this.dispatchEvent(new Event('abort')); } @@ -53,10 +76,13 @@ export default class AbortSignal extends EventTarget { * @returns AbortSignal instance. */ public static abort(reason?: Error): AbortSignal { - const signal = new AbortSignal(); + const signal = new this[PropertySymbol.window].AbortSignal(); (signal.reason) = reason || - new DOMException('signal is aborted without reason', DOMExceptionNameEnum.abortError); + new this[PropertySymbol.window].DOMException( + 'signal is aborted without reason', + DOMExceptionNameEnum.abortError + ); (signal.aborted) = true; return signal; } diff --git a/packages/happy-dom/src/fetch/Fetch.ts b/packages/happy-dom/src/fetch/Fetch.ts index 16856d0a2..e74df08c7 100644 --- a/packages/happy-dom/src/fetch/Fetch.ts +++ b/packages/happy-dom/src/fetch/Fetch.ts @@ -3,7 +3,6 @@ import * as PropertySymbol from '../PropertySymbol.js'; import IRequestInfo from './types/IRequestInfo.js'; import Headers from './Headers.js'; import FetchRequestReferrerUtility from './utilities/FetchRequestReferrerUtility.js'; -import DOMException from '../exception/DOMException.js'; import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum.js'; import HTTP, { IncomingMessage } from 'http'; import HTTPS from 'https'; @@ -85,7 +84,7 @@ export default class Fetch { this.#window = options.window; this.request = typeof options.url === 'string' || options.url instanceof URL - ? new options.browserFrame.window.Request(options.url, options.init) + ? new options.window.Request(options.url, options.init) : options.url; if (options.contentType) { (this.request[PropertySymbol.contentType]) = options.contentType; @@ -105,7 +104,10 @@ export default class Fetch { FetchRequestValidationUtility.validateSchema(this.request); if (this.request.signal.aborted) { - throw new DOMException('The operation was aborted.', DOMExceptionNameEnum.abortError); + throw new this.#window.DOMException( + 'The operation was aborted.', + DOMExceptionNameEnum.abortError + ); } if (this.request[PropertySymbol.url].protocol === 'data:') { @@ -121,8 +123,12 @@ export default class Fetch { this.request[PropertySymbol.url].protocol === 'http:' && this.#window.location.protocol === 'https:' ) { - throw new DOMException( - `Mixed Content: The page at '${this.#window.location.href}' was loaded over HTTPS, but requested an insecure XMLHttpRequest endpoint '${this.request.url}'. This request has been blocked; the content must be served over HTTPS.`, + throw new this.#window.DOMException( + `Mixed Content: The page at '${ + this.#window.location.href + }' was loaded over HTTPS, but requested an insecure XMLHttpRequest endpoint '${ + this.request.url + }'. This request has been blocked; the content must be served over HTTPS.`, DOMExceptionNameEnum.securityError ); } @@ -142,7 +148,7 @@ export default class Fetch { this.#window.console.warn( `Cross-Origin Request Blocked: The Same Origin Policy dissallows reading the remote resource at "${this.request.url}".` ); - throw new DOMException( + throw new this.#window.DOMException( `Cross-Origin Request Blocked: The Same Origin Policy dissallows reading the remote resource at "${this.request.url}".`, DOMExceptionNameEnum.networkError ); @@ -342,7 +348,7 @@ export default class Fetch { ); if (this.resolve) { - throw new Error('Fetch already sent.'); + throw new this.#window.Error('Fetch already sent.'); } this.resolve = (response: Response | Promise): void => { @@ -417,7 +423,10 @@ export default class Fetch { private onSocket(socket: Socket): void { const onSocketClose = (): void => { if (this.isChunkedTransfer && !this.isProperLastChunkReceived) { - const error = new DOMException('Premature close.', DOMExceptionNameEnum.networkError); + const error = new this.#window.DOMException( + 'Premature close.', + DOMExceptionNameEnum.networkError + ); if (this.response && this.response.body) { this.response.body[PropertySymbol.error] = error; @@ -469,8 +478,8 @@ export default class Fetch { this.finalizeRequest(); this.#window.console.error(error); this.reject( - new DOMException( - `Fetch to "${this.request.url}" failed. Error: ${error.message}`, + new this.#window.DOMException( + `Failed to execute "fetch()" on "Window" with URL "${this.request.url}": ${error.message}`, DOMExceptionNameEnum.networkError ) ); @@ -480,7 +489,20 @@ export default class Fetch { * Triggered when the async task manager aborts. */ private onAsyncTaskManagerAbort(): void { - const error = new DOMException('The operation was aborted.', DOMExceptionNameEnum.abortError); + const error = new this.#window.DOMException( + 'The operation was aborted.', + DOMExceptionNameEnum.abortError + ); + + this.request[PropertySymbol.aborted] = true; + + if (this.request.body) { + this.request.body[PropertySymbol.error] = error; + } + + if (this.listeners.onSignalAbort) { + this.request.signal.removeEventListener('abort', this.listeners.onSignalAbort); + } if (this.nodeRequest && !this.nodeRequest.destroyed) { this.nodeRequest.destroy(error); @@ -670,7 +692,7 @@ export default class Fetch { case 'error': this.finalizeRequest(); this.reject( - new DOMException( + new this.#window.DOMException( `URI requested responds with a redirect, redirect mode is set to "error": ${this.request.url}`, DOMExceptionNameEnum.abortError ) @@ -693,7 +715,7 @@ export default class Fetch { } catch { this.finalizeRequest(); this.reject( - new DOMException( + new this.#window.DOMException( `URI requested responds with an invalid redirect URL: ${locationHeader}`, DOMExceptionNameEnum.uriMismatchError ) @@ -709,7 +731,7 @@ export default class Fetch { if (FetchResponseRedirectUtility.isMaxRedirectsReached(this.redirectCount)) { this.finalizeRequest(); this.reject( - new DOMException( + new this.#window.DOMException( `Maximum redirects reached at: ${this.request.url}`, DOMExceptionNameEnum.networkError ) @@ -777,7 +799,7 @@ export default class Fetch { default: this.finalizeRequest(); this.reject( - new DOMException( + new this.#window.DOMException( `Redirect option '${this.request.redirect}' is not a valid value of IRequestRedirect` ) ); @@ -799,11 +821,17 @@ export default class Fetch { * @param reason Reason. */ private abort(reason?: Error): void { - const error = new DOMException( + const error = new this.#window.DOMException( 'The operation was aborted.' + (reason ? ' ' + reason.toString() : ''), DOMExceptionNameEnum.abortError ); + this.request[PropertySymbol.aborted] = true; + + if (this.request.body) { + this.request.body[PropertySymbol.error] = error; + } + if (this.nodeRequest && !this.nodeRequest.destroyed) { this.nodeRequest.destroy(error); } diff --git a/packages/happy-dom/src/fetch/Request.ts b/packages/happy-dom/src/fetch/Request.ts index 0852a1d30..e8749930c 100644 --- a/packages/happy-dom/src/fetch/Request.ts +++ b/packages/happy-dom/src/fetch/Request.ts @@ -1,8 +1,6 @@ import * as PropertySymbol from '../PropertySymbol.js'; -import Document from '../nodes/document/Document.js'; import IRequestInit from './types/IRequestInit.js'; import URL from '../url/URL.js'; -import DOMException from '../exception/DOMException.js'; import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum.js'; import IRequestInfo from './types/IRequestInfo.js'; import Headers from './Headers.js'; @@ -18,8 +16,8 @@ import FetchRequestHeaderUtility from './utilities/FetchRequestHeaderUtility.js' import IRequestCredentials from './types/IRequestCredentials.js'; import FormData from '../form-data/FormData.js'; import MultipartFormDataParser from './multipart/MultipartFormDataParser.js'; -import AsyncTaskManager from '../async-task-manager/AsyncTaskManager.js'; import BrowserWindow from '../window/BrowserWindow.js'; +import WindowBrowserContext from '../window/WindowBrowserContext.js'; /** * Fetch request. @@ -30,6 +28,9 @@ import BrowserWindow from '../window/BrowserWindow.js'; * @see https://fetch.spec.whatwg.org/#request-class */ export default class Request implements Request { + // Injected by WindowClassExtender + protected declare [PropertySymbol.window]: BrowserWindow; + // Public properties public readonly method: string; public readonly body: ReadableStream | null; @@ -41,42 +42,39 @@ export default class Request implements Request { public readonly credentials: IRequestCredentials; // Internal properties + public [PropertySymbol.aborted]: boolean = false; public [PropertySymbol.contentLength]: number | null = null; public [PropertySymbol.contentType]: string | null = null; public [PropertySymbol.referrer]: '' | 'no-referrer' | 'client' | URL = 'client'; public [PropertySymbol.url]: URL; public [PropertySymbol.bodyBuffer]: Buffer | null; - // Private properties - readonly #window: BrowserWindow; - readonly #asyncTaskManager: AsyncTaskManager; - /** * Constructor. * - * @param injected Injected properties. - * @param injected.window * @param input Input. - * @param injected.asyncTaskManager * @param [init] Init. */ - constructor( - injected: { window: BrowserWindow; asyncTaskManager: AsyncTaskManager }, - input: IRequestInfo, - init?: IRequestInit - ) { - this.#window = injected.window; - this.#asyncTaskManager = injected.asyncTaskManager; + constructor(input: IRequestInfo, init?: IRequestInit) { + const window = this[PropertySymbol.window]; + + if (!window) { + throw new TypeError( + `Failed to construct 'Request': 'Request' was constructed outside a Window context.` + ); + } if (!input) { - throw new TypeError(`Failed to contruct 'Request': 1 argument required, only 0 present.`); + throw new window.TypeError( + `Failed to contruct 'Request': 1 argument required, only 0 present.` + ); } this.method = (init?.method || (input).method || 'GET').toUpperCase(); const { stream, buffer, contentType, contentLength } = FetchBodyUtility.getBodyStream( input instanceof Request && (input[PropertySymbol.bodyBuffer] || input.body) - ? input[PropertySymbol.bodyBuffer] || FetchBodyUtility.cloneBodyStream(input) + ? input[PropertySymbol.bodyBuffer] || FetchBodyUtility.cloneBodyStream(window, input) : init?.body ); @@ -106,9 +104,9 @@ export default class Request implements Request { this.referrerPolicy = ( (init?.referrerPolicy || (input).referrerPolicy || '').toLowerCase() ); - this.signal = init?.signal || (input).signal || new AbortSignal(); + this.signal = init?.signal || (input).signal || new window.AbortSignal(); this[PropertySymbol.referrer] = FetchRequestReferrerUtility.getInitialReferrer( - injected.window, + window, init?.referrer !== null && init?.referrer !== undefined ? init?.referrer : (input).referrer @@ -119,16 +117,16 @@ export default class Request implements Request { } else { try { if (input instanceof Request && input.url) { - this[PropertySymbol.url] = new URL(input.url, injected.window.location.href); + this[PropertySymbol.url] = new URL(input.url, window.location.href); } else { - this[PropertySymbol.url] = new URL(input, injected.window.location.href); + this[PropertySymbol.url] = new URL(input, window.location.href); } } catch (error) { - throw new DOMException( - `Failed to construct 'Request. Invalid URL "${input}" on document location '${ - injected.window.location + throw new window.DOMException( + `Failed to construct 'Request': Invalid URL "${input}" on document location '${ + window.location }'.${ - injected.window.location.origin === 'null' + window.location.origin === 'null' ? ' Relative URLs are not permitted on current document location.' : '' }`, @@ -144,13 +142,6 @@ export default class Request implements Request { FetchRequestValidationUtility.validateRedirect(this.redirect); } - /** - * Returns owner document. - */ - protected get [PropertySymbol.ownerDocument](): Document { - throw new Error('[PropertySymbol.ownerDocument] getter needs to be implemented by sub-class.'); - } - /** * Returns referrer. * @@ -192,26 +183,35 @@ export default class Request implements Request { * @returns Array buffer. */ public async arrayBuffer(): Promise { + const window = this[PropertySymbol.window]; + if (this.bodyUsed) { - throw new DOMException( + throw new window.DOMException( `Body has already been used for "${this.url}".`, DOMExceptionNameEnum.invalidStateError ); } + const asyncTaskManager = new WindowBrowserContext(window).getAsyncTaskManager(); + (this.bodyUsed) = true; - const taskID = this.#asyncTaskManager.startTask(() => this.signal[PropertySymbol.abort]()); + const taskID = asyncTaskManager.startTask(() => { + if (this.body) { + this.body[PropertySymbol.aborted] = true; + } + this.signal[PropertySymbol.abort](); + }); let buffer: Buffer; try { - buffer = await FetchBodyUtility.consumeBodyStream(this.body); + buffer = await FetchBodyUtility.consumeBodyStream(window, this.body); } catch (error) { - this.#asyncTaskManager.endTask(taskID); + asyncTaskManager.endTask(taskID); throw error; } - this.#asyncTaskManager.endTask(taskID); + asyncTaskManager.endTask(taskID); return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength); } @@ -234,26 +234,35 @@ export default class Request implements Request { * @returns Buffer. */ public async buffer(): Promise { + const window = this[PropertySymbol.window]; + if (this.bodyUsed) { - throw new DOMException( + throw new window.DOMException( `Body has already been used for "${this.url}".`, DOMExceptionNameEnum.invalidStateError ); } + const asyncTaskManager = new WindowBrowserContext(window).getAsyncTaskManager(); + (this.bodyUsed) = true; - const taskID = this.#asyncTaskManager.startTask(() => this.signal[PropertySymbol.abort]()); + const taskID = asyncTaskManager.startTask(() => { + if (this.body) { + this.body[PropertySymbol.aborted] = true; + } + this.signal[PropertySymbol.abort](); + }); let buffer: Buffer; try { - buffer = await FetchBodyUtility.consumeBodyStream(this.body); + buffer = await FetchBodyUtility.consumeBodyStream(window, this.body); } catch (error) { - this.#asyncTaskManager.endTask(taskID); + asyncTaskManager.endTask(taskID); throw error; } - this.#asyncTaskManager.endTask(taskID); + asyncTaskManager.endTask(taskID); return buffer; } @@ -264,26 +273,35 @@ export default class Request implements Request { * @returns Text. */ public async text(): Promise { + const window = this[PropertySymbol.window]; + if (this.bodyUsed) { - throw new DOMException( + throw new window.DOMException( `Body has already been used for "${this.url}".`, DOMExceptionNameEnum.invalidStateError ); } + const asyncTaskManager = new WindowBrowserContext(window).getAsyncTaskManager(); + (this.bodyUsed) = true; - const taskID = this.#asyncTaskManager.startTask(() => this.signal[PropertySymbol.abort]()); + const taskID = asyncTaskManager.startTask(() => { + if (this.body) { + this.body[PropertySymbol.aborted] = true; + } + this.signal[PropertySymbol.abort](); + }); let buffer: Buffer; try { - buffer = await FetchBodyUtility.consumeBodyStream(this.body); + buffer = await FetchBodyUtility.consumeBodyStream(window, this.body); } catch (error) { - this.#asyncTaskManager.endTask(taskID); + asyncTaskManager.endTask(taskID); throw error; } - this.#asyncTaskManager.endTask(taskID); + asyncTaskManager.endTask(taskID); return new TextDecoder().decode(buffer); } @@ -304,12 +322,13 @@ export default class Request implements Request { * @returns FormData. */ public async formData(): Promise { + const window = this[PropertySymbol.window]; + const asyncTaskManager = new WindowBrowserContext(window).getAsyncTaskManager(); const contentType = this[PropertySymbol.contentType]; - const asyncTaskManager = this.#asyncTaskManager; if (/multipart/i.test(contentType)) { if (this.bodyUsed) { - throw new DOMException( + throw new window.DOMException( `Body has already been used for "${this.url}".`, DOMExceptionNameEnum.invalidStateError ); @@ -317,11 +336,20 @@ export default class Request implements Request { (this.bodyUsed) = true; - const taskID = asyncTaskManager.startTask(() => this.signal[PropertySymbol.abort]()); + const taskID = asyncTaskManager.startTask(() => { + if (this.body) { + this.body[PropertySymbol.aborted] = true; + } + this.signal[PropertySymbol.abort](); + }); let formData: FormData; try { - const result = await MultipartFormDataParser.streamToFormData(this.body, contentType); + const result = await MultipartFormDataParser.streamToFormData( + window, + this.body, + contentType + ); formData = result.formData; } catch (error) { asyncTaskManager.endTask(taskID); @@ -344,7 +372,7 @@ export default class Request implements Request { return formData; } - throw new DOMException( + throw new window.DOMException( `Failed to construct FormData object: The "content-type" header is neither "application/x-www-form-urlencoded" nor "multipart/form-data".`, DOMExceptionNameEnum.invalidStateError ); @@ -356,6 +384,6 @@ export default class Request implements Request { * @returns Clone. */ public clone(): Request { - return new this.#window.Request(this); + return new this[PropertySymbol.window].Request(this); } } diff --git a/packages/happy-dom/src/fetch/ResourceFetch.ts b/packages/happy-dom/src/fetch/ResourceFetch.ts index eff703049..70a33fa16 100644 --- a/packages/happy-dom/src/fetch/ResourceFetch.ts +++ b/packages/happy-dom/src/fetch/ResourceFetch.ts @@ -1,4 +1,3 @@ -import DOMException from '../exception/DOMException.js'; import BrowserWindow from '../window/BrowserWindow.js'; import URL from '../url/URL.js'; import IBrowserFrame from '../browser/types/IBrowserFrame.js'; @@ -40,7 +39,7 @@ export default class ResourceFetch { const response = await fetch.send(); if (!response.ok) { - throw new DOMException( + throw new this.window.DOMException( `Failed to perform request to "${new URL(url, this.window.location.href).href}". Status ${ response.status } ${response.statusText}.` @@ -67,7 +66,7 @@ export default class ResourceFetch { const response = fetch.send(); if (!response.ok) { - throw new DOMException( + throw new this.window.DOMException( `Failed to perform request to "${new URL(url, this.window.location.href).href}". Status ${ response.status } ${response.statusText}.` diff --git a/packages/happy-dom/src/fetch/Response.ts b/packages/happy-dom/src/fetch/Response.ts index ee9152080..fa8021008 100644 --- a/packages/happy-dom/src/fetch/Response.ts +++ b/packages/happy-dom/src/fetch/Response.ts @@ -8,13 +8,12 @@ import URL from '../url/URL.js'; import { ReadableStream } from 'stream/web'; import FormData from '../form-data/FormData.js'; import FetchBodyUtility from './utilities/FetchBodyUtility.js'; -import DOMException from '../exception/DOMException.js'; import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum.js'; import MultipartFormDataParser from './multipart/MultipartFormDataParser.js'; import BrowserWindow from '../window/BrowserWindow.js'; -import IBrowserFrame from '../browser/types/IBrowserFrame.js'; import ICachedResponse from './cache/response/ICachedResponse.js'; import { Buffer } from 'buffer'; +import WindowBrowserContext from '../window/WindowBrowserContext.js'; const REDIRECT_STATUS_CODES = [301, 302, 303, 307, 308]; @@ -27,8 +26,9 @@ const REDIRECT_STATUS_CODES = [301, 302, 303, 307, 308]; * @see https://developer.mozilla.org/en-US/docs/Web/API/Response/Response */ export default class Response implements Response { - // Needs to be injected by sub-class. - protected static [PropertySymbol.window]: BrowserWindow; + // Injected by WindowClassExtender + protected declare static [PropertySymbol.window]: BrowserWindow; + protected declare [PropertySymbol.window]: BrowserWindow; // Public properties public readonly body: ReadableStream | null = null; @@ -43,26 +43,20 @@ export default class Response implements Response { public readonly headers: Headers; public [PropertySymbol.cachedResponse]: ICachedResponse | null = null; public [PropertySymbol.buffer]: Buffer | null = null; - readonly #window: BrowserWindow; - readonly #browserFrame: IBrowserFrame; /** * Constructor. * - * @param injected Injected properties. - * @param input Input. - * @param injected.window - * @param body - * @param injected.browserFrame + * @param body Body. * @param [init] Init. */ - constructor( - injected: { window: BrowserWindow; browserFrame: IBrowserFrame }, - body?: IResponseBody, - init?: IResponseInit - ) { - this.#window = injected.window; - this.#browserFrame = injected.browserFrame; + constructor(body?: IResponseBody, init?: IResponseInit) { + if (!this[PropertySymbol.window]) { + throw new TypeError( + `Failed to construct '${this.constructor.name}': '${this.constructor.name}' was constructed outside a Window context.` + ); + } + this.status = init?.status !== undefined ? init.status : 200; this.statusText = init?.statusText || ''; this.ok = this.status >= 200 && this.status < 300; @@ -101,28 +95,37 @@ export default class Response implements Response { * @returns Array buffer. */ public async arrayBuffer(): Promise { + const window = this[PropertySymbol.window]; + if (this.bodyUsed) { - throw new DOMException( + throw new window.DOMException( `Body has already been used for "${this.url}".`, DOMExceptionNameEnum.invalidStateError ); } + const browserFrame = new WindowBrowserContext(window).getBrowserFrame(); + const asyncTaskManager = browserFrame[PropertySymbol.asyncTaskManager]; + (this.bodyUsed) = true; let buffer: Buffer | null = this[PropertySymbol.buffer]; if (!buffer) { - const taskID = this.#browserFrame[PropertySymbol.asyncTaskManager].startTask(); + const taskID = asyncTaskManager.startTask(() => { + if (this.body) { + this.body[PropertySymbol.aborted] = true; + } + }); try { - buffer = await FetchBodyUtility.consumeBodyStream(this.body); + buffer = await FetchBodyUtility.consumeBodyStream(window, this.body); } catch (error) { - this.#browserFrame[PropertySymbol.asyncTaskManager].endTask(taskID); + asyncTaskManager.endTask(taskID); throw error; } - this.#browserFrame[PropertySymbol.asyncTaskManager].endTask(taskID); + asyncTaskManager.endTask(taskID); } this.#storeBodyInCache(buffer); @@ -148,26 +151,35 @@ export default class Response implements Response { * @returns Buffer. */ public async buffer(): Promise { + const window = this[PropertySymbol.window]; + if (this.bodyUsed) { - throw new DOMException( + throw new window.DOMException( `Body has already been used for "${this.url}".`, DOMExceptionNameEnum.invalidStateError ); } + const browserFrame = new WindowBrowserContext(window).getBrowserFrame(); + const asyncTaskManager = browserFrame[PropertySymbol.asyncTaskManager]; + (this.bodyUsed) = true; let buffer: Buffer | null = this[PropertySymbol.buffer]; if (!buffer) { - const taskID = this.#browserFrame[PropertySymbol.asyncTaskManager].startTask(); + const taskID = asyncTaskManager.startTask(() => { + if (this.body) { + this.body[PropertySymbol.aborted] = true; + } + }); try { - buffer = await FetchBodyUtility.consumeBodyStream(this.body); + buffer = await FetchBodyUtility.consumeBodyStream(window, this.body); } catch (error) { - this.#browserFrame[PropertySymbol.asyncTaskManager].endTask(taskID); + asyncTaskManager.endTask(taskID); throw error; } - this.#browserFrame[PropertySymbol.asyncTaskManager].endTask(taskID); + asyncTaskManager.endTask(taskID); } this.#storeBodyInCache(buffer); @@ -181,26 +193,35 @@ export default class Response implements Response { * @returns Text. */ public async text(): Promise { + const window = this[PropertySymbol.window]; + if (this.bodyUsed) { - throw new DOMException( + throw new window.DOMException( `Body has already been used for "${this.url}".`, DOMExceptionNameEnum.invalidStateError ); } + const browserFrame = new WindowBrowserContext(window).getBrowserFrame(); + const asyncTaskManager = browserFrame[PropertySymbol.asyncTaskManager]; + (this.bodyUsed) = true; let buffer: Buffer | null = this[PropertySymbol.buffer]; if (!buffer) { - const taskID = this.#browserFrame[PropertySymbol.asyncTaskManager].startTask(); + const taskID = asyncTaskManager.startTask(() => { + if (this.body) { + this.body[PropertySymbol.aborted] = true; + } + }); try { - buffer = await FetchBodyUtility.consumeBodyStream(this.body); + buffer = await FetchBodyUtility.consumeBodyStream(window, this.body); } catch (error) { - this.#browserFrame[PropertySymbol.asyncTaskManager].endTask(taskID); + asyncTaskManager.endTask(taskID); throw error; } - this.#browserFrame[PropertySymbol.asyncTaskManager].endTask(taskID); + asyncTaskManager.endTask(taskID); } this.#storeBodyInCache(buffer); @@ -224,12 +245,14 @@ export default class Response implements Response { * @returns Form data. */ public async formData(): Promise { + const window = this[PropertySymbol.window]; + const browserFrame = new WindowBrowserContext(window).getBrowserFrame(); + const asyncTaskManager = browserFrame[PropertySymbol.asyncTaskManager]; const contentType = this.headers.get('Content-Type'); - const asyncTaskManager = this.#browserFrame[PropertySymbol.asyncTaskManager]; if (/multipart/i.test(contentType)) { if (this.bodyUsed) { - throw new DOMException( + throw new window.DOMException( `Body has already been used for "${this.url}".`, DOMExceptionNameEnum.invalidStateError ); @@ -237,12 +260,20 @@ export default class Response implements Response { (this.bodyUsed) = true; - const taskID = asyncTaskManager.startTask(); + const taskID = browserFrame[PropertySymbol.asyncTaskManager].startTask(() => { + if (this.body) { + this.body[PropertySymbol.aborted] = true; + } + }); let formData: FormData; let buffer: Buffer; try { - const result = await MultipartFormDataParser.streamToFormData(this.body, contentType); + const result = await MultipartFormDataParser.streamToFormData( + window, + this.body, + contentType + ); formData = result.formData; buffer = result.buffer; } catch (error) { @@ -267,7 +298,7 @@ export default class Response implements Response { return formData; } - throw new DOMException( + throw new window.DOMException( `Failed to build FormData object: The "content-type" header is neither "application/x-www-form-urlencoded" nor "multipart/form-data".`, DOMExceptionNameEnum.invalidStateError ); @@ -279,9 +310,10 @@ export default class Response implements Response { * @returns Clone. */ public clone(): Response { - const body = FetchBodyUtility.cloneBodyStream(this); + const window = this[PropertySymbol.window]; + const body = FetchBodyUtility.cloneBodyStream(window, this); - const response = new this.#window.Response(body, { + const response = new window.Response(body, { status: this.status, statusText: this.statusText, headers: this.headers @@ -317,14 +349,16 @@ export default class Response implements Response { * @returns Response. */ public static redirect(url: string, status = 302): Response { + const window = this[PropertySymbol.window]; + if (!REDIRECT_STATUS_CODES.includes(status)) { - throw new DOMException( + throw new window.DOMException( 'Failed to create redirect response: Invalid redirect status code.', DOMExceptionNameEnum.invalidStateError ); } - return new this[PropertySymbol.window].Response(null, { + return new window.Response(null, { headers: { location: new URL(url).toString() }, @@ -354,19 +388,20 @@ export default class Response implements Response { * @returns Response. */ public static json(data: object, init?: IResponseInit): Response { + const window = this[PropertySymbol.window]; const body = JSON.stringify(data); if (body === undefined) { - throw new TypeError('data is not JSON serializable'); + throw new window.TypeError('data is not JSON serializable'); } - const headers = new this[PropertySymbol.window].Headers(init && init.headers); + const headers = new window.Headers(init && init.headers); if (!headers.has('Content-Type')) { headers.set('Content-Type', 'application/json'); } - return new this[PropertySymbol.window].Response(body, { + return new window.Response(body, { status: 200, ...init, headers diff --git a/packages/happy-dom/src/fetch/SyncFetch.ts b/packages/happy-dom/src/fetch/SyncFetch.ts index 1991d14e8..12d869a12 100644 --- a/packages/happy-dom/src/fetch/SyncFetch.ts +++ b/packages/happy-dom/src/fetch/SyncFetch.ts @@ -1,7 +1,6 @@ import IRequestInit from './types/IRequestInit.js'; import * as PropertySymbol from '../PropertySymbol.js'; import IRequestInfo from './types/IRequestInfo.js'; -import DOMException from '../exception/DOMException.js'; import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum.js'; import URL from '../url/URL.js'; import Request from './Request.js'; @@ -70,7 +69,7 @@ export default class SyncFetch { this.#window = options.window; this.request = typeof options.url === 'string' || options.url instanceof URL - ? new options.browserFrame.window.Request(options.url, options.init) + ? new options.window.Request(options.url, options.init) : options.url; if (options.contentType) { (this.request[PropertySymbol.contentType]) = options.contentType; @@ -90,7 +89,10 @@ export default class SyncFetch { FetchRequestValidationUtility.validateSchema(this.request); if (this.request.signal.aborted) { - throw new DOMException('The operation was aborted.', DOMExceptionNameEnum.abortError); + throw new this.#window.DOMException( + 'The operation was aborted.', + DOMExceptionNameEnum.abortError + ); } if (this.request[PropertySymbol.url].protocol === 'data:') { @@ -111,8 +113,12 @@ export default class SyncFetch { this.request[PropertySymbol.url].protocol === 'http:' && this.#window.location.protocol === 'https:' ) { - throw new DOMException( - `Mixed Content: The page at '${this.#window.location.href}' was loaded over HTTPS, but requested an insecure XMLHttpRequest endpoint '${this.request.url}'. This request has been blocked; the content must be served over HTTPS.`, + throw new this.#window.DOMException( + `Mixed Content: The page at '${ + this.#window.location.href + }' was loaded over HTTPS, but requested an insecure XMLHttpRequest endpoint '${ + this.request.url + }'. This request has been blocked; the content must be served over HTTPS.`, DOMExceptionNameEnum.securityError ); } @@ -127,7 +133,7 @@ export default class SyncFetch { this.#window.console.warn( `Cross-Origin Request Blocked: The Same Origin Policy dissallows reading the remote resource at "${this.request.url}".` ); - throw new DOMException( + throw new this.#window.DOMException( `Cross-Origin Request Blocked: The Same Origin Policy dissallows reading the remote resource at "${this.request.url}".`, DOMExceptionNameEnum.networkError ); @@ -322,7 +328,7 @@ export default class SyncFetch { */ public sendRequest(): ISyncResponse { if (!this.request[PropertySymbol.bodyBuffer] && this.request.body) { - throw new DOMException( + throw new this.#window.DOMException( `Streams are not supported as request body for synchrounous requests.`, DOMExceptionNameEnum.notSupportedError ); @@ -347,7 +353,7 @@ export default class SyncFetch { // If content length is 0, then there was an error if (!content.length) { - throw new DOMException( + throw new this.#window.DOMException( `Synchronous fetch to "${this.request.url}" failed.`, DOMExceptionNameEnum.networkError ); @@ -356,7 +362,7 @@ export default class SyncFetch { const { error, incomingMessage } = JSON.parse(content.toString()); if (error) { - throw new DOMException( + throw new this.#window.DOMException( `Synchronous fetch to "${this.request.url}" failed. Error: ${error}`, DOMExceptionNameEnum.networkError ); @@ -441,7 +447,7 @@ export default class SyncFetch { return Zlib.brotliDecompressSync(options.body); } } catch (error) { - throw new DOMException( + throw new this.#window.DOMException( `Failed to read response body. Error: ${error.message}.`, DOMExceptionNameEnum.encodingError ); @@ -463,7 +469,7 @@ export default class SyncFetch { switch (this.request.redirect) { case 'error': - throw new DOMException( + throw new this.#window.DOMException( `URI requested responds with a redirect, redirect mode is set to "error": ${this.request.url}`, DOMExceptionNameEnum.abortError ); @@ -480,7 +486,7 @@ export default class SyncFetch { try { locationURL = new URL(locationHeader, this.request.url); } catch { - throw new DOMException( + throw new this.#window.DOMException( `URI requested responds with an invalid redirect URL: ${locationHeader}`, DOMExceptionNameEnum.uriMismatchError ); @@ -492,7 +498,7 @@ export default class SyncFetch { } if (FetchResponseRedirectUtility.isMaxRedirectsReached(this.redirectCount)) { - throw new DOMException( + throw new this.#window.DOMException( `Maximum redirects reached at: ${this.request.url}`, DOMExceptionNameEnum.networkError ); @@ -546,7 +552,7 @@ export default class SyncFetch { return fetch.send(); default: - throw new DOMException( + throw new this.#window.DOMException( `Redirect option '${this.request.redirect}' is not a valid value of IRequestRedirect` ); } diff --git a/packages/happy-dom/src/fetch/multipart/MultipartFormDataParser.ts b/packages/happy-dom/src/fetch/multipart/MultipartFormDataParser.ts index 6044426d7..1a1589d87 100644 --- a/packages/happy-dom/src/fetch/multipart/MultipartFormDataParser.ts +++ b/packages/happy-dom/src/fetch/multipart/MultipartFormDataParser.ts @@ -2,9 +2,9 @@ import FormData from '../../form-data/FormData.js'; import { ReadableStream } from 'stream/web'; import * as PropertySymbol from '../../PropertySymbol.js'; import MultipartReader from './MultipartReader.js'; -import DOMException from '../../exception/DOMException.js'; import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; import { Buffer } from 'buffer'; +import BrowserWindow from '../../window/BrowserWindow.js'; /** * Multipart form data factory. @@ -16,16 +16,18 @@ export default class MultipartFormDataParser { /** * Returns form data. * + * @param window Window. * @param body Body. * @param contentType Content type header value. * @returns Form data. */ public static async streamToFormData( + window: BrowserWindow, body: ReadableStream, contentType: string ): Promise<{ formData: FormData; buffer: Buffer }> { if (!/multipart/i.test(contentType)) { - throw new DOMException( + throw new window.DOMException( `Failed to build FormData object: The "content-type" header isn't of type "multipart/form-data".`, DOMExceptionNameEnum.invalidStateError ); @@ -34,14 +36,14 @@ export default class MultipartFormDataParser { const match = contentType.match(/boundary=(?:"([^"]+)"|([^;]+))/i); if (!match) { - throw new DOMException( + throw new window.DOMException( `Failed to build FormData object: The "content-type" header doesn't contain any multipart boundary.`, DOMExceptionNameEnum.invalidStateError ); } const bodyReader = body.getReader(); - const reader = new MultipartReader(match[1] || match[2]); + const reader = new MultipartReader(window, match[1] || match[2]); const chunks = []; let buffer: Buffer; const bytes = 0; @@ -49,6 +51,15 @@ export default class MultipartFormDataParser { let readResult = await bodyReader.read(); while (!readResult.done) { + if (body[PropertySymbol.error]) { + throw body[PropertySymbol.error]; + } + if (body[PropertySymbol.aborted]) { + throw new window.DOMException( + 'Failed to read response body: The stream was aborted.', + DOMExceptionNameEnum.abortError + ); + } reader.write(readResult.value); readResult = await bodyReader.read(); } @@ -57,7 +68,7 @@ export default class MultipartFormDataParser { buffer = typeof chunks[0] === 'string' ? Buffer.from(chunks.join('')) : Buffer.concat(chunks, bytes); } catch (error) { - throw new DOMException( + throw new window.DOMException( `Could not create Buffer from response body. Error: ${error.message}.`, DOMExceptionNameEnum.invalidStateError ); diff --git a/packages/happy-dom/src/fetch/multipart/MultipartReader.ts b/packages/happy-dom/src/fetch/multipart/MultipartReader.ts index c064837cc..671573eae 100644 --- a/packages/happy-dom/src/fetch/multipart/MultipartReader.ts +++ b/packages/happy-dom/src/fetch/multipart/MultipartReader.ts @@ -1,7 +1,7 @@ -import DOMException from '../../exception/DOMException.js'; import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; import File from '../../file/File.js'; import FormData from '../../form-data/FormData.js'; +import BrowserWindow from '../../window/BrowserWindow.js'; enum MultiparParserStateEnum { boundary = 0, @@ -37,15 +37,18 @@ export default class MultipartReader { contentType: null, header: '' }; + private window: BrowserWindow; /** * Constructor. * + * @param window Window. * @param formData Form data. * @param boundary Boundary. */ - constructor(boundary: string) { + constructor(window: BrowserWindow, boundary: string) { const boundaryHeader = `--${boundary}`; + this.window = window; this.boundary = new Uint8Array(boundaryHeader.length); for (let i = 0, max = boundaryHeader.length; i < max; i++) { @@ -156,7 +159,7 @@ export default class MultipartReader { */ public end(): FormData { if (this.state !== MultiparParserStateEnum.data) { - throw new DOMException( + throw new this.window.DOMException( `Unexpected end of multipart stream. Expected state to be "${MultiparParserStateEnum.data}" but got "${this.state}".`, DOMExceptionNameEnum.invalidStateError ); diff --git a/packages/happy-dom/src/fetch/utilities/FetchBodyUtility.ts b/packages/happy-dom/src/fetch/utilities/FetchBodyUtility.ts index ef46a3556..a992919ba 100644 --- a/packages/happy-dom/src/fetch/utilities/FetchBodyUtility.ts +++ b/packages/happy-dom/src/fetch/utilities/FetchBodyUtility.ts @@ -10,6 +10,7 @@ import IRequestBody from '../types/IRequestBody.js'; import IResponseBody from '../types/IResponseBody.js'; import { Buffer } from 'buffer'; import Stream from 'stream'; +import BrowserWindow from '../../window/BrowserWindow.js'; /** * Fetch body utility. @@ -97,17 +98,21 @@ export default class FetchBodyUtility { * It is actually not cloning the stream. * It creates a pass through stream and pipes the original stream to it. * + * @param window Window. * @param requestOrResponse Request or Response. * @param requestOrResponse.body Body. * @param requestOrResponse.bodyUsed Body used. * @returns New stream. */ - public static cloneBodyStream(requestOrResponse: { - body: ReadableStream | null; - bodyUsed: boolean; - }): ReadableStream { + public static cloneBodyStream( + window: BrowserWindow, + requestOrResponse: { + body: ReadableStream | null; + bodyUsed: boolean; + } + ): ReadableStream { if (requestOrResponse.bodyUsed) { - throw new DOMException( + throw new window.DOMException( `Failed to clone body stream of request: Request body is already used.`, DOMExceptionNameEnum.invalidStateError ); @@ -153,10 +158,14 @@ export default class FetchBodyUtility { * https://github.com/node-fetch/node-fetch/blob/main/src/body.js (MIT) * * @see https://fetch.spec.whatwg.org/#concept-body-consume-body + * @param window Window. * @param body Body stream. * @returns Promise. */ - public static async consumeBodyStream(body: ReadableStream | null): Promise { + public static async consumeBodyStream( + window: BrowserWindow, + body: ReadableStream | null + ): Promise { if (body === null || !(body instanceof ReadableStream)) { return Buffer.alloc(0); } @@ -175,6 +184,12 @@ export default class FetchBodyUtility { if (body[PropertySymbol.error]) { throw body[PropertySymbol.error]; } + if (body[PropertySymbol.aborted]) { + throw new window.DOMException( + 'Failed to read response body: The stream was aborted.', + DOMExceptionNameEnum.abortError + ); + } const chunk = readResult.value; bytes += chunk.length; chunks.push(chunk); @@ -184,7 +199,7 @@ export default class FetchBodyUtility { if (error instanceof DOMException) { throw error; } - throw new DOMException( + throw new window.DOMException( `Failed to read response body. Error: ${error.message}.`, DOMExceptionNameEnum.encodingError ); @@ -197,7 +212,7 @@ export default class FetchBodyUtility { return Buffer.concat(chunks, bytes); } catch (error) { - throw new DOMException( + throw new window.DOMException( `Could not create Buffer from response body. Error: ${error.message}.`, DOMExceptionNameEnum.invalidStateError ); diff --git a/packages/happy-dom/src/file/FileReader.ts b/packages/happy-dom/src/file/FileReader.ts index 9eb372783..521257ae0 100644 --- a/packages/happy-dom/src/file/FileReader.ts +++ b/packages/happy-dom/src/file/FileReader.ts @@ -1,8 +1,6 @@ import WhatwgMIMEType from 'whatwg-mimetype'; import * as PropertySymbol from '../PropertySymbol.js'; -import BrowserWindow from '../window/BrowserWindow.js'; import ProgressEvent from '../event/events/ProgressEvent.js'; -import DOMException from '../exception/DOMException.js'; import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum.js'; import Blob from './Blob.js'; import FileReaderReadyStateEnum from './FileReaderReadyStateEnum.js'; @@ -31,16 +29,18 @@ export default class FileReader extends EventTarget { #isTerminated = false; #loadTimeout: NodeJS.Timeout | null = null; #parseTimeout: NodeJS.Timeout | null = null; - readonly #window: BrowserWindow; /** * Constructor. - * - * @param window Window. */ - constructor(window: BrowserWindow) { + constructor() { super(); - this.#window = window; + + if (!this[PropertySymbol.window]) { + throw new TypeError( + `Failed to construct '${this.constructor.name}': '${this.constructor.name}' was constructed outside a Window context.` + ); + } } /** @@ -84,8 +84,10 @@ export default class FileReader extends EventTarget { * Aborts the file reader. */ public abort(): void { - this.#window.clearTimeout(this.#loadTimeout); - this.#window.clearTimeout(this.#parseTimeout); + const window = this[PropertySymbol.window]; + + window.clearTimeout(this.#loadTimeout); + window.clearTimeout(this.#parseTimeout); if ( this.readyState === FileReaderReadyStateEnum.empty || @@ -113,8 +115,10 @@ export default class FileReader extends EventTarget { * @param [encoding] Encoding. */ #readFile(blob: Blob, format: FileReaderFormatEnum, encoding: string | null = null): void { + const window = this[PropertySymbol.window]; + if (this.readyState === FileReaderReadyStateEnum.loading) { - throw new DOMException( + throw new window.DOMException( 'The object is in an invalid state.', DOMExceptionNameEnum.invalidStateError ); @@ -122,7 +126,7 @@ export default class FileReader extends EventTarget { (this.readyState) = FileReaderReadyStateEnum.loading; - this.#loadTimeout = this.#window.setTimeout(() => { + this.#loadTimeout = window.setTimeout(() => { if (this.#isTerminated) { this.#isTerminated = false; return; @@ -143,7 +147,7 @@ export default class FileReader extends EventTarget { }) ); - this.#parseTimeout = this.#window.setTimeout(() => { + this.#parseTimeout = window.setTimeout(() => { if (this.#isTerminated) { this.#isTerminated = false; return; diff --git a/packages/happy-dom/src/history/History.ts b/packages/happy-dom/src/history/History.ts index 6068a4c23..3cc10bdfb 100644 --- a/packages/happy-dom/src/history/History.ts +++ b/packages/happy-dom/src/history/History.ts @@ -3,7 +3,6 @@ import HistoryScrollRestorationEnum from './HistoryScrollRestorationEnum.js'; import * as PropertySymbol from '../PropertySymbol.js'; import IHistoryItem from './IHistoryItem.js'; import BrowserFrameURL from '../browser/utilities/BrowserFrameURL.js'; -import DOMException from '../exception/DOMException.js'; import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum.js'; import BrowserWindow from '../window/BrowserWindow.js'; @@ -15,18 +14,22 @@ import BrowserWindow from '../window/BrowserWindow.js'; */ export default class History { #browserFrame: IBrowserFrame; - #ownerWindow: BrowserWindow; + #window: BrowserWindow; #currentHistoryItem: IHistoryItem; /** * Constructor. * * @param browserFrame Browser frame. - * @param ownerWindow Owner window. + * @param window Owner window. */ - constructor(browserFrame: IBrowserFrame, ownerWindow: BrowserWindow) { + constructor(browserFrame: IBrowserFrame, window: BrowserWindow) { + if (!browserFrame) { + throw new TypeError('Illegal constructor'); + } + this.#browserFrame = browserFrame; - this.#ownerWindow = ownerWindow; + this.#window = window; const history = browserFrame[PropertySymbol.history]; @@ -83,14 +86,18 @@ export default class History { * Goes to the previous page in session history. */ public back(): void { - this.#browserFrame.goBack(); + if (!this.#window.closed) { + this.#browserFrame.goBack(); + } } /** * Goes to the next page in session history. */ public forward(): void { - this.#browserFrame.goForward(); + if (!this.#window.closed) { + this.#browserFrame.goForward(); + } } /** @@ -100,7 +107,9 @@ export default class History { * @param _delta */ public go(delta: number): void { - this.#browserFrame.goSteps(delta); + if (!this.#window.closed) { + this.#browserFrame.goSteps(delta); + } } /** @@ -111,17 +120,21 @@ export default class History { * @param [url] URL. */ public pushState(state: object, title, url?: string): void { + if (this.#window.closed) { + return; + } + const history = this.#browserFrame[PropertySymbol.history]; if (!history) { return; } - const location = this.#ownerWindow[PropertySymbol.location]; + const location = this.#window[PropertySymbol.location]; const newURL = url ? BrowserFrameURL.getRelativeURL(this.#browserFrame, url) : location; if (url && newURL.origin !== location.origin) { - throw new DOMException( + throw new this.#window.DOMException( `Failed to execute 'pushState' on 'History': A history state object with URL '${url}' cannot be created in a document with origin '${location.origin}' and URL '${location.href}'.`, DOMExceptionNameEnum.securityError ); @@ -141,7 +154,7 @@ export default class History { } const newHistoryItem: IHistoryItem = { - title: title || this.#ownerWindow.document.title, + title: title || this.#window.document.title, href: newURL.href, state: JSON.parse(JSON.stringify(state)), scrollRestoration: this.#currentHistoryItem.scrollRestoration, @@ -165,17 +178,21 @@ export default class History { * @param [url] URL. */ public replaceState(state: object, title, url?: string): void { + if (this.#window.closed) { + return; + } + const history = this.#browserFrame[PropertySymbol.history]; if (!history) { return; } - const location = this.#ownerWindow[PropertySymbol.location]; + const location = this.#window[PropertySymbol.location]; const newURL = url ? BrowserFrameURL.getRelativeURL(this.#browserFrame, url) : location; if (url && newURL.origin !== location.origin) { - throw new DOMException( + throw new this.#window.DOMException( `Failed to execute 'pushState' on 'History': A history state object with URL '${url}' cannot be created in a document with origin '${location.origin}' and URL '${location.href}'.`, DOMExceptionNameEnum.securityError ); @@ -184,7 +201,7 @@ export default class History { for (let i = history.length - 1; i >= 0; i--) { if (history[i].isCurrent) { const newHistoryItem = { - title: title || this.#ownerWindow.document.title, + title: title || this.#window.document.title, href: newURL.href, state: JSON.parse(JSON.stringify(state)), scrollRestoration: history[i].scrollRestoration, diff --git a/packages/happy-dom/src/index.ts b/packages/happy-dom/src/index.ts index c68f9ddce..1e07a1dba 100644 --- a/packages/happy-dom/src/index.ts +++ b/packages/happy-dom/src/index.ts @@ -188,7 +188,7 @@ import type IBrowserPage from './browser/types/IBrowserPage.js'; import type IBrowserSettings from './browser/types/IBrowserSettings.js'; import type IOptionalBrowserSettings from './browser/types/IOptionalBrowserSettings.js'; import type IEventInit from './event/IEventInit.js'; -import type IEventListener from './event/IEventListener.js'; +import type TEventListener from './event/TEventListener.js'; import type ITouchInit from './event/ITouchInit.js'; import type IUIEventInit from './event/IUIEventInit.js'; import type IAnimationEventInit from './event/events/IAnimationEventInit.js'; @@ -217,7 +217,7 @@ export type { ICustomEventInit, IErrorEventInit, IEventInit, - IEventListener, + TEventListener, IFocusEventInit, IHashChangeEventInit, IInputEventInit, diff --git a/packages/happy-dom/src/location/Location.ts b/packages/happy-dom/src/location/Location.ts index 893932e83..3b37077a7 100644 --- a/packages/happy-dom/src/location/Location.ts +++ b/packages/happy-dom/src/location/Location.ts @@ -21,6 +21,9 @@ export default class Location { * @param url URL. */ constructor(browserFrame: IBrowserFrame, url: string) { + if (!browserFrame) { + throw new TypeError('Illegal constructor'); + } this.#browserFrame = browserFrame; this.#url = new URL(url); } @@ -40,6 +43,10 @@ export default class Location { * @param hash Value. */ public set hash(hash: string) { + if (!this.#browserFrame) { + return; + } + const oldURL = this.#url.href; this.#url.hash = hash; const newURL = this.#url.href; @@ -68,9 +75,7 @@ export default class Location { public set host(host: string) { const url = new URL(this.#url.href); url.host = host; - this.#browserFrame - .goto(url.href) - .catch((error) => this.#browserFrame.page.console.error(error)); + this.href = url.href; } /** @@ -90,9 +95,7 @@ export default class Location { public set hostname(hostname: string) { const url = new URL(this.#url.href); url.hostname = hostname; - this.#browserFrame - .goto(url.href) - .catch((error) => this.#browserFrame.page.console.error(error)); + this.href = url.href; } /** @@ -106,7 +109,17 @@ export default class Location { * Override set href. */ public set href(url: string) { - this.#browserFrame.goto(url).catch((error) => this.#browserFrame.page.console.error(error)); + if (!this.#browserFrame) { + return; + } + + this.#browserFrame.goto(url).catch((error) => { + if (this.#browserFrame.page?.console) { + this.#browserFrame.page.console.error(error); + } else { + throw error; + } + }); } /** @@ -135,9 +148,7 @@ export default class Location { public set pathname(pathname: string) { const url = new URL(this.#url.href); url.pathname = pathname; - this.#browserFrame - .goto(url.href) - .catch((error) => this.#browserFrame.page.console.error(error)); + this.href = url.href; } /** @@ -157,9 +168,7 @@ export default class Location { public set port(port: string) { const url = new URL(this.#url.href); url.port = port; - this.#browserFrame - .goto(url.href) - .catch((error) => this.#browserFrame.page.console.error(error)); + this.href = url.href; } /** @@ -179,9 +188,7 @@ export default class Location { public set protocol(protocol: string) { const url = new URL(this.#url.href); url.protocol = protocol; - this.#browserFrame - .goto(url.href) - .catch((error) => this.#browserFrame.page.console.error(error)); + this.href = url.href; } /** @@ -201,9 +208,7 @@ export default class Location { public set search(search: string) { const url = new URL(this.#url.href); url.search = search; - this.#browserFrame - .goto(url.href) - .catch((error) => this.#browserFrame.page.console.error(error)); + this.href = url.href; } /** @@ -228,9 +233,17 @@ export default class Location { * Reloads the resource from the current URL. */ public reload(): void { - this.#browserFrame - .goto(this.href) - .catch((error) => this.#browserFrame.page.console.error(error)); + if (!this.#browserFrame) { + return; + } + + this.#browserFrame.goto(this.href).catch((error) => { + if (this.#browserFrame.page?.console) { + this.#browserFrame.page.console.error(error); + } else { + throw error; + } + }); } /** @@ -240,6 +253,10 @@ export default class Location { * @param url URL. */ public [PropertySymbol.setURL](browserFrame: IBrowserFrame, url: string): void { + if (!this.#browserFrame) { + return; + } + if (this.#browserFrame !== browserFrame) { throw new Error('Failed to set URL. Browser frame mismatch.'); } @@ -247,6 +264,13 @@ export default class Location { this.#url.href = url; } + /** + * Destroys the location. + */ + public [PropertySymbol.destroy](): void { + this.#browserFrame = null; + } + /** * Returns the URL as a string. * diff --git a/packages/happy-dom/src/match-media/MediaQueryItem.ts b/packages/happy-dom/src/match-media/MediaQueryItem.ts index afdffa23b..1122371b0 100644 --- a/packages/happy-dom/src/match-media/MediaQueryItem.ts +++ b/packages/happy-dom/src/match-media/MediaQueryItem.ts @@ -1,6 +1,6 @@ import CSSMeasurementConverter from '../css/declaration/measurement-converter/CSSMeasurementConverter.js'; import BrowserWindow from '../window/BrowserWindow.js'; -import WindowBrowserSettingsReader from '../window/WindowBrowserSettingsReader.js'; +import WindowBrowserContext from '../window/WindowBrowserContext.js'; import IMediaQueryRange from './IMediaQueryRange.js'; import IMediaQueryRule from './IMediaQueryRule.js'; import MediaQueryTypeEnum from './MediaQueryTypeEnum.js'; @@ -14,13 +14,13 @@ export default class MediaQueryItem { public rules: IMediaQueryRule[]; public ranges: IMediaQueryRange[]; private rootFontSize: string | number | null = null; - private ownerWindow: BrowserWindow; + private window: BrowserWindow; /** * Constructor. * * @param options Options. - * @param options.ownerWindow Owner window. + * @param options.window Owner window. * @param [options.rootFontSize] Root font size. * @param [options.mediaTypes] Media types. * @param [options.not] Not. @@ -28,14 +28,14 @@ export default class MediaQueryItem { * @param [options.ranges] Ranges. */ constructor(options: { - ownerWindow: BrowserWindow; + window: BrowserWindow; rootFontSize?: string | number | null; mediaTypes?: MediaQueryTypeEnum[]; not?: boolean; rules?: IMediaQueryRule[]; ranges?: IMediaQueryRange[]; }) { - this.ownerWindow = options.ownerWindow; + this.window = options.window; this.rootFontSize = options.rootFontSize || null; this.mediaTypes = options.mediaTypes || []; this.not = options.not || false; @@ -115,7 +115,7 @@ export default class MediaQueryItem { if (mediaType === MediaQueryTypeEnum.all) { return true; } - return mediaType === WindowBrowserSettingsReader.getSettings(this.ownerWindow).device.mediaType; + return mediaType === new WindowBrowserContext(this.window).getSettings()?.device.mediaType; } /** @@ -125,8 +125,7 @@ export default class MediaQueryItem { * @returns "true" if the range matches. */ private matchesRange(range: IMediaQueryRange): boolean { - const windowSize = - range.type === 'width' ? this.ownerWindow.innerWidth : this.ownerWindow.innerHeight; + const windowSize = range.type === 'width' ? this.window.innerWidth : this.window.innerHeight; if (range.before) { const beforeValue = this.toPixels(range.before.value); @@ -226,38 +225,38 @@ export default class MediaQueryItem { switch (rule.name) { case 'min-width': const minWidth = this.toPixels(rule.value); - return minWidth !== null && this.ownerWindow.innerWidth >= minWidth; + return minWidth !== null && this.window.innerWidth >= minWidth; case 'max-width': const maxWidth = this.toPixels(rule.value); - return maxWidth !== null && this.ownerWindow.innerWidth <= maxWidth; + return maxWidth !== null && this.window.innerWidth <= maxWidth; case 'min-height': const minHeight = this.toPixels(rule.value); - return minHeight !== null && this.ownerWindow.innerHeight >= minHeight; + return minHeight !== null && this.window.innerHeight >= minHeight; case 'max-height': const maxHeight = this.toPixels(rule.value); - return maxHeight !== null && this.ownerWindow.innerHeight <= maxHeight; + return maxHeight !== null && this.window.innerHeight <= maxHeight; case 'width': const width = this.toPixels(rule.value); - return width !== null && this.ownerWindow.innerWidth === width; + return width !== null && this.window.innerWidth === width; case 'height': const height = this.toPixels(rule.value); - return height !== null && this.ownerWindow.innerHeight === height; + return height !== null && this.window.innerHeight === height; case 'orientation': return rule.value === 'landscape' - ? this.ownerWindow.innerWidth > this.ownerWindow.innerHeight - : this.ownerWindow.innerWidth < this.ownerWindow.innerHeight; + ? this.window.innerWidth > this.window.innerHeight + : this.window.innerWidth < this.window.innerHeight; case 'prefers-color-scheme': return ( rule.value === - WindowBrowserSettingsReader.getSettings(this.ownerWindow).device.prefersColorScheme + new WindowBrowserContext(this.window).getSettings().device.prefersColorScheme ); case 'any-hover': case 'hover': if (rule.value === 'none') { - return this.ownerWindow.navigator.maxTouchPoints > 0; + return this.window.navigator.maxTouchPoints > 0; } if (rule.value === 'hover') { - return this.ownerWindow.navigator.maxTouchPoints === 0; + return this.window.navigator.maxTouchPoints === 0; } return false; case 'any-pointer': @@ -267,11 +266,11 @@ export default class MediaQueryItem { } if (rule.value === 'coarse') { - return this.ownerWindow.navigator.maxTouchPoints > 0; + return this.window.navigator.maxTouchPoints > 0; } if (rule.value === 'fine') { - return this.ownerWindow.navigator.maxTouchPoints === 0; + return this.window.navigator.maxTouchPoints === 0; } return false; @@ -292,17 +291,17 @@ export default class MediaQueryItem { case 'min-aspect-ratio': return ( aspectRatioWidth / aspectRatioHeight <= - this.ownerWindow.innerWidth / this.ownerWindow.innerHeight + this.window.innerWidth / this.window.innerHeight ); case 'max-aspect-ratio': return ( aspectRatioWidth / aspectRatioHeight >= - this.ownerWindow.innerWidth / this.ownerWindow.innerHeight + this.window.innerWidth / this.window.innerHeight ); case 'aspect-ratio': return ( aspectRatioWidth / aspectRatioHeight === - this.ownerWindow.innerWidth / this.ownerWindow.innerHeight + this.window.innerWidth / this.window.innerHeight ); } } @@ -318,23 +317,21 @@ export default class MediaQueryItem { */ private toPixels(value: string): number | null { if ( - !WindowBrowserSettingsReader.getSettings(this.ownerWindow).disableComputedStyleRendering && + !new WindowBrowserContext(this.window).getSettings()?.disableComputedStyleRendering && value.endsWith('em') ) { this.rootFontSize = this.rootFontSize || - parseFloat( - this.ownerWindow.getComputedStyle(this.ownerWindow.document.documentElement).fontSize - ); + parseFloat(this.window.getComputedStyle(this.window.document.documentElement).fontSize); return CSSMeasurementConverter.toPixels({ - ownerWindow: this.ownerWindow, + window: this.window, value, rootFontSize: this.rootFontSize, parentFontSize: this.rootFontSize }); } return CSSMeasurementConverter.toPixels({ - ownerWindow: this.ownerWindow, + window: this.window, value, rootFontSize: 16, parentFontSize: 16 diff --git a/packages/happy-dom/src/match-media/MediaQueryList.ts b/packages/happy-dom/src/match-media/MediaQueryList.ts index 6ba648595..90c9406b7 100644 --- a/packages/happy-dom/src/match-media/MediaQueryList.ts +++ b/packages/happy-dom/src/match-media/MediaQueryList.ts @@ -2,7 +2,7 @@ import EventTarget from '../event/EventTarget.js'; import * as PropertySymbol from '../PropertySymbol.js'; import Event from '../event/Event.js'; import BrowserWindow from '../window/BrowserWindow.js'; -import IEventListener from '../event/IEventListener.js'; +import TEventListener from '../event/TEventListener.js'; import MediaQueryListEvent from '../event/events/MediaQueryListEvent.js'; import IMediaQueryItem from './MediaQueryItem.js'; import MediaQueryParser from './MediaQueryParser.js'; @@ -15,7 +15,7 @@ import MediaQueryParser from './MediaQueryParser.js'; */ export default class MediaQueryList extends EventTarget { public onchange: (event: Event) => void = null; - #ownerWindow: BrowserWindow; + #window: BrowserWindow; #items: IMediaQueryItem[] | null = null; #media: string; #rootFontSize: string | number | null = null; @@ -24,17 +24,13 @@ export default class MediaQueryList extends EventTarget { * Constructor. * * @param options Options. - * @param options.ownerWindow Owner window. + * @param options.window Owner window. * @param options.media Media. * @param [options.rootFontSize] Root font size. */ - constructor(options: { - ownerWindow: BrowserWindow; - media: string; - rootFontSize?: string | number; - }) { + constructor(options: { window: BrowserWindow; media: string; rootFontSize?: string | number }) { super(); - this.#ownerWindow = options.ownerWindow; + this.#window = options.window; this.#media = options.media; this.#rootFontSize = options.rootFontSize || null; } @@ -48,7 +44,7 @@ export default class MediaQueryList extends EventTarget { this.#items = this.#items || MediaQueryParser.parse({ - ownerWindow: this.#ownerWindow, + window: this.#window, mediaQuery: this.#media, rootFontSize: this.#rootFontSize }); @@ -65,7 +61,7 @@ export default class MediaQueryList extends EventTarget { this.#items = this.#items || MediaQueryParser.parse({ - ownerWindow: this.#ownerWindow, + window: this.#window, mediaQuery: this.#media, rootFontSize: this.#rootFontSize }); @@ -102,7 +98,7 @@ export default class MediaQueryList extends EventTarget { /** * @override */ - public addEventListener(type: string, listener: IEventListener | ((event: Event) => void)): void { + public addEventListener(type: string, listener: TEventListener): void { super.addEventListener(type, listener); if (type === 'change') { let matchesState = false; @@ -114,23 +110,17 @@ export default class MediaQueryList extends EventTarget { } }; listener[PropertySymbol.windowResizeListener] = resizeListener; - this.#ownerWindow.addEventListener('resize', resizeListener); + this.#window.addEventListener('resize', resizeListener); } } /** * @override */ - public removeEventListener( - type: string, - listener: IEventListener | ((event: Event) => void) - ): void { + public removeEventListener(type: string, listener: TEventListener): void { super.removeEventListener(type, listener); if (type === 'change' && listener[PropertySymbol.windowResizeListener]) { - this.#ownerWindow.removeEventListener( - 'resize', - listener[PropertySymbol.windowResizeListener] - ); + this.#window.removeEventListener('resize', listener[PropertySymbol.windowResizeListener]); } } } diff --git a/packages/happy-dom/src/match-media/MediaQueryParser.ts b/packages/happy-dom/src/match-media/MediaQueryParser.ts index 69e600adb..cf8f7bbc5 100644 --- a/packages/happy-dom/src/match-media/MediaQueryParser.ts +++ b/packages/happy-dom/src/match-media/MediaQueryParser.ts @@ -38,18 +38,18 @@ export default class MediaQueryParser { * Parses a media query string. * * @param options Options. - * @param options.ownerWindow Owner window. + * @param options.window Owner window. * @param options.mediaQuery Media query string. * @param [options.rootFontSize] Root font size. * @returns Media query items. */ public static parse(options: { - ownerWindow: BrowserWindow; + window: BrowserWindow; mediaQuery: string; rootFontSize?: string | number | null; }): MediaQueryItem[] { let currentMediaQueryItem: MediaQueryItem = new MediaQueryItem({ - ownerWindow: options.ownerWindow, + window: options.window, rootFontSize: options.rootFontSize }); const mediaQueryItems: MediaQueryItem[] = [currentMediaQueryItem]; @@ -59,7 +59,7 @@ export default class MediaQueryParser { while ((match = regexp.exec(options.mediaQuery.toLowerCase()))) { if (match[4] === ',' || match[5] === 'or') { currentMediaQueryItem = new MediaQueryItem({ - ownerWindow: options.ownerWindow, + window: options.window, rootFontSize: options.rootFontSize }); mediaQueryItems.push(currentMediaQueryItem); @@ -93,7 +93,7 @@ export default class MediaQueryParser { if (!trimmedValue && !match[3]) { return [ new MediaQueryItem({ - ownerWindow: options.ownerWindow, + window: options.window, rootFontSize: options.rootFontSize, not: true, mediaTypes: [MediaQueryTypeEnum.all] diff --git a/packages/happy-dom/src/mutation-observer/MutationObserver.ts b/packages/happy-dom/src/mutation-observer/MutationObserver.ts index 25947a667..ffb4a7f91 100644 --- a/packages/happy-dom/src/mutation-observer/MutationObserver.ts +++ b/packages/happy-dom/src/mutation-observer/MutationObserver.ts @@ -1,9 +1,9 @@ import * as PropertySymbol from '../PropertySymbol.js'; import Node from '../nodes/node/Node.js'; +import BrowserWindow from '../window/BrowserWindow.js'; import IMutationObserverInit from './IMutationObserverInit.js'; import MutationObserverListener from './MutationObserverListener.js'; import MutationRecord from './MutationRecord.js'; -import BrowserWindow from '../window/BrowserWindow.js'; /** * The MutationObserver interface provides the ability to watch for changes being made to the DOM tree. @@ -11,9 +11,11 @@ import BrowserWindow from '../window/BrowserWindow.js'; * @see https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver */ export default class MutationObserver { + // Injected by WindowClassExtender + protected declare [PropertySymbol.window]: BrowserWindow; #callback: (records: MutationRecord[], observer: MutationObserver) => void; #listeners: MutationObserverListener[] = []; - #window: BrowserWindow | null = null; + #destroyed: boolean = false; /** * Constructor. @@ -21,6 +23,12 @@ export default class MutationObserver { * @param callback Callback. */ constructor(callback: (records: MutationRecord[], observer: MutationObserver) => void) { + if (!this[PropertySymbol.window]) { + throw new TypeError( + `Failed to construct '${this.constructor.name}': '${this.constructor.name}' was constructed outside a Window context.` + ); + } + this.#callback = callback; } @@ -31,8 +39,12 @@ export default class MutationObserver { * @param options Options. */ public observe(target: Node, options: IMutationObserverInit): void { + if (this.#destroyed) { + return; + } + if (!target) { - throw new TypeError( + throw new this[PropertySymbol.window].TypeError( `Failed to execute 'observe' on 'MutationObserver': The first parameter "target" should be of type "Node".` ); } @@ -47,13 +59,13 @@ export default class MutationObserver { } if (!options.attributes && options.attributeOldValue) { - throw new TypeError( + throw new this[PropertySymbol.window].TypeError( `Failed to execute 'observe' on 'MutationObserver': The options object may only set 'attributeOldValue' to true when 'attributes' is true or not present.` ); } if (!options.attributes && options.attributeFilter) { - throw new TypeError( + throw new this[PropertySymbol.window].TypeError( `Failed to execute 'observe' on 'MutationObserver': The options object may only set 'attributeFilter' when 'attributes' is true or not present.` ); } @@ -68,24 +80,18 @@ export default class MutationObserver { } if (!options.characterData && options.characterDataOldValue) { - throw new TypeError( + throw new this[PropertySymbol.window].TypeError( `Failed to execute 'observe' on 'MutationObserver': The options object may only set 'characterDataOldValue' to true when 'characterData' is true or not present.` ); } } if (!options || (!options.childList && !options.attributes && !options.characterData)) { - throw new TypeError( + throw new this[PropertySymbol.window].TypeError( `Failed to execute 'observe' on 'MutationObserver': The options object must set at least one of 'attributes', 'characterData', or 'childList' to true.` ); } - if (!this.#window) { - this.#window = target[PropertySymbol.ownerDocument] - ? target[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow] - : target[PropertySymbol.ownerWindow]; - } - // Makes sure that attribute names are lower case. // TODO: Is this correct? options = Object.assign({}, options, { @@ -105,7 +111,7 @@ export default class MutationObserver { } const listener = new MutationObserverListener({ - window: this.#window, + window: this[PropertySymbol.window], options, callback: this.#callback.bind(this), observer: this, @@ -115,8 +121,8 @@ export default class MutationObserver { this.#listeners.push(listener); // Stores all observers on the window object, so that they can be disconnected when the window is closed. - if (!this.#window[PropertySymbol.mutationObservers].includes(this)) { - this.#window[PropertySymbol.mutationObservers].push(this); + if (!this[PropertySymbol.window][PropertySymbol.mutationObservers].includes(this)) { + this[PropertySymbol.window][PropertySymbol.mutationObservers].push(this); } // Starts observing target node. @@ -127,6 +133,8 @@ export default class MutationObserver { * Disconnects. */ public disconnect(): void { + this.#destroyed = true; + if (this.#listeners.length === 0) { return; } @@ -138,7 +146,7 @@ export default class MutationObserver { this.#listeners = []; - const mutationObservers = this.#window[PropertySymbol.mutationObservers]; + const mutationObservers = this[PropertySymbol.window][PropertySymbol.mutationObservers]; const index = mutationObservers.indexOf(this); if (index !== -1) { diff --git a/packages/happy-dom/src/navigator/Navigator.ts b/packages/happy-dom/src/navigator/Navigator.ts index ebf93067a..ed0664356 100644 --- a/packages/happy-dom/src/navigator/Navigator.ts +++ b/packages/happy-dom/src/navigator/Navigator.ts @@ -3,7 +3,7 @@ import PluginArray from './PluginArray.js'; import BrowserWindow from '../window/BrowserWindow.js'; import Permissions from '../permissions/Permissions.js'; import Clipboard from '../clipboard/Clipboard.js'; -import WindowBrowserSettingsReader from '../window/WindowBrowserSettingsReader.js'; +import WindowBrowserContext from '../window/WindowBrowserContext.js'; import Blob from '../file/Blob.js'; import FormData from '../form-data/FormData.js'; @@ -16,19 +16,23 @@ import FormData from '../form-data/FormData.js'; * https://html.spec.whatwg.org/multipage/system-state.html#dom-navigator. */ export default class Navigator { - #ownerWindow: BrowserWindow; + #window: BrowserWindow; #clipboard: Clipboard; #permissions: Permissions; /** * Constructor. * - * @param ownerWindow Owner window. + * @param window Owner window. */ - constructor(ownerWindow: BrowserWindow) { - this.#ownerWindow = ownerWindow; - this.#clipboard = new Clipboard(ownerWindow); - this.#permissions = new Permissions(); + constructor(window: BrowserWindow) { + if (!window) { + throw new TypeError('Invalid constructor'); + } + + this.#window = window; + this.#clipboard = new Clipboard(window); + this.#permissions = new Permissions(window); } /** @@ -77,9 +81,7 @@ export default class Navigator { * Maximum number of simultaneous touch contact points are supported by the current device. */ public get maxTouchPoints(): number { - return ( - WindowBrowserSettingsReader.getSettings(this.#ownerWindow)?.navigator.maxTouchPoints || 0 - ); + return new WindowBrowserContext(this.#window).getSettings()?.navigator.maxTouchPoints || 0; } /** @@ -156,7 +158,7 @@ export default class Navigator { * "appCodeName/appVersion number (Platform; Security; OS-or-CPU; Localization; rv: revision-version-number) product/productSub Application-Name Application-Name-version". */ public get userAgent(): string { - return WindowBrowserSettingsReader.getSettings(this.#ownerWindow)?.navigator.userAgent || ''; + return new WindowBrowserContext(this.#window).getSettings()?.navigator.userAgent || ''; } /** @@ -227,7 +229,7 @@ export default class Navigator { url: string, data: string | Blob | ArrayBuffer | ArrayBufferView | FormData ): boolean { - this.#ownerWindow.fetch(url, { + this.#window.fetch(url, { method: 'POST', body: data }); diff --git a/packages/happy-dom/src/nodes/NodeFactory.ts b/packages/happy-dom/src/nodes/NodeFactory.ts index 120419569..41ab98283 100644 --- a/packages/happy-dom/src/nodes/NodeFactory.ts +++ b/packages/happy-dom/src/nodes/NodeFactory.ts @@ -21,7 +21,7 @@ export default class NodeFactory { nodeClass: new (...args) => T, ...args: any[] ): T { - if (!nodeClass[PropertySymbol.ownerDocument]) { + if (!nodeClass.prototype[PropertySymbol.window]) { this.ownerDocuments.push(ownerDocument); } return new nodeClass(...args); diff --git a/packages/happy-dom/src/nodes/document/Document.ts b/packages/happy-dom/src/nodes/document/Document.ts index 6f16b00b3..8fefd0755 100644 --- a/packages/happy-dom/src/nodes/document/Document.ts +++ b/packages/happy-dom/src/nodes/document/Document.ts @@ -14,7 +14,6 @@ import DocumentType from '../document-type/DocumentType.js'; import ParentNodeUtility from '../parent-node/ParentNodeUtility.js'; import QuerySelector from '../../query-selector/QuerySelector.js'; import CSSStyleSheet from '../../css/CSSStyleSheet.js'; -import DOMException from '../../exception/DOMException.js'; import HTMLScriptElement from '../html-script-element/HTMLScriptElement.js'; import HTMLElement from '../html-element/HTMLElement.js'; import Comment from '../comment/Comment.js'; @@ -33,7 +32,6 @@ import ProcessingInstruction from '../processing-instruction/ProcessingInstructi import VisibilityStateEnum from './VisibilityStateEnum.js'; import NodeTypeEnum from '../node/NodeTypeEnum.js'; import CookieStringUtility from '../../cookie/urilities/CookieStringUtility.js'; -import IBrowserFrame from '../../browser/types/IBrowserFrame.js'; import { URL } from 'url'; import IHTMLElementTagNameMap from '../../config/IHTMLElementTagNameMap.js'; import ISVGElementTagNameMap from '../../config/ISVGElementTagNameMap.js'; @@ -47,6 +45,7 @@ import HTMLHeadElement from '../html-head-element/HTMLHeadElement.js'; import HTMLBaseElement from '../html-base-element/HTMLBaseElement.js'; import ICachedResult from '../node/ICachedResult.js'; import HTMLTitleElement from '../html-title-element/HTMLTitleElement.js'; +import WindowBrowserContext from '../../window/WindowBrowserContext.js'; import NodeFactory from '../NodeFactory.js'; const PROCESSING_INSTRUCTION_TARGET_REGEXP = /^[a-z][a-z0-9-]+$/; @@ -55,9 +54,6 @@ const PROCESSING_INSTRUCTION_TARGET_REGEXP = /^[a-z][a-z0-9-]+$/; * Document. */ export default class Document extends Node { - // Static properties - public static [PropertySymbol.ownerDocument]: Document = {}; - // Internal properties public [PropertySymbol.children]: HTMLCollection | null = null; public [PropertySymbol.activeElement]: HTMLElement | SVGElement = null; @@ -73,9 +69,9 @@ export default class Document extends Node { public [PropertySymbol.readyState] = DocumentReadyStateEnum.interactive; public [PropertySymbol.referrer] = ''; public [PropertySymbol.defaultView]: BrowserWindow | null = null; - public [PropertySymbol.ownerWindow]: BrowserWindow; public [PropertySymbol.forms]: HTMLCollection | null = null; public [PropertySymbol.affectsComputedStyleCache]: ICachedResult[] = []; + public [PropertySymbol.ownerDocument]: Document | null = null; public [PropertySymbol.elementIdMap]: Map< string, { htmlCollection: HTMLCollection | null; elements: Element[] } @@ -84,7 +80,6 @@ export default class Document extends Node { // Private properties #selection: Selection = null; - #browserFrame: IBrowserFrame; // Events public onreadystatechange: (event: Event) => void = null; @@ -197,20 +192,6 @@ export default class Document extends Node { public onpaste: (event: Event) => void = null; public onbeforematch: (event: Event) => void = null; - /** - * Constructor. - * - * @param injected Injected properties. - * @param injected.browserFrame Browser frame. - * @param injected.window Window. - */ - constructor(injected: { browserFrame: IBrowserFrame; window: BrowserWindow }) { - super(); - this.#browserFrame = injected.browserFrame; - this[PropertySymbol.ownerWindow] = injected.window; - this[PropertySymbol.ownerDocument] = null; - } - /** * Returns adopted style sheets. * @@ -383,9 +364,13 @@ export default class Document extends Node { * @returns Cookie. */ public get cookie(): string { + const browserFrame = new WindowBrowserContext(this[PropertySymbol.window]).getBrowserFrame(); + if (!browserFrame) { + return ''; + } return CookieStringUtility.cookiesToString( - this.#browserFrame.page.context.cookieContainer.getCookies( - new URL(this[PropertySymbol.ownerWindow].location.href), + browserFrame.page.context.cookieContainer.getCookies( + new URL(this[PropertySymbol.window].location.href), true ) ); @@ -397,11 +382,12 @@ export default class Document extends Node { * @param cookie Cookie string. */ public set cookie(cookie: string) { - this.#browserFrame.page.context.cookieContainer.addCookies([ - CookieStringUtility.stringToCookie( - new URL(this[PropertySymbol.ownerWindow].location.href), - cookie - ) + const browserFrame = new WindowBrowserContext(this[PropertySymbol.window]).getBrowserFrame(); + if (!browserFrame) { + return; + } + browserFrame.page.context.cookieContainer.addCookies([ + CookieStringUtility.stringToCookie(new URL(this[PropertySymbol.window].location.href), cookie) ]); } @@ -519,7 +505,7 @@ export default class Document extends Node { * @returns Location. */ public get location(): Location { - return this[PropertySymbol.ownerWindow].location; + return this[PropertySymbol.window].location; } /** @@ -542,7 +528,7 @@ export default class Document extends Node { if (element) { return element.href; } - return this[PropertySymbol.ownerWindow].location.href; + return this[PropertySymbol.window].location.href; } /** @@ -551,7 +537,7 @@ export default class Document extends Node { * @returns URL of the current document. * */ public get URL(): string { - return this[PropertySymbol.ownerWindow].location.href; + return this[PropertySymbol.window].location.href; } /** @@ -569,7 +555,7 @@ export default class Document extends Node { * @returns Domain. * */ public get domain(): string { - return this[PropertySymbol.ownerWindow].location.hostname; + return this[PropertySymbol.window].location.hostname; } /** @@ -717,7 +703,7 @@ export default class Document extends Node { */ public queryCommandSupported(_: string): boolean { if (!arguments.length) { - throw new TypeError( + throw new this[PropertySymbol.window].TypeError( "Failed to execute 'queryCommandSupported' on 'Document': 1 argument required, but only 0 present." ); } @@ -946,8 +932,17 @@ export default class Document extends Node { public open(): Document { this[PropertySymbol.isFirstWriteAfterOpen] = true; - for (const eventType of Object.keys(this[PropertySymbol.listeners])) { - const listeners = this[PropertySymbol.listeners][eventType]; + for (const eventType of this[PropertySymbol.listeners].bubbling.keys()) { + const listeners = this[PropertySymbol.listeners].bubbling.get(eventType); + if (listeners) { + for (const listener of listeners) { + this.removeEventListener(eventType, listener); + } + } + } + + for (const eventType of this[PropertySymbol.listeners].capturing.keys()) { + const listeners = this[PropertySymbol.listeners].capturing.get(eventType); if (listeners) { for (const listener of listeners) { this.removeEventListener(eventType, listener); @@ -1080,7 +1075,7 @@ export default class Document extends Node { qualifiedName = String(qualifiedName); if (!qualifiedName) { - throw new DOMException( + throw new this[PropertySymbol.window].DOMException( "Failed to execute 'createElementNS' on 'Document': The qualified name provided is empty." ); } @@ -1089,8 +1084,8 @@ export default class Document extends Node { if (namespaceURI === NamespaceURI.svg) { const elementClass = qualifiedName === 'svg' - ? this[PropertySymbol.ownerWindow].SVGSVGElement - : this[PropertySymbol.ownerWindow].SVGElement; + ? this[PropertySymbol.window].SVGSVGElement + : this[PropertySymbol.window].SVGElement; const element = NodeFactory.createNode(this, elementClass); @@ -1104,7 +1099,7 @@ export default class Document extends Node { // Custom HTML element const customElement = - this[PropertySymbol.ownerWindow].customElements[PropertySymbol.registry]?.[ + this[PropertySymbol.window].customElements[PropertySymbol.registry]?.[ options && options.is ? String(options.is) : qualifiedName ]; @@ -1119,7 +1114,7 @@ export default class Document extends Node { const localName = qualifiedName.toLowerCase(); const elementClass = HTMLElementConfig[localName] - ? this[PropertySymbol.ownerWindow][HTMLElementConfig[localName].className] + ? this[PropertySymbol.window][HTMLElementConfig[localName].className] : null; // Known HTML element @@ -1136,8 +1131,8 @@ export default class Document extends Node { // Unknown HTML element const unknownElementClass = localName.includes('-') - ? this[PropertySymbol.ownerWindow].HTMLElement - : this[PropertySymbol.ownerWindow].HTMLUnknownElement; + ? this[PropertySymbol.window].HTMLElement + : this[PropertySymbol.window].HTMLUnknownElement; const element = NodeFactory.createNode(this, unknownElementClass); @@ -1159,11 +1154,12 @@ export default class Document extends Node { */ public createTextNode(data: string): Text { if (arguments.length < 1) { - throw new TypeError( + throw new this[PropertySymbol.window].TypeError( `Failed to execute 'createTextNode' on 'Document': 1 argument required, but only ${arguments.length} present.` ); } - return new this[PropertySymbol.ownerWindow].Text(String(data)); + // We should use the NodeFactory and not the class constructor, so that owner document will be this document + return NodeFactory.createNode(this, Text, String(data)); } /** @@ -1174,11 +1170,12 @@ export default class Document extends Node { */ public createComment(data?: string): Comment { if (arguments.length < 1) { - throw new TypeError( + throw new this[PropertySymbol.window].TypeError( `Failed to execute 'createComment' on 'Document': 1 argument required, but only ${arguments.length} present.` ); } - return new this[PropertySymbol.ownerWindow].Comment(String(data)); + // We should use the NodeFactory and not the class constructor, so that owner document will be this document + return NodeFactory.createNode(this, Comment, String(data)); } /** @@ -1187,7 +1184,8 @@ export default class Document extends Node { * @returns Document fragment. */ public createDocumentFragment(): DocumentFragment { - return new this[PropertySymbol.ownerWindow].DocumentFragment(); + // We should use the NodeFactory and not the class constructor, so that owner document will be this document + return NodeFactory.createNode(this, DocumentFragment); } /** @@ -1220,8 +1218,8 @@ export default class Document extends Node { * @returns Event. */ public createEvent(type: string): Event { - if (typeof this[PropertySymbol.ownerWindow][type] === 'function') { - return new this[PropertySymbol.ownerWindow][type]('init'); + if (typeof this[PropertySymbol.window][type] === 'function') { + return new this[PropertySymbol.window][type]('init'); } return new Event('init'); } @@ -1244,7 +1242,8 @@ export default class Document extends Node { * @returns Element. */ public createAttributeNS(namespaceURI: string, qualifiedName: string): Attr { - const attribute = NodeFactory.createNode(this, this[PropertySymbol.ownerWindow].Attr); + // We should use the NodeFactory and not the class constructor, so that owner document will be this document + const attribute = NodeFactory.createNode(this, Attr); const parts = qualifiedName.split(':'); attribute[PropertySymbol.namespaceURI] = namespaceURI; @@ -1263,7 +1262,7 @@ export default class Document extends Node { */ public importNode(node: Node, deep = false): Node { if (!(node instanceof Node)) { - throw new DOMException('Parameter 1 was not of type Node.'); + throw new this[PropertySymbol.window].DOMException('Parameter 1 was not of type Node.'); } const clone = node.cloneNode(deep); this.#importNode(clone); @@ -1276,7 +1275,7 @@ export default class Document extends Node { * @returns Range. */ public createRange(): Range { - return new this[PropertySymbol.ownerWindow].Range(); + return new this[PropertySymbol.window].Range(); } /** @@ -1287,7 +1286,7 @@ export default class Document extends Node { */ public adoptNode(node: Node): Node { if (!(node instanceof Node)) { - throw new DOMException('Parameter 1 was not of type Node.'); + throw new this[PropertySymbol.window].DOMException('Parameter 1 was not of type Node.'); } const adopted = node[PropertySymbol.parentNode] @@ -1328,7 +1327,7 @@ export default class Document extends Node { */ public createProcessingInstruction(target: string, data: string): ProcessingInstruction { if (arguments.length < 2) { - throw new TypeError( + throw new this[PropertySymbol.window].TypeError( `Failed to execute 'createProcessingInstruction' on 'Document': 2 arguments required, but only ${arguments.length} present.` ); } @@ -1337,20 +1336,17 @@ export default class Document extends Node { data = String(data); if (!target || !PROCESSING_INSTRUCTION_TARGET_REGEXP.test(target)) { - throw new DOMException( + throw new this[PropertySymbol.window].DOMException( `Failed to execute 'createProcessingInstruction' on 'Document': The target provided ('${target}') is not a valid name.` ); } if (data.includes('?>')) { - throw new DOMException( + throw new this[PropertySymbol.window].DOMException( `Failed to execute 'createProcessingInstruction' on 'Document': The data provided ('?>') contains '?>'` ); } - - const element = NodeFactory.createNode( - this, - this[PropertySymbol.ownerWindow].ProcessingInstruction - ); + // We should use the NodeFactory and not the class constructor, so that owner document will be this document + const element = NodeFactory.createNode(this, ProcessingInstruction); element[PropertySymbol.data] = data; element[PropertySymbol.target] = target; diff --git a/packages/happy-dom/src/nodes/document/DocumentReadyStateManager.ts b/packages/happy-dom/src/nodes/document/DocumentReadyStateManager.ts index a4d3de2fc..1224f7d96 100644 --- a/packages/happy-dom/src/nodes/document/DocumentReadyStateManager.ts +++ b/packages/happy-dom/src/nodes/document/DocumentReadyStateManager.ts @@ -30,9 +30,8 @@ export default class DocumentReadyStateManager { resolve(); } else { this.readyStateCallbacks.push(resolve); - if (this.totalTasks === 0 && !this.immediate) { - this.immediate = this.window.requestAnimationFrame(this.endTask.bind(this)); - } + this.startTask(); + this.endTask(); } }); } @@ -68,15 +67,19 @@ export default class DocumentReadyStateManager { this.totalTasks--; - if (this.totalTasks <= 0) { - const callbacks = this.readyStateCallbacks; + this.immediate = this.window.requestAnimationFrame(() => { + this.immediate = null; + + if (this.totalTasks <= 0) { + const callbacks = this.readyStateCallbacks; - this.readyStateCallbacks = []; - this.isComplete = true; + this.readyStateCallbacks = []; + this.isComplete = true; - for (const callback of callbacks) { - callback(); + for (const callback of callbacks) { + callback(); + } } - } + }); } } diff --git a/packages/happy-dom/src/nodes/element/Element.ts b/packages/happy-dom/src/nodes/element/Element.ts index 3a9932faa..5c70559aa 100644 --- a/packages/happy-dom/src/nodes/element/Element.ts +++ b/packages/happy-dom/src/nodes/element/Element.ts @@ -9,7 +9,6 @@ import XMLSerializer from '../../xml-serializer/XMLSerializer.js'; import ChildNodeUtility from '../child-node/ChildNodeUtility.js'; import ParentNodeUtility from '../parent-node/ParentNodeUtility.js'; import NonDocumentChildNodeUtility from '../child-node/NonDocumentChildNodeUtility.js'; -import DOMException from '../../exception/DOMException.js'; import HTMLCollection from './HTMLCollection.js'; import Text from '../text/Text.js'; import DOMRectList from './DOMRectList.js'; @@ -19,7 +18,7 @@ import Event from '../../event/Event.js'; import EventPhaseEnum from '../../event/EventPhaseEnum.js'; import DocumentFragment from '../document-fragment/DocumentFragment.js'; import WindowErrorUtility from '../../window/WindowErrorUtility.js'; -import WindowBrowserSettingsReader from '../../window/WindowBrowserSettingsReader.js'; +import WindowBrowserContext from '../../window/WindowBrowserContext.js'; import BrowserErrorCaptureEnum from '../../browser/enums/BrowserErrorCaptureEnum.js'; import NodeTypeEnum from '../node/NodeTypeEnum.js'; import IHTMLElementTagNameMap from '../../config/IHTMLElementTagNameMap.js'; @@ -93,9 +92,6 @@ export default class Element public [PropertySymbol.classList]: DOMTokenList | null = null; public [PropertySymbol.isValue]: string | null = null; public [PropertySymbol.nodeType] = NodeTypeEnum.elementNode; - public [PropertySymbol.tagName]: string | null = this.constructor[PropertySymbol.tagName] || null; - public [PropertySymbol.localName]: string | null = - this.constructor[PropertySymbol.localName] || null; public [PropertySymbol.prefix]: string | null = null; public [PropertySymbol.shadowRoot]: ShadowRoot | null = null; public [PropertySymbol.scrollHeight] = 0; @@ -104,10 +100,33 @@ export default class Element public [PropertySymbol.scrollLeft] = 0; public [PropertySymbol.attributes] = new NamedNodeMap(this); public [PropertySymbol.attributesProxy]: NamedNodeMap | null = null; - public [PropertySymbol.namespaceURI]: string | null = - this.constructor[PropertySymbol.namespaceURI] || null; public [PropertySymbol.children]: HTMLCollection | null = null; public [PropertySymbol.computedStyle]: CSSStyleDeclaration | null = null; + public declare [PropertySymbol.tagName]: string | null; + public declare [PropertySymbol.localName]: string | null; + public declare [PropertySymbol.namespaceURI]: string | null; + + /** + * Constructor. + */ + constructor() { + super(); + + // CustomElementRegistry will populate the properties upon calling "CustomElementRegistry.define()". + // Elements that can be constructed with the "new" keyword (without using "Document.createElement()") will also populate the properties. + + if (!this[PropertySymbol.tagName]) { + this[PropertySymbol.tagName] = null; + } + + if (!this[PropertySymbol.localName]) { + this[PropertySymbol.localName] = null; + } + + if (!this[PropertySymbol.namespaceURI]) { + this[PropertySymbol.namespaceURI] = null; + } + } /** * Returns tag name. @@ -825,33 +844,35 @@ export default class Element serializable?: boolean; slotAssignment?: 'named' | 'manual'; }): ShadowRoot { + const window = this[PropertySymbol.window]; + if (!init) { - throw new TypeError( + throw new window.TypeError( "Failed to execute 'attachShadow' on 'Element': 1 argument required, but only 0 present." ); } if (!init.mode) { - throw new TypeError( + throw new window.TypeError( "Failed to execute 'attachShadow' on 'Element': Failed to read the 'mode' property from 'ShadowRootInit': Required member is undefined." ); } if (init.mode !== 'open' && init.mode !== 'closed') { - throw new TypeError( + throw new window.TypeError( `Failed to execute 'attachShadow' on 'Element': Failed to read the 'mode' property from 'ShadowRootInit': The provided value '${init.mode}' is not a valid enum value of type ShadowRootMode.` ); } if (this[PropertySymbol.shadowRoot]) { - throw new DOMException( + throw new window.DOMException( "Failed to execute 'attachShadow' on 'Element': Shadow root cannot be created on a host which already hosts a shadow tree." ); } - const shadowRoot = NodeFactory.createNode( + const shadowRoot = NodeFactory.createNode( this[PropertySymbol.ownerDocument], - this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow].ShadowRoot + this[PropertySymbol.window].ShadowRoot ); this[PropertySymbol.shadowRoot] = shadowRoot; @@ -1148,7 +1169,7 @@ export default class Element */ public removeAttributeNode(attribute: Attr): Attr | null { if (attribute[PropertySymbol.ownerElement] !== this) { - throw new DOMException( + throw new this[PropertySymbol.window].DOMException( "Failed to execute 'removeAttributeNode' on 'Element': The node provided is owned by another element." ); } @@ -1165,7 +1186,7 @@ export default class Element public scroll(x: { top?: number; left?: number; behavior?: string } | number, y?: number): void { if (typeof x === 'object') { if (x.behavior === 'smooth') { - this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow].setTimeout(() => { + this[PropertySymbol.window].setTimeout(() => { if (x.top !== undefined) { (this.scrollTop) = x.top; } @@ -1222,33 +1243,27 @@ export default class Element */ public override dispatchEvent(event: Event): boolean { const returnValue = super.dispatchEvent(event); - const browserSettings = WindowBrowserSettingsReader.getSettings( - this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow] - ); + const browserSettings = new WindowBrowserContext(this[PropertySymbol.window]).getSettings(); if ( browserSettings && !browserSettings.disableJavaScriptEvaluation && - (event.eventPhase === EventPhaseEnum.atTarget || - event.eventPhase === EventPhaseEnum.bubbling) && + event.eventPhase === EventPhaseEnum.none && !event[PropertySymbol.immediatePropagationStopped] ) { const attribute = this.getAttribute('on' + event.type); if (attribute && !event[PropertySymbol.immediatePropagationStopped]) { - const code = `//# sourceURL=${ - this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow].location.href - }\n${attribute}`; + const code = `//# sourceURL=${this[PropertySymbol.window].location.href}\n${attribute}`; if ( browserSettings.disableErrorCapturing || browserSettings.errorCapture !== BrowserErrorCaptureEnum.tryAndCatch ) { - this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow].eval(code); + this[PropertySymbol.window].eval(code); } else { - WindowErrorUtility.captureError( - this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow], - () => this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow].eval(code) + WindowErrorUtility.captureError(this[PropertySymbol.window], () => + this[PropertySymbol.window].eval(code) ); } } @@ -1460,7 +1475,7 @@ export default class Element } const document = this[PropertySymbol.ownerDocument]; - const window = document[PropertySymbol.ownerWindow]; + const window = this[PropertySymbol.window]; // We should not add the identifier when inside a shadow root if (this[PropertySymbol.rootNode] && this[PropertySymbol.rootNode] !== document) { @@ -1506,7 +1521,7 @@ export default class Element } const document = this[PropertySymbol.ownerDocument]; - const window = document[PropertySymbol.ownerWindow]; + const window = this[PropertySymbol.window]; // We should not add the identifier when inside a shadow root if (this[PropertySymbol.rootNode] && this[PropertySymbol.rootNode] !== document) { diff --git a/packages/happy-dom/src/nodes/element/NamedNodeMap.ts b/packages/happy-dom/src/nodes/element/NamedNodeMap.ts index bce3574cf..ba6f29f49 100644 --- a/packages/happy-dom/src/nodes/element/NamedNodeMap.ts +++ b/packages/happy-dom/src/nodes/element/NamedNodeMap.ts @@ -4,6 +4,7 @@ import DOMException from '../../exception/DOMException.js'; import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; import Element from './Element.js'; import NamespaceURI from '../../config/NamespaceURI.js'; +import ClassMethodBinder from '../../ClassMethodBinder.js'; /** * Named Node Map. @@ -28,6 +29,9 @@ export default class NamedNodeMap { */ constructor(ownerElement: Element) { this[PropertySymbol.ownerElement] = ownerElement; + ClassMethodBinder.bindMethods(this, [NamedNodeMap], { + bindSymbols: true + }); } /** diff --git a/packages/happy-dom/src/nodes/element/NamedNodeMapProxyFactory.ts b/packages/happy-dom/src/nodes/element/NamedNodeMapProxyFactory.ts index 788c830be..1719c7ccc 100644 --- a/packages/happy-dom/src/nodes/element/NamedNodeMapProxyFactory.ts +++ b/packages/happy-dom/src/nodes/element/NamedNodeMapProxyFactory.ts @@ -1,6 +1,5 @@ /* eslint-disable filenames/match-exported */ -import ClassMethodBinder from '../../ClassMethodBinder.js'; import * as PropertySymbol from '../../PropertySymbol.js'; import NamedNodeMap from './NamedNodeMap.js'; @@ -19,11 +18,6 @@ export default class NamedNodeMapProxyFactory { const namedItems = namedNodeMap[PropertySymbol.namedItems]; const namespaceItems = namedNodeMap[PropertySymbol.namespaceItems]; - ClassMethodBinder.bindMethods(namedNodeMap, [NamedNodeMap], { - bindSymbols: true, - forwardToPrototype: true - }); - return new Proxy(namedNodeMap, { get: (target, property) => { if (property === 'length') { diff --git a/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElement.ts b/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElement.ts index 994acb3a6..04559648a 100644 --- a/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElement.ts +++ b/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElement.ts @@ -376,10 +376,10 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLHyper const returnValue = super.dispatchEvent(event); if ( + !event[PropertySymbol.defaultPrevented] && event.type === 'click' && event instanceof MouseEvent && - event.eventPhase === EventPhaseEnum.none && - !event.defaultPrevented + event.eventPhase === EventPhaseEnum.none ) { const href = this.href; @@ -394,13 +394,9 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLHyper features.push('noopener'); } - this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow].open( - href, - this.target || '_self', - features.join(',') - ); + this[PropertySymbol.window].open(href, this.target || '_self', features.join(',')); - if (this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow].closed) { + if (this[PropertySymbol.window].closed) { event.stopImmediatePropagation(); } } diff --git a/packages/happy-dom/src/nodes/html-area-element/HTMLAreaElement.ts b/packages/happy-dom/src/nodes/html-area-element/HTMLAreaElement.ts index 33a2ec2ce..34c80ad11 100644 --- a/packages/happy-dom/src/nodes/html-area-element/HTMLAreaElement.ts +++ b/packages/happy-dom/src/nodes/html-area-element/HTMLAreaElement.ts @@ -3,9 +3,9 @@ import * as PropertySymbol from '../../PropertySymbol.js'; import DOMTokenList from '../element/DOMTokenList.js'; import HTMLHyperlinkElementUtility from '../html-hyperlink-element/HTMLHyperlinkElementUtility.js'; import IHTMLHyperlinkElement from '../html-hyperlink-element/IHTMLHyperlinkElement.js'; -import PointerEvent from '../../event/events/PointerEvent.js'; import Event from '../../event/Event.js'; import EventPhaseEnum from '../../event/EventPhaseEnum.js'; +import MouseEvent from '../../event/events/MouseEvent.js'; /** * HTMLAreaElement @@ -375,19 +375,15 @@ export default class HTMLAreaElement extends HTMLElement implements IHTMLHyperli const returnValue = super.dispatchEvent(event); if ( - event.type === 'click' && - event instanceof PointerEvent && - (event.eventPhase === EventPhaseEnum.atTarget || - event.eventPhase === EventPhaseEnum.bubbling) && - !event.defaultPrevented + !event[PropertySymbol.defaultPrevented] && + event[PropertySymbol.type] === 'click' && + event[PropertySymbol.eventPhase] === EventPhaseEnum.none && + event instanceof MouseEvent ) { const href = this.href; if (href) { - this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow].open( - href, - this.target || '_self' - ); - if (this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow].closed) { + this[PropertySymbol.window].open(href, this.target || '_self'); + if (this[PropertySymbol.window].closed) { event.stopImmediatePropagation(); } } diff --git a/packages/happy-dom/src/nodes/html-audio-element/Audio.ts b/packages/happy-dom/src/nodes/html-audio-element/Audio.ts index 3fca58915..87f8c4887 100644 --- a/packages/happy-dom/src/nodes/html-audio-element/Audio.ts +++ b/packages/happy-dom/src/nodes/html-audio-element/Audio.ts @@ -1,4 +1,6 @@ +import NamespaceURI from '../../config/NamespaceURI.js'; import HTMLAudioElement from './HTMLAudioElement.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; /** * Image as constructor. @@ -7,6 +9,10 @@ import HTMLAudioElement from './HTMLAudioElement.js'; * https://developer.mozilla.org/en-US/docs/Web/API/HTMLAudioElement/Audio. */ export default class Audio extends HTMLAudioElement { + public [PropertySymbol.tagName] = 'AUDIO'; + public [PropertySymbol.localName] = 'audio'; + public [PropertySymbol.namespaceURI] = NamespaceURI.html; + /** * Constructor. * diff --git a/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElement.ts b/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElement.ts index 87443f8b9..1aa264253 100644 --- a/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElement.ts +++ b/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElement.ts @@ -106,16 +106,20 @@ export default class HTMLButtonElement extends HTMLElement { * @returns Type */ public get type(): string { - return this.#sanitizeType(this.getAttribute('type')); + const type = this.getAttribute('type'); + if (type === null || !BUTTON_TYPES.includes(type)) { + return 'submit'; + } + return type; } /** * Sets type * - * @param v Type + * @param value Type */ - public set type(v: string) { - this.setAttribute('type', this.#sanitizeType(v)); + public set type(value: string) { + this.setAttribute('type', value); } /** @@ -267,7 +271,7 @@ export default class HTMLButtonElement extends HTMLElement { */ public set popoverTargetElement(popoverTargetElement: HTMLElement | null) { if (popoverTargetElement !== null && !(popoverTargetElement instanceof HTMLElement)) { - throw new TypeError( + throw new this[PropertySymbol.window].TypeError( `Failed to set the 'popoverTargetElement' property on 'HTMLInputElement': Failed to convert value to 'Element'.` ); } @@ -347,44 +351,24 @@ export default class HTMLButtonElement extends HTMLElement { const returnValue = super.dispatchEvent(event); if ( + !event[PropertySymbol.defaultPrevented] && event.type === 'click' && - event instanceof MouseEvent && - (event.eventPhase === EventPhaseEnum.atTarget || - event.eventPhase === EventPhaseEnum.bubbling) && - this[PropertySymbol.isConnected] + event.eventPhase === EventPhaseEnum.none && + event instanceof MouseEvent ) { - const form = this.form; - if (!form) { - return returnValue; - } - switch (this.type) { - case 'submit': - form.requestSubmit(this); - break; - case 'reset': - form.reset(); - break; + const type = this.type; + if (type === 'submit' || type === 'reset') { + const form = this.form; + if (form) { + if (type === 'submit' && this[PropertySymbol.isConnected]) { + form.requestSubmit(this); + } else if (type === 'reset') { + form.reset(); + } + } } } return returnValue; } - - /** - * Sanitizes type. - * - * TODO: We can improve performance a bit if we make the types as a constant. - * - * @param type Type. - * @returns Type sanitized. - */ - #sanitizeType(type: string): string { - type = (type && type.toLowerCase()) || 'submit'; - - if (!BUTTON_TYPES.includes(type)) { - type = 'submit'; - } - - return type; - } } diff --git a/packages/happy-dom/src/nodes/html-canvas-element/HTMLCanvasElement.ts b/packages/happy-dom/src/nodes/html-canvas-element/HTMLCanvasElement.ts index 21aa2a8ec..3a23abd9b 100644 --- a/packages/happy-dom/src/nodes/html-canvas-element/HTMLCanvasElement.ts +++ b/packages/happy-dom/src/nodes/html-canvas-element/HTMLCanvasElement.ts @@ -1,5 +1,4 @@ import HTMLElement from '../html-element/HTMLElement.js'; -import CanvasCaptureMediaStreamTrack from './CanvasCaptureMediaStreamTrack.js'; import * as PropertySymbol from '../../PropertySymbol.js'; import Blob from '../../file/Blob.js'; import OffscreenCanvas from './OffscreenCanvas.js'; @@ -66,8 +65,11 @@ export default class HTMLCanvasElement extends HTMLElement { * @returns Capture stream. */ public captureStream(frameRate?: number): MediaStream { - const stream = new MediaStream(); - const track = new CanvasCaptureMediaStreamTrack(PropertySymbol.illegalConstructor, this); + const stream = new this[PropertySymbol.window].MediaStream(); + const track = new this[PropertySymbol.window].CanvasCaptureMediaStreamTrack( + PropertySymbol.illegalConstructor, + this + ); track[PropertySymbol.kind] = 'video'; track[PropertySymbol.capabilities].deviceId = DEVICE_ID; diff --git a/packages/happy-dom/src/nodes/html-document/HTMLDocument.ts b/packages/happy-dom/src/nodes/html-document/HTMLDocument.ts index d142243cc..4a5366dc7 100644 --- a/packages/happy-dom/src/nodes/html-document/HTMLDocument.ts +++ b/packages/happy-dom/src/nodes/html-document/HTMLDocument.ts @@ -1,5 +1,3 @@ -import IBrowserFrame from '../../browser/types/IBrowserFrame.js'; -import BrowserWindow from '../../window/BrowserWindow.js'; import Document from '../document/Document.js'; import * as PropertySymbol from '../../PropertySymbol.js'; @@ -9,13 +7,9 @@ import * as PropertySymbol from '../../PropertySymbol.js'; export default class HTMLDocument extends Document { /** * Constructor. - * - * @param injected Injected properties. - * @param injected.browserFrame Browser frame. - * @param injected.window Window. */ - constructor(injected: { browserFrame: IBrowserFrame; window: BrowserWindow }) { - super(injected); + constructor() { + super(); // Default document elements const doctype = this[PropertySymbol.implementation].createDocumentType('html', '', ''); diff --git a/packages/happy-dom/src/nodes/html-element/HTMLElement.ts b/packages/happy-dom/src/nodes/html-element/HTMLElement.ts index 286988a15..7603452ec 100644 --- a/packages/happy-dom/src/nodes/html-element/HTMLElement.ts +++ b/packages/happy-dom/src/nodes/html-element/HTMLElement.ts @@ -3,10 +3,10 @@ import * as PropertySymbol from '../../PropertySymbol.js'; import CSSStyleDeclaration from '../../css/declaration/CSSStyleDeclaration.js'; import PointerEvent from '../../event/events/PointerEvent.js'; import NodeTypeEnum from '../node/NodeTypeEnum.js'; -import DOMException from '../../exception/DOMException.js'; import Event from '../../event/Event.js'; import HTMLElementUtility from './HTMLElementUtility.js'; import DOMStringMap from '../element/DOMStringMap.js'; +import WindowBrowserContext from '../../window/WindowBrowserContext.js'; /** * HTML Element. @@ -219,10 +219,7 @@ export default class HTMLElement extends Element { for (const childNode of this[PropertySymbol.nodeArray]) { if (childNode[PropertySymbol.nodeType] === NodeTypeEnum.elementNode) { const childElement = childNode; - const computedStyle = - this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow].getComputedStyle( - childElement - ); + const computedStyle = this[PropertySymbol.window].getComputedStyle(childElement); if ( childElement[PropertySymbol.tagName] !== 'SCRIPT' && @@ -303,7 +300,7 @@ export default class HTMLElement extends Element { */ public set outerText(text: string) { if (!this[PropertySymbol.parentNode]) { - throw new DOMException( + throw new this[PropertySymbol.window].DOMException( "Failed to set the 'outerHTML' property on 'Element': This element has no parent node." ); } @@ -492,14 +489,13 @@ export default class HTMLElement extends Element { * Triggers a click event. */ public click(): void { - const event = new PointerEvent('click', { - bubbles: true, - composed: true, - cancelable: true - }); - event[PropertySymbol.target] = this; - event[PropertySymbol.currentTarget] = this; - this.dispatchEvent(event); + this.dispatchEvent( + new PointerEvent('click', { + bubbles: true, + composed: true, + cancelable: true + }) + ); } /** @@ -534,102 +530,21 @@ export default class HTMLElement extends Element { * @see https://html.spec.whatwg.org/multipage/dom.html#htmlelement */ public override [PropertySymbol.connectedToNode](): void { + const window = this[PropertySymbol.window]; const localName = this[PropertySymbol.localName]; + const allCallbacks = window.customElements[PropertySymbol.callbacks]; // This element can potentially be a custom element that has not been defined yet // Therefore we need to register a callback for when it is defined in CustomElementRegistry and replace it with the registered element (see #404) - if ( - this.constructor === HTMLElement && - localName.includes('-') && - this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow].customElements[ - PropertySymbol.callbacks - ] - ) { - const callbacks = - this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow].customElements[ - PropertySymbol.callbacks - ]; - + if (this.constructor === window.HTMLElement && localName.includes('-') && allCallbacks) { if (!this.#customElementDefineCallback) { - const callback = (): void => { - if (this[PropertySymbol.parentNode]) { - const newElement = ( - this[PropertySymbol.ownerDocument].createElement(localName) - ); - const newCache = newElement[PropertySymbol.cache]; - newElement[PropertySymbol.nodeArray] = this[PropertySymbol.nodeArray]; - newElement[PropertySymbol.elementArray] = this[PropertySymbol.elementArray]; - newElement[PropertySymbol.childNodes] = null; - newElement[PropertySymbol.children] = null; - newElement[PropertySymbol.isConnected] = this[PropertySymbol.isConnected]; - - newElement[PropertySymbol.rootNode] = this[PropertySymbol.rootNode]; - newElement[PropertySymbol.formNode] = this[PropertySymbol.formNode]; - newElement[PropertySymbol.parentNode] = this[PropertySymbol.parentNode]; - newElement[PropertySymbol.selectNode] = this[PropertySymbol.selectNode]; - newElement[PropertySymbol.textAreaNode] = this[PropertySymbol.textAreaNode]; - newElement[PropertySymbol.mutationListeners] = this[PropertySymbol.mutationListeners]; - newElement[PropertySymbol.isValue] = this[PropertySymbol.isValue]; - newElement[PropertySymbol.cache] = this[PropertySymbol.cache]; - newElement[PropertySymbol.affectsCache] = this[PropertySymbol.affectsCache]; - newElement[PropertySymbol.attributes][PropertySymbol.namedItems] = - this[PropertySymbol.attributes][PropertySymbol.namedItems]; - newElement[PropertySymbol.attributes][PropertySymbol.namespaceItems] = - this[PropertySymbol.attributes][PropertySymbol.namespaceItems]; - - for (const attr of newElement[PropertySymbol.attributes][ - PropertySymbol.namedItems - ].values()) { - attr[PropertySymbol.ownerElement] = newElement; - } - - this[PropertySymbol.nodeArray] = []; - this[PropertySymbol.elementArray] = []; - this[PropertySymbol.childNodes] = null; - this[PropertySymbol.children] = null; - - this[PropertySymbol.rootNode] = null; - this[PropertySymbol.formNode] = null; - this[PropertySymbol.selectNode] = null; - this[PropertySymbol.textAreaNode] = null; - this[PropertySymbol.mutationListeners] = []; - this[PropertySymbol.isValue] = null; - this[PropertySymbol.cache] = newCache; - this[PropertySymbol.affectsCache] = []; - this[PropertySymbol.attributes][PropertySymbol.namedItems] = new Map(); - this[PropertySymbol.attributes][PropertySymbol.namespaceItems] = new Map(); - - const parentChildNodes = this[PropertySymbol.parentNode][PropertySymbol.nodeArray]; - const parentChildElements = - this[PropertySymbol.parentNode][PropertySymbol.elementArray]; - parentChildNodes[parentChildNodes.indexOf(this)] = newElement; - parentChildElements[parentChildElements.indexOf(this)] = newElement; - - if (newElement[PropertySymbol.isConnected] && newElement.connectedCallback) { - const result = >newElement.connectedCallback(); - /** - * It is common to import dependencies in the connectedCallback() method of web components. - * As Happy DOM doesn't have support for dynamic imports yet, this is a temporary solution to wait for imports in connectedCallback(). - * - * @see https://github.com/capricorn86/happy-dom/issues/1442 - */ - if (result instanceof Promise) { - const asyncTaskManager = - this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow][ - PropertySymbol.asyncTaskManager - ]; - const taskID = asyncTaskManager.startTask(); - result - .then(() => asyncTaskManager.endTask(taskID)) - .catch(() => asyncTaskManager.endTask(taskID)); - } - } - - this[PropertySymbol.disconnectedFromDocument](); - } - }; - callbacks[localName] = callbacks[localName] || []; - callbacks[localName].push(callback); + const callback = this.#onCustomElementConnected.bind(this); + const callbacks = allCallbacks.get(localName); + if (callbacks) { + callbacks.push(callback); + } else { + allCallbacks.set(localName, [callback]); + } this.#customElementDefineCallback = callback; } } @@ -641,29 +556,22 @@ export default class HTMLElement extends Element { * @override */ public override [PropertySymbol.disconnectedFromNode](): void { + const window = this[PropertySymbol.window]; const localName = this[PropertySymbol.localName]; + const allCallbacks = window.customElements[PropertySymbol.callbacks]; // This element can potentially be a custom element that has not been defined yet // Therefore we need to register a callback for when it is defined in CustomElementRegistry and replace it with the registered element (see #404) - if ( - this.constructor === HTMLElement && - localName.includes('-') && - this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow].customElements[ - PropertySymbol.callbacks - ] - ) { - const callbacks = - this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow].customElements[ - PropertySymbol.callbacks - ]; - - if (callbacks[localName] && this.#customElementDefineCallback) { - const index = callbacks[localName].indexOf(this.#customElementDefineCallback); + if (this.constructor === window.HTMLElement && localName.includes('-') && allCallbacks) { + const callbacks = allCallbacks.get(localName); + + if (callbacks && this.#customElementDefineCallback) { + const index = callbacks.indexOf(this.#customElementDefineCallback); if (index !== -1) { - callbacks[localName].splice(index, 1); + callbacks.splice(index, 1); } - if (!callbacks[localName].length) { - delete callbacks[localName]; + if (!callbacks.length) { + allCallbacks.delete(localName); } this.#customElementDefineCallback = null; } @@ -671,4 +579,85 @@ export default class HTMLElement extends Element { super[PropertySymbol.disconnectedFromNode](); } + + /** + * Triggered when a custom element is connected to the DOM. + */ + #onCustomElementConnected(): void { + if (!this[PropertySymbol.parentNode]) { + return; + } + + const localName = this[PropertySymbol.localName]; + const newElement = this[PropertySymbol.ownerDocument].createElement(localName); + const newCache = newElement[PropertySymbol.cache]; + + newElement[PropertySymbol.nodeArray] = this[PropertySymbol.nodeArray]; + newElement[PropertySymbol.elementArray] = this[PropertySymbol.elementArray]; + newElement[PropertySymbol.childNodes] = null; + newElement[PropertySymbol.children] = null; + newElement[PropertySymbol.isConnected] = this[PropertySymbol.isConnected]; + + newElement[PropertySymbol.rootNode] = this[PropertySymbol.rootNode]; + newElement[PropertySymbol.formNode] = this[PropertySymbol.formNode]; + newElement[PropertySymbol.parentNode] = this[PropertySymbol.parentNode]; + newElement[PropertySymbol.selectNode] = this[PropertySymbol.selectNode]; + newElement[PropertySymbol.textAreaNode] = this[PropertySymbol.textAreaNode]; + newElement[PropertySymbol.mutationListeners] = this[PropertySymbol.mutationListeners]; + newElement[PropertySymbol.isValue] = this[PropertySymbol.isValue]; + newElement[PropertySymbol.cache] = this[PropertySymbol.cache]; + newElement[PropertySymbol.affectsCache] = this[PropertySymbol.affectsCache]; + newElement[PropertySymbol.attributes][PropertySymbol.namedItems] = + this[PropertySymbol.attributes][PropertySymbol.namedItems]; + newElement[PropertySymbol.attributes][PropertySymbol.namespaceItems] = + this[PropertySymbol.attributes][PropertySymbol.namespaceItems]; + + for (const attr of newElement[PropertySymbol.attributes][PropertySymbol.namedItems].values()) { + attr[PropertySymbol.ownerElement] = newElement; + } + + this[PropertySymbol.nodeArray] = []; + this[PropertySymbol.elementArray] = []; + this[PropertySymbol.childNodes] = null; + this[PropertySymbol.children] = null; + + this[PropertySymbol.rootNode] = null; + this[PropertySymbol.formNode] = null; + this[PropertySymbol.selectNode] = null; + this[PropertySymbol.textAreaNode] = null; + this[PropertySymbol.mutationListeners] = []; + this[PropertySymbol.isValue] = null; + this[PropertySymbol.cache] = newCache; + this[PropertySymbol.affectsCache] = []; + this[PropertySymbol.attributes][PropertySymbol.namedItems] = new Map(); + this[PropertySymbol.attributes][PropertySymbol.namespaceItems] = new Map(); + + const parentChildNodes = this[PropertySymbol.parentNode][PropertySymbol.nodeArray]; + const parentChildElements = this[PropertySymbol.parentNode][PropertySymbol.elementArray]; + parentChildNodes[parentChildNodes.indexOf(this)] = newElement; + parentChildElements[parentChildElements.indexOf(this)] = newElement; + + if (newElement[PropertySymbol.isConnected] && newElement.connectedCallback) { + const result = >newElement.connectedCallback(); + /** + * It is common to import dependencies in the connectedCallback() method of web components. + * As Happy DOM doesn't have support for dynamic imports yet, this is a temporary solution to wait for imports in connectedCallback(). + * + * @see https://github.com/capricorn86/happy-dom/issues/1442 + */ + if (result instanceof Promise) { + const asyncTaskManager = new WindowBrowserContext( + this[PropertySymbol.window] + ).getAsyncTaskManager(); + if (asyncTaskManager) { + const taskID = asyncTaskManager.startTask(); + result + .then(() => asyncTaskManager.endTask(taskID)) + .catch(() => asyncTaskManager.endTask(taskID)); + } + } + } + + this[PropertySymbol.disconnectedFromDocument](); + } } diff --git a/packages/happy-dom/src/nodes/html-form-element/HTMLFormElement.ts b/packages/happy-dom/src/nodes/html-form-element/HTMLFormElement.ts index 0e23882ea..f48743b57 100644 --- a/packages/happy-dom/src/nodes/html-form-element/HTMLFormElement.ts +++ b/packages/happy-dom/src/nodes/html-form-element/HTMLFormElement.ts @@ -17,6 +17,7 @@ import Element from '../element/Element.js'; import EventTarget from '../../event/EventTarget.js'; import Node from '../node/Node.js'; import ClassMethodBinder from '../../ClassMethodBinder.js'; +import WindowBrowserContext from '../../window/WindowBrowserContext.js'; /** * HTML Form Element. @@ -37,19 +38,12 @@ export default class HTMLFormElement extends HTMLElement { public onreset: (event: Event) => void | null = null; public onsubmit: (event: Event) => void | null = null; - // Private properties - #browserFrame: IBrowserFrame; - /** * Constructor. - * - * @param browserFrame Browser frame. */ - constructor(browserFrame) { + constructor() { super(); - this.#browserFrame = browserFrame; - ClassMethodBinder.bindMethods( this, [EventTarget, Node, Element, HTMLElement, HTMLFormElement], @@ -576,7 +570,11 @@ export default class HTMLFormElement extends HTMLElement { const action = submitter?.hasAttribute('formaction') ? submitter?.formAction || this.action : this.action; - const browserFrame = this.#browserFrame; + const browserFrame = new WindowBrowserContext(this[PropertySymbol.window]).getBrowserFrame(); + + if (!browserFrame) { + return; + } if (!action) { // The URL is invalid when the action is empty. diff --git a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts index fc33922e7..7a6225a9f 100644 --- a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts +++ b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts @@ -10,9 +10,9 @@ import Attr from '../attr/Attr.js'; import BrowserFrameFactory from '../../browser/utilities/BrowserFrameFactory.js'; import BrowserFrameURL from '../../browser/utilities/BrowserFrameURL.js'; import WindowErrorUtility from '../../window/WindowErrorUtility.js'; -import DOMException from '../../exception/DOMException.js'; import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; import IRequestReferrerPolicy from '../../fetch/types/IRequestReferrerPolicy.js'; +import WindowBrowserContext from '../../window/WindowBrowserContext.js'; const SANDBOX_FLAGS = [ 'allow-downloads', @@ -51,21 +51,9 @@ export default class HTMLIFrameElement extends HTMLElement { #contentWindowContainer: { window: BrowserWindow | CrossOriginBrowserWindow | null } = { window: null }; - #browserFrame: IBrowserFrame; - #browserChildFrame: IBrowserFrame; + #iframe: IBrowserFrame; #loadedSrcdoc: string | null = null; - /** - * Constructor. - * - * @param browserFrame Browser frame. - */ - constructor(browserFrame) { - super(); - - this.#browserFrame = browserFrame; - } - /** * Returns source. * @@ -306,7 +294,7 @@ export default class HTMLIFrameElement extends HTMLElement { * @param vconsole */ #validateSandboxFlags(): void { - const window = this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow]; + const window = this[PropertySymbol.window]; const invalidFlags: string[] = []; for (const token of this.sandbox) { @@ -338,7 +326,12 @@ export default class HTMLIFrameElement extends HTMLElement { } const srcdoc = this.getAttribute('srcdoc'); - const window = this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow]; + const window = this[PropertySymbol.window]; + const browserFrame = new WindowBrowserContext(window).getBrowserFrame(); + + if (!browserFrame) { + return; + } if (srcdoc !== null) { if (this.#loadedSrcdoc === srcdoc) { @@ -347,20 +340,20 @@ export default class HTMLIFrameElement extends HTMLElement { this.#unloadPage(); - this.#browserChildFrame = BrowserFrameFactory.createChildFrame(this.#browserFrame); - this.#browserChildFrame.url = 'about:srcdoc'; + this.#iframe = BrowserFrameFactory.createChildFrame(browserFrame); + this.#iframe.url = 'about:srcdoc'; - this.#contentWindowContainer.window = this.#browserChildFrame.window; + this.#contentWindowContainer.window = this.#iframe.window; - this.#browserChildFrame.window[PropertySymbol.top] = this.#browserFrame.window.top; - this.#browserChildFrame.window[PropertySymbol.parent] = this.#browserFrame.window; + this.#iframe.window[PropertySymbol.top] = browserFrame.window.top; + this.#iframe.window[PropertySymbol.parent] = browserFrame.window; - this.#browserChildFrame.window.document.open(); - this.#browserChildFrame.window.document.write(srcdoc); + this.#iframe.window.document.open(); + this.#iframe.window.document.write(srcdoc); this.#loadedSrcdoc = srcdoc; - this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow].requestAnimationFrame(() => + this[PropertySymbol.window].requestAnimationFrame(() => this.dispatchEvent(new Event('load')) ); return; @@ -370,20 +363,17 @@ export default class HTMLIFrameElement extends HTMLElement { this.#unloadPage(); } - const originURL = this.#browserFrame.window.location; - const targetURL = BrowserFrameURL.getRelativeURL(this.#browserFrame, this.src); + const originURL = browserFrame.window.location; + const targetURL = BrowserFrameURL.getRelativeURL(browserFrame, this.src); - if ( - this.#browserChildFrame && - this.#browserChildFrame.window.location.href === targetURL.href - ) { + if (this.#iframe && this.#iframe.window.location.href === targetURL.href) { return; } - if (this.#browserFrame.page.context.browser.settings.disableIframePageLoading) { + if (browserFrame.page.context.browser.settings.disableIframePageLoading) { WindowErrorUtility.dispatchError( this, - new DOMException( + new window.DOMException( `Failed to load iframe page "${targetURL.href}". Iframe page loading is disabled.`, DOMExceptionNameEnum.notSupportedError ) @@ -395,13 +385,12 @@ export default class HTMLIFrameElement extends HTMLElement { const isSameOrigin = originURL.origin === targetURL.origin || targetURL.origin === 'null'; const parentWindow = isSameOrigin ? window : new CrossOriginBrowserWindow(window); - this.#browserChildFrame = - this.#browserChildFrame ?? BrowserFrameFactory.createChildFrame(this.#browserFrame); + this.#iframe = this.#iframe ?? BrowserFrameFactory.createChildFrame(browserFrame); - this.#browserChildFrame.window[PropertySymbol.top] = parentWindow; - this.#browserChildFrame.window[PropertySymbol.parent] = parentWindow; + this.#iframe.window[PropertySymbol.top] = parentWindow; + this.#iframe.window[PropertySymbol.parent] = parentWindow; - this.#browserChildFrame + this.#iframe .goto(targetURL.href, { referrer: originURL.origin, referrerPolicy: this.referrerPolicy @@ -410,17 +399,17 @@ export default class HTMLIFrameElement extends HTMLElement { .catch((error) => WindowErrorUtility.dispatchError(this, error)); this.#contentWindowContainer.window = isSameOrigin - ? this.#browserChildFrame.window - : new CrossOriginBrowserWindow(this.#browserChildFrame.window, window); + ? this.#iframe.window + : new CrossOriginBrowserWindow(this.#iframe.window, window); } /** * Unloads an iframe page. */ #unloadPage(): void { - if (this.#browserChildFrame) { - BrowserFrameFactory.destroyFrame(this.#browserChildFrame); - this.#browserChildFrame = null; + if (this.#iframe) { + BrowserFrameFactory.destroyFrame(this.#iframe); + this.#iframe = null; } this.#contentWindowContainer.window = null; this.#loadedSrcdoc = null; diff --git a/packages/happy-dom/src/nodes/html-image-element/Image.ts b/packages/happy-dom/src/nodes/html-image-element/Image.ts index f8e028129..4be1f95a7 100644 --- a/packages/happy-dom/src/nodes/html-image-element/Image.ts +++ b/packages/happy-dom/src/nodes/html-image-element/Image.ts @@ -1,4 +1,6 @@ import HTMLImageElement from './HTMLImageElement.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; +import NamespaceURI from '../../config/NamespaceURI.js'; /** * Image as constructor. @@ -7,6 +9,10 @@ import HTMLImageElement from './HTMLImageElement.js'; * https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/Image. */ export default class Image extends HTMLImageElement { + public [PropertySymbol.tagName] = 'IMG'; + public [PropertySymbol.localName] = 'img'; + public [PropertySymbol.namespaceURI] = NamespaceURI.html; + /** * Constructor. * diff --git a/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts b/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts index 0627ed34c..0fb6724cb 100644 --- a/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts +++ b/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts @@ -1,7 +1,6 @@ import HTMLElement from '../html-element/HTMLElement.js'; import * as PropertySymbol from '../../PropertySymbol.js'; import ValidityState from '../../validity-state/ValidityState.js'; -import DOMException from '../../exception/DOMException.js'; import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; import Event from '../../event/Event.js'; import HTMLInputElementValueSanitizer from './HTMLInputElementValueSanitizer.js'; @@ -793,7 +792,7 @@ export default class HTMLInputElement extends HTMLElement { break; case 'file': if (value !== '') { - throw new DOMException( + throw new this[PropertySymbol.window].DOMException( 'Input elements of type "file" may only programmatically set the value to empty string.', DOMExceptionNameEnum.invalidStateError ); @@ -837,7 +836,7 @@ export default class HTMLInputElement extends HTMLElement { */ public set selectionStart(start: number) { if (!this.#isSelectionSupported()) { - throw new DOMException( + throw new this[PropertySymbol.window].DOMException( `The input element's type (${this.type}) does not support selection.`, DOMExceptionNameEnum.invalidStateError ); @@ -870,7 +869,7 @@ export default class HTMLInputElement extends HTMLElement { */ public set selectionEnd(end: number) { if (!this.#isSelectionSupported()) { - throw new DOMException( + throw new this[PropertySymbol.window].DOMException( `The input element's type (${this.type}) does not support selection.`, DOMExceptionNameEnum.invalidStateError ); @@ -899,7 +898,7 @@ export default class HTMLInputElement extends HTMLElement { */ public set selectionDirection(direction: string) { if (!this.#isSelectionSupported()) { - throw new DOMException( + throw new this[PropertySymbol.window].DOMException( `The input element's type (${this.type}) does not support selection.`, DOMExceptionNameEnum.invalidStateError ); @@ -954,17 +953,17 @@ export default class HTMLInputElement extends HTMLElement { public set valueAsDate(value: Date | null) { // Specs at https://html.spec.whatwg.org/multipage/input.html#dom-input-valueasdate if (!['date', 'month', 'time', 'week'].includes(this.type)) { - throw new DOMException( + throw new this[PropertySymbol.window].DOMException( "Failed to set the 'valueAsDate' property on 'HTMLInputElement': This input element does not support Date values.", DOMExceptionNameEnum.invalidStateError ); } if (typeof value !== 'object') { - throw new TypeError( + throw new this[PropertySymbol.window].TypeError( "Failed to set the 'valueAsDate' property on 'HTMLInputElement': Failed to convert value to 'object'." ); } else if (value && !(value instanceof Date)) { - throw new TypeError( + throw new this[PropertySymbol.window].TypeError( "Failed to set the 'valueAsDate' property on 'HTMLInputElement': The provided value is not a Date." ); } else if (value === null || isNaN(value.getTime())) { @@ -1085,7 +1084,7 @@ export default class HTMLInputElement extends HTMLElement { break; } default: - throw new DOMException( + throw new this[PropertySymbol.window].DOMException( "Failed to set the 'valueAsNumber' property on 'HTMLInputElement': This input element does not support Number values.", DOMExceptionNameEnum.invalidStateError ); @@ -1132,7 +1131,7 @@ export default class HTMLInputElement extends HTMLElement { */ public set popoverTargetElement(popoverTargetElement: HTMLElement | null) { if (popoverTargetElement !== null && !(popoverTargetElement instanceof HTMLElement)) { - throw new TypeError( + throw new this[PropertySymbol.window].TypeError( `Failed to set the 'popoverTargetElement' property on 'HTMLInputElement': Failed to convert value to 'Element'.` ); } @@ -1194,7 +1193,7 @@ export default class HTMLInputElement extends HTMLElement { */ public setSelectionRange(start: number, end: number, direction = 'none'): void { if (!this.#isSelectionSupported()) { - throw new DOMException( + throw new this[PropertySymbol.window].DOMException( `The input element's type (${this.type}) does not support selection.`, DOMExceptionNameEnum.invalidStateError ); @@ -1226,7 +1225,7 @@ export default class HTMLInputElement extends HTMLElement { selectionMode = HTMLInputElementSelectionModeEnum.preserve ): void { if (!this.#isSelectionSupported()) { - throw new DOMException( + throw new this[PropertySymbol.window].DOMException( `The input element's type (${this.type}) does not support selection.`, DOMExceptionNameEnum.invalidStateError ); @@ -1240,7 +1239,7 @@ export default class HTMLInputElement extends HTMLElement { } if (start > end) { - throw new DOMException( + throw new this[PropertySymbol.window].DOMException( 'The index is not in the allowed range.', DOMExceptionNameEnum.invalidStateError ); @@ -1359,13 +1358,16 @@ export default class HTMLInputElement extends HTMLElement { * @override */ public override dispatchEvent(event: Event): boolean { - // Do nothing if the input element is disabled and the event is a click event. if ( - event.type === 'click' && - event instanceof MouseEvent && - event.eventPhase === EventPhaseEnum.none && - this.disabled + event[PropertySymbol.type] !== 'click' || + event[PropertySymbol.eventPhase] !== EventPhaseEnum.none || + !(event instanceof MouseEvent) ) { + return super.dispatchEvent(event); + } + + // Do nothing if the input element is disabled and the event is a click event. + if (this.disabled) { return false; } @@ -1373,63 +1375,50 @@ export default class HTMLInputElement extends HTMLElement { // The checkbox or radio button has to be checked before the click event is dispatched, so that event listeners can check the checked value. // However, the value has to be restored if preventDefault() is called on the click event. - if ( - (event.eventPhase === EventPhaseEnum.atTarget || - event.eventPhase === EventPhaseEnum.bubbling) && - event.type === 'click' && - event instanceof MouseEvent - ) { - const inputType = this.type; - if (inputType === 'checkbox' || inputType === 'radio') { - previousCheckedValue = this.checked; - this.#setChecked(inputType === 'checkbox' ? !previousCheckedValue : true); - } + const type = this.type; + + if (type === 'checkbox' || type === 'radio') { + previousCheckedValue = this.checked; + this.#setChecked(type === 'checkbox' ? !previousCheckedValue : true); } + // Dispatches the event const returnValue = super.dispatchEvent(event); if ( - !event.defaultPrevented && - (event.eventPhase === EventPhaseEnum.atTarget || - event.eventPhase === EventPhaseEnum.bubbling) && - event.type === 'click' && - event instanceof MouseEvent && - this[PropertySymbol.isConnected] + event[PropertySymbol.type] !== 'click' || + event[PropertySymbol.eventPhase] !== EventPhaseEnum.none || + !(event instanceof MouseEvent) ) { - const inputType = this.type; - if (!this.readOnly || inputType === 'checkbox' || inputType === 'radio') { - if (inputType === 'checkbox' || inputType === 'radio') { - this.dispatchEvent(new Event('input', { bubbles: true, cancelable: true })); - this.dispatchEvent(new Event('change', { bubbles: true, cancelable: true })); - } else if (inputType === 'submit') { - const form = this.form; - if (form) { + return returnValue; + } + + if (event[PropertySymbol.defaultPrevented]) { + // Restore checked state if preventDefault() is triggered inside a listener of the click event. + if (previousCheckedValue !== null) { + this.#setChecked(previousCheckedValue); + } + } else { + const type = this.type; + + if (type === 'checkbox' && this[PropertySymbol.isConnected]) { + this.dispatchEvent(new Event('input', { bubbles: true, cancelable: true })); + this.dispatchEvent(new Event('change', { bubbles: true, cancelable: true })); + } else if (type === 'radio' && !previousCheckedValue && this[PropertySymbol.isConnected]) { + this.dispatchEvent(new Event('input', { bubbles: true, cancelable: true })); + this.dispatchEvent(new Event('change', { bubbles: true, cancelable: true })); + } else if (type === 'submit' || type === 'reset') { + const form = this.form; + if (form) { + if (type === 'submit' && this[PropertySymbol.isConnected]) { form.requestSubmit(this); - } - } else if (inputType === 'reset' && this[PropertySymbol.isConnected]) { - const form = this.form; - if (form) { + } else if (type === 'reset') { form.reset(); } } } } - // Restore checked state if preventDefault() is triggered on the click event. - if ( - event.defaultPrevented && - (event.eventPhase === EventPhaseEnum.atTarget || - event.eventPhase === EventPhaseEnum.bubbling) && - event.type === 'click' && - event instanceof MouseEvent && - previousCheckedValue !== null - ) { - const inputType = this.type; - if (inputType === 'checkbox' || inputType === 'radio') { - this.#setChecked(previousCheckedValue); - } - } - return returnValue; } diff --git a/packages/happy-dom/src/nodes/html-label-element/HTMLLabelElement.ts b/packages/happy-dom/src/nodes/html-label-element/HTMLLabelElement.ts index 1021413e1..3f5e79754 100644 --- a/packages/happy-dom/src/nodes/html-label-element/HTMLLabelElement.ts +++ b/packages/happy-dom/src/nodes/html-label-element/HTMLLabelElement.ts @@ -6,6 +6,12 @@ import EventPhaseEnum from '../../event/EventPhaseEnum.js'; import HTMLInputElement from '../html-input-element/HTMLInputElement.js'; import Document from '../document/Document.js'; import MouseEvent from '../../event/events/MouseEvent.js'; +import HTMLButtonElement from '../html-button-element/HTMLButtonElement.js'; +import HTMLMeterElement from '../html-meter-element/HTMLMeterElement.js'; +import HTMLOutputElement from '../html-output-element/HTMLOutputElement.js'; +import HTMLProgressElement from '../html-progress-element/HTMLProgressElement.js'; +import HTMLSelectElement from '../html-select-element/HTMLSelectElement.js'; +import HTMLTextAreaElement from '../html-text-area-element/HTMLTextAreaElement.js'; /** * HTML Label Element. @@ -44,7 +50,15 @@ export default class HTMLLabelElement extends HTMLElement { * * @returns Control element. */ - public get control(): HTMLElement | null { + public get control(): + | HTMLInputElement + | HTMLButtonElement + | HTMLMeterElement + | HTMLOutputElement + | HTMLProgressElement + | HTMLSelectElement + | HTMLTextAreaElement + | null { const htmlFor = this.getAttribute('for'); if (htmlFor !== null) { if (!htmlFor || !this[PropertySymbol.isConnected]) { @@ -56,20 +70,20 @@ export default class HTMLLabelElement extends HTMLElement { if (control) { switch (control[PropertySymbol.tagName]) { case 'INPUT': - return (control).type !== 'hidden' ? control : null; + return (control).type !== 'hidden' ? control : null; case 'BUTTON': case 'METER': case 'OUTPUT': case 'PROGRESS': case 'SELECT': case 'TEXTAREA': - return control; + return control; default: return null; } } } - return ( + return ( this.querySelector('button,input:not([type="hidden"]),meter,output,progress,select,textarea') ); } @@ -97,9 +111,10 @@ export default class HTMLLabelElement extends HTMLElement { const returnValue = super.dispatchEvent(event); if ( + !event[PropertySymbol.defaultPrevented] && event.type === 'click' && - event instanceof MouseEvent && - (event.eventPhase === EventPhaseEnum.atTarget || event.eventPhase === EventPhaseEnum.bubbling) + event.eventPhase === EventPhaseEnum.none && + event instanceof MouseEvent ) { const control = this.control; if (control && event.target !== control) { diff --git a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElement.ts b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElement.ts index 989be7572..a5319f3ef 100644 --- a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElement.ts +++ b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElement.ts @@ -4,13 +4,12 @@ import HTMLElement from '../html-element/HTMLElement.js'; import Event from '../../event/Event.js'; import ErrorEvent from '../../event/events/ErrorEvent.js'; import DOMTokenList from '../element/DOMTokenList.js'; -import IBrowserFrame from '../../browser/types/IBrowserFrame.js'; import Attr from '../attr/Attr.js'; import WindowErrorUtility from '../../window/WindowErrorUtility.js'; -import DOMException from '../../exception/DOMException.js'; import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; import ResourceFetch from '../../fetch/ResourceFetch.js'; import DocumentReadyStateManager from '../document/DocumentReadyStateManager.js'; +import WindowBrowserContext from '../../window/WindowBrowserContext.js'; /** * HTML Link Element. @@ -28,18 +27,6 @@ export default class HTMLLinkElement extends HTMLElement { public [PropertySymbol.evaluateCSS] = true; public [PropertySymbol.relList]: DOMTokenList | null = null; #loadedStyleSheetURL: string | null = null; - #browserFrame: IBrowserFrame; - - /** - * Constructor. - * - * @param browserFrame Browser frame. - */ - constructor(browserFrame) { - super(); - - this.#browserFrame = browserFrame; - } /** * Returns sheet. @@ -246,8 +233,14 @@ export default class HTMLLinkElement extends HTMLElement { * @param rel Rel. */ async #loadStyleSheet(url: string | null, rel: string | null): Promise { - const browserSettings = this.#browserFrame.page.context.browser.settings; - const window = this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow]; + const window = this[PropertySymbol.window]; + const browserFrame = new WindowBrowserContext(window).getBrowserFrame(); + + if (!browserFrame) { + return; + } + + const browserSettings = browserFrame.page?.context?.browser?.settings; if (!url || !rel || rel.toLowerCase() !== 'stylesheet' || !this[PropertySymbol.isConnected]) { return; @@ -264,13 +257,13 @@ export default class HTMLLinkElement extends HTMLElement { return; } - if (browserSettings.disableCSSFileLoading) { + if (browserSettings && browserSettings.disableCSSFileLoading) { if (browserSettings.handleDisabledFileLoadingAsSuccess) { this.dispatchEvent(new Event('load')); } else { WindowErrorUtility.dispatchError( this, - new DOMException( + new window.DOMException( `Failed to load external stylesheet "${absoluteURL}". CSS file loading is disabled.`, DOMExceptionNameEnum.notSupportedError ) @@ -280,7 +273,7 @@ export default class HTMLLinkElement extends HTMLElement { } const resourceFetch = new ResourceFetch({ - browserFrame: this.#browserFrame, + browserFrame, window: window }); const readyStateManager = (<{ [PropertySymbol.readyStateManager]: DocumentReadyStateManager }>( diff --git a/packages/happy-dom/src/nodes/html-media-element/HTMLMediaElement.ts b/packages/happy-dom/src/nodes/html-media-element/HTMLMediaElement.ts index 9dd0a2b39..5d579d350 100644 --- a/packages/happy-dom/src/nodes/html-media-element/HTMLMediaElement.ts +++ b/packages/happy-dom/src/nodes/html-media-element/HTMLMediaElement.ts @@ -1,6 +1,5 @@ import ErrorEvent from '../../event/events/ErrorEvent.js'; import Event from '../../event/Event.js'; -import DOMException from '../../exception/DOMException.js'; import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; import HTMLElement from '../html-element/HTMLElement.js'; import TimeRanges from './TimeRanges.js'; @@ -72,7 +71,7 @@ export default class HTMLMediaElement extends HTMLElement { public [PropertySymbol.seekable] = new TimeRanges(PropertySymbol.illegalConstructor); public [PropertySymbol.sinkId]: string = ''; public [PropertySymbol.played] = new TimeRanges(PropertySymbol.illegalConstructor); - public [PropertySymbol.remote] = new RemotePlayback(); + public [PropertySymbol.remote] = new this[PropertySymbol.window].RemotePlayback(); public [PropertySymbol.controlsList]: DOMTokenList | null = null; public [PropertySymbol.mediaKeys]: object | null = null; public [PropertySymbol.srcObject]: MediaStream | null = null; @@ -413,7 +412,7 @@ export default class HTMLMediaElement extends HTMLElement { for (const track of this.querySelectorAll('track')[PropertySymbol.items]) { items.push(track.track); } - return new TextTrackList(PropertySymbol.illegalConstructor, items); + return new this[PropertySymbol.window].TextTrackList(PropertySymbol.illegalConstructor, items); } /** @@ -448,12 +447,12 @@ export default class HTMLMediaElement extends HTMLElement { const parsedVolume = Number(volume); if (isNaN(parsedVolume)) { - throw new TypeError( + throw new this[PropertySymbol.window].TypeError( `Failed to set the 'volume' property on 'HTMLMediaElement': The provided double value is non-finite.` ); } if (parsedVolume < 0 || parsedVolume > 1) { - throw new DOMException( + throw new this[PropertySymbol.window].DOMException( `Failed to set the 'volume' property on 'HTMLMediaElement': The volume provided (${parsedVolume}) is outside the range [0, 1].`, DOMExceptionNameEnum.indexSizeError ); @@ -504,7 +503,7 @@ export default class HTMLMediaElement extends HTMLElement { public set currentTime(currentTime: number | string) { const parsedCurrentTime = Number(currentTime); if (isNaN(parsedCurrentTime)) { - throw new TypeError( + throw new this[PropertySymbol.window].TypeError( `Failed to set the 'currentTime' property on 'HTMLMediaElement': The provided double value is non-finite.` ); } @@ -528,7 +527,7 @@ export default class HTMLMediaElement extends HTMLElement { public set playbackRate(playbackRate: number | string) { const parsedPlaybackRate = Number(playbackRate); if (isNaN(parsedPlaybackRate)) { - throw new TypeError( + throw new this[PropertySymbol.window].TypeError( `Failed to set the 'playbackRate' property on 'HTMLMediaElement': The provided double value is non-finite.` ); } @@ -552,7 +551,7 @@ export default class HTMLMediaElement extends HTMLElement { public set defaultPlaybackRate(defaultPlaybackRate: number | string) { const parsedDefaultPlaybackRate = Number(defaultPlaybackRate); if (isNaN(parsedDefaultPlaybackRate)) { - throw new TypeError( + throw new this[PropertySymbol.window].TypeError( `Failed to set the 'defaultPlaybackRate' property on 'HTMLMediaElement': The provided double value is non-finite.` ); } @@ -594,17 +593,21 @@ export default class HTMLMediaElement extends HTMLElement { * @param language The language of the text track data. */ public addTextTrack(kind: TextTrackKindEnum, label?: string, language?: string): TextTrack { + const window = this[PropertySymbol.window]; + if (arguments.length === 0) { - throw new TypeError( + throw new window.TypeError( `Failed to execute 'addTextTrack' on 'HTMLMediaElement': 1 argument required, but only 0 present.` ); } + if (!TextTrackKindEnum[kind]) { - throw new TypeError( + throw new window.TypeError( `Failed to execute 'addTextTrack' on 'HTMLMediaElement': The provided value '${kind}' is not a valid enum value of type TextTrackKind.` ); } - const track = new TextTrack(PropertySymbol.illegalConstructor); + + const track = new window.TextTrack(PropertySymbol.illegalConstructor); track[PropertySymbol.kind] = kind; track[PropertySymbol.label] = label || ''; track[PropertySymbol.language] = language || ''; @@ -688,7 +691,7 @@ export default class HTMLMediaElement extends HTMLElement { * @returns MediaStream. */ public captureStream(): MediaStream { - return new MediaStream(); + return new this[PropertySymbol.window].MediaStream(); } /** diff --git a/packages/happy-dom/src/nodes/html-media-element/MediaStream.ts b/packages/happy-dom/src/nodes/html-media-element/MediaStream.ts index 85171b5fe..b9d972dca 100644 --- a/packages/happy-dom/src/nodes/html-media-element/MediaStream.ts +++ b/packages/happy-dom/src/nodes/html-media-element/MediaStream.ts @@ -29,6 +29,12 @@ export default class MediaStream extends EventTarget { constructor(streamOrTracks?: MediaStream | MediaStreamTrack[]) { super(); + if (!this[PropertySymbol.window]) { + throw new TypeError( + `Failed to construct '${this.constructor.name}': '${this.constructor.name}' was constructed outside a Window context.` + ); + } + if (streamOrTracks !== undefined) { this[PropertySymbol.tracks] = streamOrTracks instanceof MediaStream diff --git a/packages/happy-dom/src/nodes/html-media-element/MediaStreamTrack.ts b/packages/happy-dom/src/nodes/html-media-element/MediaStreamTrack.ts index f6a3f55e2..995960286 100644 --- a/packages/happy-dom/src/nodes/html-media-element/MediaStreamTrack.ts +++ b/packages/happy-dom/src/nodes/html-media-element/MediaStreamTrack.ts @@ -74,6 +74,12 @@ export default class MediaStreamTrack extends EventTarget { if (illegalConstructorSymbol !== PropertySymbol.illegalConstructor) { throw new TypeError('Illegal constructor'); } + + if (!this[PropertySymbol.window]) { + throw new TypeError( + `Failed to construct '${this.constructor.name}': '${this.constructor.name}' was constructed outside a Window context.` + ); + } } /** diff --git a/packages/happy-dom/src/nodes/html-media-element/TextTrack.ts b/packages/happy-dom/src/nodes/html-media-element/TextTrack.ts index 86637f4d3..718dc58a5 100644 --- a/packages/happy-dom/src/nodes/html-media-element/TextTrack.ts +++ b/packages/happy-dom/src/nodes/html-media-element/TextTrack.ts @@ -38,6 +38,12 @@ export default class TextTrack extends EventTarget { if (illegalConstructorSymbol !== PropertySymbol.illegalConstructor) { throw new TypeError('Illegal constructor'); } + + if (!this[PropertySymbol.window]) { + throw new TypeError( + `Failed to construct '${this.constructor.name}': '${this.constructor.name}' was constructed outside a Window context.` + ); + } } /** diff --git a/packages/happy-dom/src/nodes/html-media-element/TextTrackCue.ts b/packages/happy-dom/src/nodes/html-media-element/TextTrackCue.ts index 9b415931d..484b8bc49 100644 --- a/packages/happy-dom/src/nodes/html-media-element/TextTrackCue.ts +++ b/packages/happy-dom/src/nodes/html-media-element/TextTrackCue.ts @@ -32,6 +32,12 @@ export default abstract class TextTrackCue extends EventTarget { if (illegalConstructorSymbol !== PropertySymbol.illegalConstructor) { throw new TypeError('Illegal constructor'); } + + if (!this[PropertySymbol.window]) { + throw new TypeError( + `Failed to construct '${this.constructor.name}': '${this.constructor.name}' was constructed outside a Window context.` + ); + } } /** diff --git a/packages/happy-dom/src/nodes/html-media-element/VTTCue.ts b/packages/happy-dom/src/nodes/html-media-element/VTTCue.ts index 4dbc9bba8..7036e9ff3 100644 --- a/packages/happy-dom/src/nodes/html-media-element/VTTCue.ts +++ b/packages/happy-dom/src/nodes/html-media-element/VTTCue.ts @@ -1,7 +1,6 @@ import DocumentFragment from '../document-fragment/DocumentFragment.js'; import TextTrackCue from './TextTrackCue.js'; import VTTRegion from './VTTRegion.js'; -import BrowserWindow from '../../window/BrowserWindow.js'; import * as PropertySymbol from '../../PropertySymbol.js'; /** @@ -20,19 +19,19 @@ export default class VTTCue extends TextTrackCue { public size: number = 100; public align: string = ''; public text: string = ''; - #window: BrowserWindow; /** * Constructor. * - * @param window Owner window object. Injected by BrowserWindow. * @param startTime The start time for the cue. * @param endTime The end time for the cue. * @param text The text of the cue. */ - constructor(window: BrowserWindow, startTime: number, endTime: number, text: string) { + constructor(startTime: number, endTime: number, text: string) { super(PropertySymbol.illegalConstructor); + const window = this[PropertySymbol.window]; + // TODO: Can we find a better solution for counting arguments by using the "arguments" property? let argumentCount = 0; @@ -48,7 +47,7 @@ export default class VTTCue extends TextTrackCue { } if (argumentCount < 3) { - throw new TypeError( + throw new window.TypeError( `Failed to construct 'VTTCue': 3 arguments required, but only ${argumentCount} present.` ); } @@ -57,10 +56,11 @@ export default class VTTCue extends TextTrackCue { endTime = Number(endTime); if (isNaN(startTime) || isNaN(endTime)) { - throw new TypeError(`Failed to construct 'VTTCue': The provided double value is non-finite.`); + throw new window.TypeError( + `Failed to construct 'VTTCue': The provided double value is non-finite.` + ); } - this.#window = window; this.startTime = startTime; this.endTime = endTime; this.text = String(text); @@ -72,8 +72,9 @@ export default class VTTCue extends TextTrackCue { * @returns DocumentFragment */ public getCueAsHTML(): DocumentFragment { - const fragment = this.#window.document.createDocumentFragment(); - fragment.appendChild(this.#window.document.createTextNode(this.text)); + const window = this[PropertySymbol.window]; + const fragment = window.document.createDocumentFragment(); + fragment.appendChild(window.document.createTextNode(this.text)); return fragment; } } diff --git a/packages/happy-dom/src/nodes/html-meter-element/HTMLMeterElement.ts b/packages/happy-dom/src/nodes/html-meter-element/HTMLMeterElement.ts index 2cecf8f59..91c3cf887 100644 --- a/packages/happy-dom/src/nodes/html-meter-element/HTMLMeterElement.ts +++ b/packages/happy-dom/src/nodes/html-meter-element/HTMLMeterElement.ts @@ -2,6 +2,8 @@ import HTMLElement from '../html-element/HTMLElement.js'; import HTMLLabelElement from '../html-label-element/HTMLLabelElement.js'; import HTMLLabelElementUtility from '../html-label-element/HTMLLabelElementUtility.js'; import NodeList from '../node/NodeList.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; + /** * HTMLMeterElement * @@ -31,7 +33,7 @@ export default class HTMLMeterElement extends HTMLElement { */ public set high(high: number) { if (typeof high !== 'number') { - throw new TypeError( + throw new this[PropertySymbol.window].TypeError( "Failed to set the 'high' property on 'HTMLMeterElement': The provided double value is non-finite." ); } @@ -62,7 +64,7 @@ export default class HTMLMeterElement extends HTMLElement { */ public set low(low: number) { if (typeof low !== 'number') { - throw new TypeError( + throw new this[PropertySymbol.window].TypeError( "Failed to set the 'low' property on 'HTMLMeterElement': The provided double value is non-finite." ); } @@ -93,7 +95,7 @@ export default class HTMLMeterElement extends HTMLElement { */ public set max(max: number) { if (typeof max !== 'number') { - throw new TypeError( + throw new this[PropertySymbol.window].TypeError( "Failed to set the 'max' property on 'HTMLMeterElement': The provided double value is non-finite." ); } @@ -124,7 +126,7 @@ export default class HTMLMeterElement extends HTMLElement { */ public set min(min: number) { if (typeof min !== 'number') { - throw new TypeError( + throw new this[PropertySymbol.window].TypeError( "Failed to set the 'min' property on 'HTMLMeterElement': The provided double value is non-finite." ); } @@ -158,7 +160,7 @@ export default class HTMLMeterElement extends HTMLElement { */ public set optimum(optimum: number) { if (typeof optimum !== 'number') { - throw new TypeError( + throw new this[PropertySymbol.window].TypeError( "Failed to set the 'optimum' property on 'HTMLMeterElement': The provided double value is non-finite." ); } @@ -189,7 +191,7 @@ export default class HTMLMeterElement extends HTMLElement { */ public set value(value: number) { if (typeof value !== 'number') { - throw new TypeError( + throw new this[PropertySymbol.window].TypeError( "Failed to set the 'value' property on 'HTMLMeterElement': The provided double value is non-finite." ); } diff --git a/packages/happy-dom/src/nodes/html-progress-element/HTMLProgressElement.ts b/packages/happy-dom/src/nodes/html-progress-element/HTMLProgressElement.ts index 553f68b3d..60928e444 100644 --- a/packages/happy-dom/src/nodes/html-progress-element/HTMLProgressElement.ts +++ b/packages/happy-dom/src/nodes/html-progress-element/HTMLProgressElement.ts @@ -2,6 +2,8 @@ import HTMLElement from '../html-element/HTMLElement.js'; import HTMLLabelElement from '../html-label-element/HTMLLabelElement.js'; import HTMLLabelElementUtility from '../html-label-element/HTMLLabelElementUtility.js'; import NodeList from '../node/NodeList.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; + /** * HTMLProgressElement * @@ -31,7 +33,7 @@ export default class HTMLProgressElement extends HTMLElement { */ public set max(max: number) { if (typeof max !== 'number') { - throw new TypeError( + throw new this[PropertySymbol.window].TypeError( "Failed to set the 'max' property on 'HTMLProgressElement': The provided double value is non-finite." ); } @@ -62,7 +64,7 @@ export default class HTMLProgressElement extends HTMLElement { */ public set value(value: number) { if (typeof value !== 'number') { - throw new TypeError( + throw new this[PropertySymbol.window].TypeError( "Failed to set the 'value' property on 'HTMLProgressElement': The provided double value is non-finite." ); } 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 853a0ba14..627c7f235 100644 --- a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts +++ b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts @@ -3,11 +3,9 @@ import * as PropertySymbol from '../../PropertySymbol.js'; import Event from '../../event/Event.js'; import ErrorEvent from '../../event/events/ErrorEvent.js'; import WindowErrorUtility from '../../window/WindowErrorUtility.js'; -import WindowBrowserSettingsReader from '../../window/WindowBrowserSettingsReader.js'; -import IBrowserFrame from '../../browser/types/IBrowserFrame.js'; +import WindowBrowserContext from '../../window/WindowBrowserContext.js'; import BrowserErrorCaptureEnum from '../../browser/enums/BrowserErrorCaptureEnum.js'; import Attr from '../attr/Attr.js'; -import DOMException from '../../exception/DOMException.js'; import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; import ResourceFetch from '../../fetch/ResourceFetch.js'; import DocumentReadyStateManager from '../document/DocumentReadyStateManager.js'; @@ -30,20 +28,7 @@ export default class HTMLScriptElement extends HTMLElement { public [PropertySymbol.evaluateScript] = true; // Private properties - #browserFrame: IBrowserFrame; #loadedScriptURL: string | null = null; - - /** - * Constructor. - * - * @param browserFrame Browser frame. - */ - constructor(browserFrame) { - super(); - - this.#browserFrame = browserFrame; - } - /** * Returns type. * @@ -198,9 +183,7 @@ export default class HTMLScriptElement extends HTMLElement { * @override */ public override [PropertySymbol.connectedToDocument](): void { - const browserSettings = WindowBrowserSettingsReader.getSettings( - this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow] - ); + const browserSettings = new WindowBrowserContext(this[PropertySymbol.window]).getSettings(); super[PropertySymbol.connectedToDocument](); @@ -209,7 +192,7 @@ export default class HTMLScriptElement extends HTMLElement { if (src !== null) { this.#loadScript(src); - } else if (!browserSettings.disableJavaScriptEvaluation) { + } else if (browserSettings && !browserSettings.disableJavaScriptEvaluation) { const textContent = this.textContent; const type = this.getAttribute('type'); if ( @@ -221,20 +204,16 @@ export default class HTMLScriptElement extends HTMLElement { ) { this[PropertySymbol.ownerDocument][PropertySymbol.currentScript] = this; - const code = - `//# sourceURL=${ - this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow].location.href - }\n` + textContent; + const code = `//# sourceURL=${this[PropertySymbol.window].location.href}\n` + textContent; if ( browserSettings.disableErrorCapturing || browserSettings.errorCapture !== BrowserErrorCaptureEnum.tryAndCatch ) { - this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow].eval(code); + this[PropertySymbol.window].eval(code); } else { - WindowErrorUtility.captureError( - this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow], - () => this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow].eval(code) + WindowErrorUtility.captureError(this[PropertySymbol.window], () => + this[PropertySymbol.window].eval(code) ); } @@ -268,19 +247,23 @@ export default class HTMLScriptElement extends HTMLElement { * @param url URL. */ async #loadScript(url: string): Promise { - const browserSettings = this.#browserFrame.page.context.browser.settings; + const window = this[PropertySymbol.window]; + const browserFrame = new WindowBrowserContext(window).getBrowserFrame(); const async = this.getAttribute('async') !== null; + if (!browserFrame) { + return; + } + + const browserSettings = browserFrame.page?.context?.browser?.settings; + if (!url || !this[PropertySymbol.isConnected]) { return; } let absoluteURL: string; try { - absoluteURL = new URL( - url, - this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow].location.href - ).href; + absoluteURL = new URL(url, this[PropertySymbol.window].location.href).href; } catch (error) { return; } @@ -290,15 +273,15 @@ export default class HTMLScriptElement extends HTMLElement { } if ( - browserSettings.disableJavaScriptFileLoading || - browserSettings.disableJavaScriptEvaluation + browserSettings && + (browserSettings.disableJavaScriptFileLoading || browserSettings.disableJavaScriptEvaluation) ) { if (browserSettings.handleDisabledFileLoadingAsSuccess) { this.dispatchEvent(new Event('load')); } else { WindowErrorUtility.dispatchError( this, - new DOMException( + new window.DOMException( `Failed to load external script "${absoluteURL}". JavaScript file loading is disabled.`, DOMExceptionNameEnum.notSupportedError ) @@ -308,8 +291,8 @@ export default class HTMLScriptElement extends HTMLElement { } const resourceFetch = new ResourceFetch({ - browserFrame: this.#browserFrame, - window: this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow] + browserFrame, + window: this[PropertySymbol.window] }); let code: string | null = null; let error: Error | null = null; @@ -319,9 +302,7 @@ export default class HTMLScriptElement extends HTMLElement { if (async) { const readyStateManager = (< { [PropertySymbol.readyStateManager]: DocumentReadyStateManager } - >(this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow]))[ - PropertySymbol.readyStateManager - ]; + >(this[PropertySymbol.window]))[PropertySymbol.readyStateManager]; readyStateManager.startTask(); @@ -350,11 +331,10 @@ export default class HTMLScriptElement extends HTMLElement { browserSettings.disableErrorCapturing || browserSettings.errorCapture !== BrowserErrorCaptureEnum.tryAndCatch ) { - this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow].eval(code); + this[PropertySymbol.window].eval(code); } else { - WindowErrorUtility.captureError( - this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow], - () => this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow].eval(code) + WindowErrorUtility.captureError(this[PropertySymbol.window], () => + this[PropertySymbol.window].eval(code) ); } this[PropertySymbol.ownerDocument][PropertySymbol.currentScript] = null; diff --git a/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts b/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts index 18588d037..a11538fae 100644 --- a/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts +++ b/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts @@ -11,7 +11,6 @@ import HTMLCollection from '../element/HTMLCollection.js'; import NodeTypeEnum from '../node/NodeTypeEnum.js'; import QuerySelector from '../../query-selector/QuerySelector.js'; import NodeList from '../node/NodeList.js'; -import DOMException from '../../exception/DOMException.js'; import ClassMethodBinder from '../../ClassMethodBinder.js'; import Element from '../element/Element.js'; import Node from '../node/Node.js'; @@ -74,7 +73,7 @@ export default class HTMLSelectElement extends HTMLElement { } if (!newValue || !(newValue instanceof HTMLOptionElement)) { - throw new Error( + throw new this[PropertySymbol.window].Error( `TypeError: Failed to set an indexed property [${index}] on 'HTMLSelectElement': parameter 2 is not of type 'HTMLOptionElement'.` ); } @@ -136,7 +135,7 @@ export default class HTMLSelectElement extends HTMLElement { } if (!descriptor.value || !(descriptor.value instanceof HTMLOptionElement)) { - throw new Error( + throw new this[PropertySymbol.window].Error( `TypeError: Failed to set an indexed property [${index}] on 'HTMLSelectElement': parameter 2 is not of type 'HTMLOptionElement'.` ); } @@ -553,9 +552,11 @@ export default class HTMLSelectElement extends HTMLElement { return; } + const window = this[PropertySymbol.window]; + if (typeof before !== 'number') { if (!(before instanceof HTMLOptionElement)) { - throw new DOMException( + throw new window.DOMException( "Failed to execute 'add' on 'HTMLFormElement': The node before which the new node is to be inserted before is not an 'HTMLOptionElement'." ); } @@ -566,7 +567,7 @@ export default class HTMLSelectElement extends HTMLElement { const optionsElement = options[before]; if (!optionsElement) { - throw new DOMException( + throw new window.DOMException( "Failed to execute 'add' on 'HTMLFormElement': The node before which the new node is to be inserted before is not a child of this node." ); } diff --git a/packages/happy-dom/src/nodes/html-table-element/HTMLTableElement.ts b/packages/happy-dom/src/nodes/html-table-element/HTMLTableElement.ts index 8a225968e..f3205c4bf 100644 --- a/packages/happy-dom/src/nodes/html-table-element/HTMLTableElement.ts +++ b/packages/happy-dom/src/nodes/html-table-element/HTMLTableElement.ts @@ -5,7 +5,6 @@ import * as PropertySymbol from '../../PropertySymbol.js'; import QuerySelector from '../../query-selector/QuerySelector.js'; import HTMLTableCaptionElement from '../html-table-caption-element/HTMLTableCaptionElement.js'; import HTMLTableSectionElement from '../html-table-section-element/HTMLTableSectionElement.js'; -import DOMException from '../../exception/DOMException.js'; import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; /** @@ -34,7 +33,7 @@ export default class HTMLTableElement extends HTMLElement { public set caption(caption: HTMLTableCaptionElement | null) { if (caption) { if (!(caption instanceof HTMLTableCaptionElement)) { - throw new TypeError( + throw new this[PropertySymbol.window].TypeError( "Failed to set the 'caption' property on 'HTMLTableElement': Failed to convert value to 'HTMLTableCaptionElement'." ); } @@ -63,7 +62,7 @@ export default class HTMLTableElement extends HTMLElement { public set tHead(tHead: HTMLTableSectionElement | null) { if (tHead) { if (!(tHead instanceof HTMLTableSectionElement)) { - throw new TypeError( + throw new this[PropertySymbol.window].TypeError( "Failed to set the 'tHead' property on 'HTMLTableElement': Failed to convert value to 'HTMLTableSectionElement'." ); } @@ -109,7 +108,7 @@ export default class HTMLTableElement extends HTMLElement { public set tFoot(tFoot: HTMLTableSectionElement | null) { if (tFoot) { if (!(tFoot instanceof HTMLTableSectionElement)) { - throw new TypeError( + throw new this[PropertySymbol.window].TypeError( "Failed to set the 'tFoot' property on 'HTMLTableElement': Failed to convert value to 'HTMLTableSectionElement'." ); } @@ -271,14 +270,14 @@ export default class HTMLTableElement extends HTMLElement { const rows = QuerySelector.querySelectorAll(this, 'tr')[PropertySymbol.items]; if (index < -1) { - throw new DOMException( + throw new this[PropertySymbol.window].DOMException( `Failed to execute 'insertRow' on 'HTMLTableElement': The index provided (${index}) is less than -1.`, DOMExceptionNameEnum.indexSizeError ); } if (index > rows.length) { - throw new DOMException( + throw new this[PropertySymbol.window].DOMException( `Failed to execute 'insertRow' on 'HTMLTableElement': The index provided (${index}) is greater than the number of rows (${rows.length}).`, DOMExceptionNameEnum.indexSizeError ); @@ -303,7 +302,7 @@ export default class HTMLTableElement extends HTMLElement { */ public deleteRow(index: number): void { if (arguments.length === 0) { - throw new TypeError( + throw new this[PropertySymbol.window].TypeError( "Failed to execute 'deleteRow' on 'HTMLTableElement': 1 argument required, but only 0 present." ); } @@ -313,7 +312,7 @@ export default class HTMLTableElement extends HTMLElement { } if (index < -1) { - throw new DOMException( + throw new this[PropertySymbol.window].DOMException( `Failed to execute 'deleteRow' on 'HTMLTableElement': The index provided (${index}) is less than -1.`, DOMExceptionNameEnum.indexSizeError ); @@ -322,7 +321,7 @@ export default class HTMLTableElement extends HTMLElement { const rows = QuerySelector.querySelectorAll(this, 'tr')[PropertySymbol.items]; if (index >= rows.length) { - throw new DOMException( + throw new this[PropertySymbol.window].DOMException( `Failed to execute 'deleteRow' on 'HTMLTableElement': The index provided (${index}) is greater than the number of rows in the table (${rows.length}).`, DOMExceptionNameEnum.indexSizeError ); diff --git a/packages/happy-dom/src/nodes/html-table-row-element/HTMLTableRowElement.ts b/packages/happy-dom/src/nodes/html-table-row-element/HTMLTableRowElement.ts index 34247083d..d5565d5bf 100644 --- a/packages/happy-dom/src/nodes/html-table-row-element/HTMLTableRowElement.ts +++ b/packages/happy-dom/src/nodes/html-table-row-element/HTMLTableRowElement.ts @@ -4,7 +4,6 @@ import HTMLTableCellElement from '../html-table-cell-element/HTMLTableCellElemen import * as PropertySymbol from '../../PropertySymbol.js'; import QuerySelector from '../../query-selector/QuerySelector.js'; import HTMLTableSectionElement from '../html-table-section-element/HTMLTableSectionElement.js'; -import DOMException from '../../exception/DOMException.js'; import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; /** @@ -83,14 +82,14 @@ export default class HTMLTableRowElement extends HTMLElement { const cells = QuerySelector.querySelectorAll(this, 'td,th')[PropertySymbol.items]; if (index < -1) { - throw new DOMException( + throw new this[PropertySymbol.window].DOMException( `Failed to execute 'insertCell' on 'HTMLTableRowElement': The index provided (${index}) is less than -1.`, DOMExceptionNameEnum.indexSizeError ); } if (index > cells.length) { - throw new DOMException( + throw new this[PropertySymbol.window].DOMException( `Failed to execute 'insertCell' on 'HTMLTableRowElement': The index provided (${index}) is greater than the number of cells (${cells.length}).`, DOMExceptionNameEnum.indexSizeError ); @@ -115,7 +114,7 @@ export default class HTMLTableRowElement extends HTMLElement { */ public deleteCell(index: number): void { if (arguments.length === 0) { - throw new TypeError( + throw new this[PropertySymbol.window].TypeError( "Failed to execute 'deleteCell' on 'HTMLTableRowElement': 1 argument required, but only 0 present." ); } @@ -125,7 +124,7 @@ export default class HTMLTableRowElement extends HTMLElement { } if (index < -1) { - throw new DOMException( + throw new this[PropertySymbol.window].DOMException( `Failed to execute 'deleteCell' on 'HTMLTableRowElement': The index provided (${index}) is less than -1.`, DOMExceptionNameEnum.indexSizeError ); @@ -134,7 +133,7 @@ export default class HTMLTableRowElement extends HTMLElement { const cells = QuerySelector.querySelectorAll(this, 'td,th')[PropertySymbol.items]; if (index >= cells.length) { - throw new DOMException( + throw new this[PropertySymbol.window].DOMException( `Failed to execute 'deleteCell' on 'HTMLTableRowElement': The index provided (${index}) is greater than the number of cells in the row (${cells.length}).`, DOMExceptionNameEnum.indexSizeError ); diff --git a/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElement.ts b/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElement.ts index 075bfca6d..df5f67b11 100644 --- a/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElement.ts +++ b/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElement.ts @@ -1,6 +1,5 @@ import Event from '../../event/Event.js'; import * as PropertySymbol from '../../PropertySymbol.js'; -import DOMException from '../../exception/DOMException.js'; import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; import HTMLElement from '../html-element/HTMLElement.js'; import HTMLFormElement from '../html-form-element/HTMLFormElement.js'; @@ -494,7 +493,7 @@ export default class HTMLTextAreaElement extends HTMLElement { } if (start > end) { - throw new DOMException( + throw new this[PropertySymbol.window].DOMException( 'The index is not in the allowed range.', DOMExceptionNameEnum.invalidStateError ); diff --git a/packages/happy-dom/src/nodes/html-track-element/HTMLTrackElement.ts b/packages/happy-dom/src/nodes/html-track-element/HTMLTrackElement.ts index 29b511694..36fd4f2d6 100644 --- a/packages/happy-dom/src/nodes/html-track-element/HTMLTrackElement.ts +++ b/packages/happy-dom/src/nodes/html-track-element/HTMLTrackElement.ts @@ -140,7 +140,7 @@ export default class HTMLTrackElement extends HTMLElement { * @returns TextTrack */ public get track(): TextTrack { - const textTrack = new TextTrack(PropertySymbol.illegalConstructor); + const textTrack = new this[PropertySymbol.window].TextTrack(PropertySymbol.illegalConstructor); textTrack[PropertySymbol.kind] = this.kind; textTrack[PropertySymbol.label] = this.label; textTrack[PropertySymbol.language] = this.srclang; diff --git a/packages/happy-dom/src/nodes/node/Node.ts b/packages/happy-dom/src/nodes/node/Node.ts index f6a743f22..f3e78ab20 100644 --- a/packages/happy-dom/src/nodes/node/Node.ts +++ b/packages/happy-dom/src/nodes/node/Node.ts @@ -9,7 +9,6 @@ import Attr from '../attr/Attr.js'; import NodeList from './NodeList.js'; import MutationRecord from '../../mutation-observer/MutationRecord.js'; import MutationTypeEnum from '../../mutation-observer/MutationTypeEnum.js'; -import DOMException from '../../exception/DOMException.js'; import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; import IMutationListener from '../../mutation-observer/IMutationListener.js'; import ICachedQuerySelectorAllResult from './ICachedQuerySelectorAllResult.js'; @@ -26,14 +25,15 @@ import HTMLFormElement from '../html-form-element/HTMLFormElement.js'; import HTMLSelectElement from '../html-select-element/HTMLSelectElement.js'; import HTMLTextAreaElement from '../html-text-area-element/HTMLTextAreaElement.js'; import HTMLSlotElement from '../html-slot-element/HTMLSlotElement.js'; +import WindowBrowserContext from '../../window/WindowBrowserContext.js'; import NodeFactory from '../NodeFactory.js'; /** * Node. */ export default class Node extends EventTarget { - // This is used when overriding a Node class and set it in a owner document context (used in BrowserWindow.constructor()). - public static [PropertySymbol.ownerDocument]: Document | null; + // Can be injected by CustomElementRegistry or a sub-class + public declare [PropertySymbol.ownerDocument]: Document; // Public properties public static readonly ELEMENT_NODE = NodeTypeEnum.elementNode; @@ -72,7 +72,6 @@ export default class Node extends EventTarget { // Internal properties public [PropertySymbol.isConnected] = false; - public [PropertySymbol.ownerDocument]: Document; public [PropertySymbol.parentNode]: Node | null = null; public [PropertySymbol.nodeType]: NodeTypeEnum; public [PropertySymbol.rootNode]: Node = null; @@ -114,17 +113,18 @@ export default class Node extends EventTarget { constructor() { super(); - const definedOwnerDocument = (this.constructor)[PropertySymbol.ownerDocument]; - - if (definedOwnerDocument) { - this[PropertySymbol.ownerDocument] = definedOwnerDocument; + // Window injected by WindowClassExtender (used when the Node can be created using "new" keyword) + if (this[PropertySymbol.window]) { + this[PropertySymbol.ownerDocument] = this[PropertySymbol.window].document; } else { const ownerDocument = NodeFactory.pullOwnerDocument(); if (!ownerDocument) { throw new TypeError('Illegal constructor'); } + this[PropertySymbol.ownerDocument] = ownerDocument; + this[PropertySymbol.window] = ownerDocument[PropertySymbol.window]; } } @@ -314,7 +314,7 @@ export default class Node extends EventTarget { if (base) { return base.href; } - return this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow].location.href; + return this[PropertySymbol.window].location.href; } /** @@ -386,7 +386,7 @@ export default class Node extends EventTarget { */ public appendChild(node: Node): Node { if (arguments.length < 1) { - throw new TypeError( + throw new this[PropertySymbol.window].TypeError( `Failed to execute 'appendChild' on 'Node': 1 argument required, but only 0 present` ); } @@ -401,7 +401,7 @@ export default class Node extends EventTarget { */ public removeChild(node: Node): Node { if (arguments.length < 1) { - throw new TypeError( + throw new this[PropertySymbol.window].TypeError( `Failed to execute 'removeChild' on 'Node': 1 argument required, but only 0 present` ); } @@ -417,7 +417,7 @@ export default class Node extends EventTarget { */ public insertBefore(newNode: Node, referenceNode: Node | null): Node { if (arguments.length < 2) { - throw new TypeError( + throw new this[PropertySymbol.window].TypeError( `Failed to execute 'insertBefore' on 'Node': 2 arguments required, but only ${arguments.length} present.` ); } @@ -433,7 +433,7 @@ export default class Node extends EventTarget { */ public replaceChild(newChild: Node, oldChild: Node): Node { if (arguments.length < 2) { - throw new TypeError( + throw new this[PropertySymbol.window].TypeError( `Failed to execute 'replaceChild' on 'Node': 2 arguments required, but only ${arguments.length} present.` ); } @@ -453,6 +453,7 @@ export default class Node extends EventTarget { ); // Document has childNodes directly when it is created + // We need to remove them if (clone[PropertySymbol.nodeArray].length) { const childNodes = clone[PropertySymbol.nodeArray]; while (childNodes.length) { @@ -485,13 +486,13 @@ export default class Node extends EventTarget { public [PropertySymbol.appendChild](node: Node, disableValidations = false): Node { if (!disableValidations) { if (node === this) { - throw new DOMException( + throw new this[PropertySymbol.window].DOMException( "Failed to execute 'appendChild' on 'Node': Not possible to append a node as a child of itself." ); } if (NodeUtility.isInclusiveAncestor(node, this, true)) { - throw new DOMException( + throw new this[PropertySymbol.window].DOMException( "Failed to execute 'appendChild' on 'Node': The new node is a parent of the node to insert to.", DOMExceptionNameEnum.domException ); @@ -557,7 +558,7 @@ export default class Node extends EventTarget { const index = this[PropertySymbol.nodeArray].indexOf(node); if (index === -1) { - throw new DOMException( + throw new this[PropertySymbol.window].DOMException( `Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.` ); } @@ -612,7 +613,7 @@ export default class Node extends EventTarget { } if (NodeUtility.isInclusiveAncestor(newNode, this, true)) { - throw new DOMException( + throw new this[PropertySymbol.window].DOMException( "Failed to execute 'insertBefore' on 'Node': The new node is a parent of the node to insert to.", DOMExceptionNameEnum.domException ); @@ -639,7 +640,7 @@ export default class Node extends EventTarget { // We need to check if the referenceNode is a child of this node before removing it from its parent, as the parent may be the same node and the index would be wrong. if (!nodeArray.includes(referenceNode)) { - throw new DOMException( + throw new this[PropertySymbol.window].DOMException( "Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node." ); } @@ -1007,14 +1008,15 @@ export default class Node extends EventTarget { * @see https://github.com/capricorn86/happy-dom/issues/1442 */ if (result instanceof Promise) { - const asyncTaskManager = - this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow][ - PropertySymbol.asyncTaskManager - ]; - const taskID = asyncTaskManager.startTask(); - result - .then(() => asyncTaskManager.endTask(taskID)) - .catch(() => asyncTaskManager.endTask(taskID)); + const asyncTaskManager = new WindowBrowserContext( + this[PropertySymbol.window] + ).getAsyncTaskManager(); + if (asyncTaskManager) { + const taskID = asyncTaskManager.startTask(); + result + .then(() => asyncTaskManager.endTask(taskID)) + .catch(() => asyncTaskManager.endTask(taskID)); + } } } } diff --git a/packages/happy-dom/src/nodes/node/NodeList.ts b/packages/happy-dom/src/nodes/node/NodeList.ts index 58b7c4bfd..2055f7d5b 100644 --- a/packages/happy-dom/src/nodes/node/NodeList.ts +++ b/packages/happy-dom/src/nodes/node/NodeList.ts @@ -24,14 +24,7 @@ class NodeList { this[PropertySymbol.items] = items; - // This only works for one level of inheritance, but it should be fine as there is no collection that goes deeper according to spec. - ClassMethodBinder.bindMethods( - this, - this.constructor !== NodeList ? [NodeList, this.constructor] : [NodeList], - { bindSymbols: true } - ); - - return new Proxy(this, { + const proxy = new Proxy(this, { get: (target, property) => { if (property === 'length') { return items.length; @@ -103,6 +96,15 @@ class NodeList { } } }); + + // This only works for one level of inheritance, but it should be fine as there is no collection that goes deeper according to spec. + ClassMethodBinder.bindMethods( + this, + this.constructor !== NodeList ? [NodeList, this.constructor] : [NodeList], + { bindSymbols: true, proxy } + ); + + return proxy; } /** diff --git a/packages/happy-dom/src/nodes/text/Text.ts b/packages/happy-dom/src/nodes/text/Text.ts index 0170bd264..84d0ea4bd 100644 --- a/packages/happy-dom/src/nodes/text/Text.ts +++ b/packages/happy-dom/src/nodes/text/Text.ts @@ -1,6 +1,5 @@ import * as PropertySymbol from '../../PropertySymbol.js'; import CharacterData from '../character-data/CharacterData.js'; -import DOMException from '../../exception/DOMException.js'; import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; import HTMLTextAreaElement from '../html-text-area-element/HTMLTextAreaElement.js'; import NodeTypeEnum from '../node/NodeTypeEnum.js'; @@ -57,7 +56,7 @@ export default class Text extends CharacterData { const length = this[PropertySymbol.data].length; if (offset < 0 || offset > length) { - throw new DOMException( + throw new this[PropertySymbol.window].DOMException( 'The index is not in the allowed range.', DOMExceptionNameEnum.indexSizeError ); diff --git a/packages/happy-dom/src/permissions/Permissions.ts b/packages/happy-dom/src/permissions/Permissions.ts index 963b04dd7..4ee7f0c96 100644 --- a/packages/happy-dom/src/permissions/Permissions.ts +++ b/packages/happy-dom/src/permissions/Permissions.ts @@ -1,5 +1,6 @@ import PermissionStatus from './PermissionStatus.js'; import PermissionNameEnum from './PermissionNameEnum.js'; +import BrowserWindow from '../window/BrowserWindow.js'; /** * Permissions API. @@ -11,6 +12,19 @@ export default class Permissions { #permissionStatus: { [name in PermissionNameEnum]?: PermissionStatus; } = {}; + #window: BrowserWindow; + + /** + * Constructor. + * + * @param window Window. + */ + constructor(window: BrowserWindow) { + if (!window?.document) { + new TypeError('Invalid constructor'); + } + this.#window = window; + } /** * Returns scroll restoration. @@ -38,7 +52,9 @@ export default class Permissions { ); } - this.#permissionStatus[permissionDescriptor.name] = new PermissionStatus('granted'); + this.#permissionStatus[permissionDescriptor.name] = new this.#window.PermissionStatus( + 'granted' + ); return this.#permissionStatus[permissionDescriptor.name]; } diff --git a/packages/happy-dom/src/query-selector/QuerySelector.ts b/packages/happy-dom/src/query-selector/QuerySelector.ts index 6a6ad73d6..0aebf3bab 100644 --- a/packages/happy-dom/src/query-selector/QuerySelector.ts +++ b/packages/happy-dom/src/query-selector/QuerySelector.ts @@ -13,7 +13,6 @@ import ISVGElementTagNameMap from '../config/ISVGElementTagNameMap.js'; import ICachedQuerySelectorAllItem from '../nodes/node/ICachedQuerySelectorAllResult.js'; import ICachedQuerySelectorItem from '../nodes/node/ICachedQuerySelectorResult.js'; import ICachedMatchesItem from '../nodes/node/ICachedMatchesResult.js'; -import DOMException from '../exception/DOMException.js'; import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum.js'; type DocumentPositionAndElement = { @@ -83,14 +82,16 @@ export default class QuerySelector { return new NodeList(PropertySymbol.illegalConstructor, []); } + const window = node[PropertySymbol.window]; + if (selector === '') { - throw new Error( + throw new window.Error( `Failed to execute 'querySelectorAll' on '${node.constructor.name}': The provided selector is empty.` ); } if (typeof selector !== 'string' && typeof selector !== 'boolean') { - throw new DOMException( + throw new window.DOMException( `Failed to execute 'querySelectorAll' on '${node.constructor.name}': '${selector}' is not a valid selector.`, 'SyntaxError' ); @@ -109,7 +110,7 @@ export default class QuerySelector { } if (INVALID_SELECTOR_REGEXP.test(selector)) { - throw new Error( + throw new window.Error( `Failed to execute 'querySelectorAll' on '${node.constructor.name}': '${selector}' is not a valid selector.` ); } @@ -202,14 +203,16 @@ export default class QuerySelector { return null; } + const window = node[PropertySymbol.window]; + if (selector === '') { - throw new Error( + throw new window.Error( `Failed to execute 'querySelector' on '${node.constructor.name}': The provided selector is empty.` ); } if (typeof selector !== 'string' && typeof selector !== 'boolean') { - throw new DOMException( + throw new window.DOMException( `Failed to execute 'querySelector' on '${node.constructor.name}': '${selector}' is not a valid selector.`, 'SyntaxError' ); @@ -227,7 +230,7 @@ export default class QuerySelector { } if (INVALID_SELECTOR_REGEXP.test(selector)) { - throw new Error( + throw new window.Error( `Failed to execute 'querySelector' on '${node.constructor.name}': '${selector}' is not a valid selector.` ); } @@ -284,14 +287,16 @@ export default class QuerySelector { }; } + const window = element[PropertySymbol.window]; + if (selector === '') { - throw new Error( + throw new window.Error( `Failed to execute 'matches' on '${element.constructor.name}': The provided selector is empty.` ); } if (typeof selector !== 'string' && typeof selector !== 'boolean') { - throw new DOMException( + throw new window.DOMException( `Failed to execute 'matches' on '${element.constructor.name}': '${selector}' is not a valid selector.`, DOMExceptionNameEnum.syntaxError ); @@ -316,7 +321,7 @@ export default class QuerySelector { if (ignoreErrors) { return null; } - throw new Error( + throw new window.Error( `Failed to execute 'matches' on '${element.constructor.name}': '${selector}' is not a valid selector.` ); } diff --git a/packages/happy-dom/src/range/Range.ts b/packages/happy-dom/src/range/Range.ts index fa52c59f1..88795d6c7 100644 --- a/packages/happy-dom/src/range/Range.ts +++ b/packages/happy-dom/src/range/Range.ts @@ -4,7 +4,6 @@ import Document from '../nodes/document/Document.js'; import DocumentFragment from '../nodes/document-fragment/DocumentFragment.js'; import DOMRect from '../nodes/element/DOMRect.js'; import RangeHowEnum from './RangeHowEnum.js'; -import DOMException from '../exception/DOMException.js'; import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum.js'; import RangeUtility from './RangeUtility.js'; import NodeTypeEnum from '../nodes/node/NodeTypeEnum.js'; @@ -26,6 +25,9 @@ import BrowserWindow from '../window/BrowserWindow.js'; * https://developer.mozilla.org/en-US/docs/Web/API/Range. */ export default class Range { + // Injected by WindowClassExtender + protected declare [PropertySymbol.window]: BrowserWindow; + public static readonly END_TO_END: number = RangeHowEnum.endToEnd; public static readonly END_TO_START: number = RangeHowEnum.endToStart; public static readonly START_TO_END: number = RangeHowEnum.startToEnd; @@ -36,7 +38,6 @@ export default class Range { public readonly START_TO_START: number = RangeHowEnum.startToStart; public [PropertySymbol.start]: IRangeBoundaryPoint | null = null; public [PropertySymbol.end]: IRangeBoundaryPoint | null = null; - #window: BrowserWindow; public readonly [PropertySymbol.ownerDocument]: Document; /** @@ -44,8 +45,15 @@ export default class Range { * * @param window Window. */ - constructor(window: BrowserWindow) { - this.#window = window; + constructor() { + const window = this[PropertySymbol.window]; + + if (!window) { + throw new TypeError( + `Failed to construct '${this.constructor.name}': '${this.constructor.name}' was constructed outside a Window context.` + ); + } + this[PropertySymbol.ownerDocument] = window.document; this[PropertySymbol.start] = { node: window.document, offset: 0 }; this[PropertySymbol.end] = { node: window.document, offset: 0 }; @@ -166,14 +174,14 @@ export default class Range { how !== RangeHowEnum.endToEnd && how !== RangeHowEnum.endToStart ) { - throw new DOMException( + throw new this[PropertySymbol.window].DOMException( `The comparison method provided must be one of '${RangeHowEnum.startToStart}', '${RangeHowEnum.startToEnd}', '${RangeHowEnum.endToEnd}' or '${RangeHowEnum.endToStart}'.`, DOMExceptionNameEnum.notSupportedError ); } if (this[PropertySymbol.ownerDocument] !== sourceRange[PropertySymbol.ownerDocument]) { - throw new DOMException( + throw new this[PropertySymbol.window].DOMException( `The two Ranges are not in the same tree.`, DOMExceptionNameEnum.wrongDocumentError ); @@ -228,7 +236,7 @@ export default class Range { */ public comparePoint(node: Node, offset): number { if (node[PropertySymbol.ownerDocument] !== this[PropertySymbol.ownerDocument]) { - throw new DOMException( + throw new this[PropertySymbol.window].DOMException( `The two Ranges are not in the same tree.`, DOMExceptionNameEnum.wrongDocumentError ); @@ -264,6 +272,7 @@ export default class Range { * @returns Document fragment. */ public cloneContents(): DocumentFragment { + const window = this[PropertySymbol.window]; const fragment = this[PropertySymbol.ownerDocument].createDocumentFragment(); const startOffset = this.startOffset; const endOffset = this.endOffset; @@ -329,7 +338,7 @@ export default class Range { for (const node of (commonAncestor)[PropertySymbol.nodeArray]) { if (RangeUtility.isContained(node, this)) { if (node[PropertySymbol.nodeType] === NodeTypeEnum.documentTypeNode) { - throw new DOMException( + throw new window.DOMException( 'Invalid document type element.', DOMExceptionNameEnum.hierarchyRequestError ); @@ -356,7 +365,7 @@ export default class Range { const clone = firstPartialContainedChild.cloneNode(); fragment.appendChild(clone); - const subRange = new this.#window.Range(); + const subRange = new window.Range(); subRange[PropertySymbol.start].node = this[PropertySymbol.start].node; subRange[PropertySymbol.start].offset = startOffset; subRange[PropertySymbol.end].node = firstPartialContainedChild; @@ -386,7 +395,7 @@ export default class Range { const clone = lastPartiallyContainedChild.cloneNode(false); fragment.appendChild(clone); - const subRange = new this.#window.Range(); + const subRange = new window.Range(); subRange[PropertySymbol.start].node = lastPartiallyContainedChild; subRange[PropertySymbol.start].offset = 0; subRange[PropertySymbol.end].node = this[PropertySymbol.end].node; @@ -406,7 +415,7 @@ export default class Range { * @returns Range. */ public cloneRange(): Range { - const clone = new this.#window.Range(); + const clone = new this[PropertySymbol.window].Range(); clone[PropertySymbol.start].node = this[PropertySymbol.start].node; clone[PropertySymbol.start].offset = this[PropertySymbol.start].offset; @@ -549,6 +558,7 @@ export default class Range { * @returns Document fragment. */ public extractContents(): DocumentFragment { + const window = this[PropertySymbol.window]; const fragment = this[PropertySymbol.ownerDocument].createDocumentFragment(); const startOffset = this.startOffset; const endOffset = this.endOffset; @@ -622,7 +632,7 @@ export default class Range { for (const node of (commonAncestor)[PropertySymbol.nodeArray]) { if (RangeUtility.isContained(node, this)) { if (node[PropertySymbol.nodeType] === NodeTypeEnum.documentTypeNode) { - throw new DOMException( + throw new this[PropertySymbol.window].DOMException( 'Invalid document type element.', DOMExceptionNameEnum.hierarchyRequestError ); @@ -685,7 +695,7 @@ export default class Range { const clone = firstPartialContainedChild.cloneNode(false); fragment.appendChild(clone); - const subRange = new this.#window.Range(); + const subRange = new window.Range(); subRange[PropertySymbol.start].node = this[PropertySymbol.start].node; subRange[PropertySymbol.start].offset = startOffset; subRange[PropertySymbol.end].node = firstPartialContainedChild; @@ -716,7 +726,7 @@ export default class Range { const clone = lastPartiallyContainedChild.cloneNode(false); fragment.appendChild(clone); - const subRange = new this.#window.Range(); + const subRange = new window.Range(); subRange[PropertySymbol.start].node = lastPartiallyContainedChild; subRange[PropertySymbol.start].offset = 0; subRange[PropertySymbol.end].node = this[PropertySymbol.end].node; @@ -802,7 +812,10 @@ export default class Range { !this[PropertySymbol.start].node[PropertySymbol.parentNode]) || newNode === this[PropertySymbol.start].node ) { - throw new DOMException('Invalid start node.', DOMExceptionNameEnum.hierarchyRequestError); + throw new this[PropertySymbol.window].DOMException( + 'Invalid start node.', + DOMExceptionNameEnum.hierarchyRequestError + ); } let referenceNode = @@ -885,7 +898,7 @@ export default class Range { */ public selectNode(node: Node): void { if (!node[PropertySymbol.parentNode]) { - throw new DOMException( + throw new this[PropertySymbol.window].DOMException( `The given Node has no parent.`, DOMExceptionNameEnum.invalidNodeTypeError ); @@ -907,7 +920,7 @@ export default class Range { */ public selectNodeContents(node: Node): void { if (node[PropertySymbol.nodeType] === NodeTypeEnum.documentTypeNode) { - throw new DOMException( + throw new this[PropertySymbol.window].DOMException( "DocumentType Node can't be used as boundary point.", DOMExceptionNameEnum.invalidNodeTypeError ); @@ -981,7 +994,7 @@ export default class Range { */ public setEndAfter(node: Node): void { if (!node[PropertySymbol.parentNode]) { - throw new DOMException( + throw new this[PropertySymbol.window].DOMException( 'The given Node has no parent.', DOMExceptionNameEnum.invalidNodeTypeError ); @@ -1000,7 +1013,7 @@ export default class Range { */ public setEndBefore(node: Node): void { if (!node[PropertySymbol.parentNode]) { - throw new DOMException( + throw new this[PropertySymbol.window].DOMException( 'The given Node has no parent.', DOMExceptionNameEnum.invalidNodeTypeError ); @@ -1019,7 +1032,7 @@ export default class Range { */ public setStartAfter(node: Node): void { if (!node[PropertySymbol.parentNode]) { - throw new DOMException( + throw new this[PropertySymbol.window].DOMException( 'The given Node has no parent.', DOMExceptionNameEnum.invalidNodeTypeError ); @@ -1038,7 +1051,7 @@ export default class Range { */ public setStartBefore(node: Node): void { if (!node[PropertySymbol.parentNode]) { - throw new DOMException( + throw new this[PropertySymbol.window].DOMException( 'The given Node has no parent.', DOMExceptionNameEnum.invalidNodeTypeError ); @@ -1063,7 +1076,7 @@ export default class Range { node[PropertySymbol.nodeType] !== NodeTypeEnum.textNode && RangeUtility.isPartiallyContained(node, this) ) { - throw new DOMException( + throw new this[PropertySymbol.window].DOMException( 'The Range has partially contains a non-Text node.', DOMExceptionNameEnum.invalidStateError ); @@ -1077,7 +1090,10 @@ export default class Range { newParent[PropertySymbol.nodeType] === NodeTypeEnum.documentTypeNode || newParent[PropertySymbol.nodeType] === NodeTypeEnum.documentFragmentNode ) { - throw new DOMException('Invalid element type.', DOMExceptionNameEnum.invalidNodeTypeError); + throw new this[PropertySymbol.window].DOMException( + 'Invalid element type.', + DOMExceptionNameEnum.invalidNodeTypeError + ); } const fragment = this.extractContents(); diff --git a/packages/happy-dom/src/selection/Selection.ts b/packages/happy-dom/src/selection/Selection.ts index 0f8f2cf85..da641946d 100644 --- a/packages/happy-dom/src/selection/Selection.ts +++ b/packages/happy-dom/src/selection/Selection.ts @@ -1,6 +1,5 @@ import Event from '../event/Event.js'; import * as PropertySymbol from '../PropertySymbol.js'; -import DOMException from '../exception/DOMException.js'; import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum.js'; import Document from '../nodes/document/Document.js'; import Node from '../nodes/node/Node.js'; @@ -171,7 +170,9 @@ export default class Selection { */ public addRange(newRange: Range): void { if (!newRange) { - throw new Error('Failed to execute addRange on Selection. Parameter 1 is not of type Range.'); + throw new this.#ownerDocument[PropertySymbol.window].TypeError( + 'Failed to execute addRange on Selection. Parameter 1 is not of type Range.' + ); } if (!this.#range && newRange[PropertySymbol.ownerDocument] === this.#ownerDocument) { this.#associateRange(newRange); @@ -187,7 +188,10 @@ export default class Selection { */ public getRangeAt(index: number): Range { if (!this.#range || index !== 0) { - throw new DOMException('Invalid range index.', DOMExceptionNameEnum.indexSizeError); + throw new this.#ownerDocument[PropertySymbol.window].DOMException( + 'Invalid range index.', + DOMExceptionNameEnum.indexSizeError + ); } return this.#range; @@ -201,7 +205,10 @@ export default class Selection { */ public removeRange(range: Range): void { if (this.#range !== range) { - throw new DOMException('Invalid range.', DOMExceptionNameEnum.notFoundError); + throw new this.#ownerDocument[PropertySymbol.window].DOMException( + 'Invalid range.', + DOMExceptionNameEnum.notFoundError + ); } this.#associateRange(null); } @@ -236,21 +243,24 @@ export default class Selection { } if (node[PropertySymbol.nodeType] === NodeTypeEnum.documentTypeNode) { - throw new DOMException( + throw new this.#ownerDocument[PropertySymbol.window].DOMException( "DocumentType Node can't be used as boundary point.", DOMExceptionNameEnum.invalidNodeTypeError ); } if (offset > NodeUtility.getNodeLength(node)) { - throw new DOMException('Invalid range index.', DOMExceptionNameEnum.indexSizeError); + throw new this.#ownerDocument[PropertySymbol.window].DOMException( + 'Invalid range index.', + DOMExceptionNameEnum.indexSizeError + ); } if (node[PropertySymbol.ownerDocument] !== this.#ownerDocument) { return; } - const newRange = new this.#ownerDocument[PropertySymbol.ownerWindow].Range(); + const newRange = new this.#ownerDocument[PropertySymbol.window].Range(); newRange[PropertySymbol.start].node = node; newRange[PropertySymbol.start].offset = offset; @@ -279,14 +289,14 @@ export default class Selection { */ public collapseToEnd(): void { if (this.#range === null) { - throw new DOMException( + throw new this.#ownerDocument[PropertySymbol.window].DOMException( 'There is no selection to collapse.', DOMExceptionNameEnum.invalidStateError ); } const { node, offset } = this.#range[PropertySymbol.end]; - const newRange = new this.#ownerDocument[PropertySymbol.ownerWindow].Range(); + const newRange = new this.#ownerDocument[PropertySymbol.window].Range(); newRange[PropertySymbol.start].node = node; newRange[PropertySymbol.start].offset = offset; @@ -303,14 +313,14 @@ export default class Selection { */ public collapseToStart(): void { if (!this.#range) { - throw new DOMException( + throw new this.#ownerDocument[PropertySymbol.window].DOMException( 'There is no selection to collapse.', DOMExceptionNameEnum.invalidStateError ); } const { node, offset } = this.#range[PropertySymbol.start]; - const newRange = new this.#ownerDocument[PropertySymbol.ownerWindow].Range(); + const newRange = new this.#ownerDocument[PropertySymbol.window].Range(); newRange[PropertySymbol.start].node = node; newRange[PropertySymbol.start].offset = offset; @@ -373,7 +383,7 @@ export default class Selection { } if (!this.#range) { - throw new DOMException( + throw new this.#ownerDocument[PropertySymbol.window].DOMException( 'There is no selection to extend.', DOMExceptionNameEnum.invalidStateError ); @@ -381,7 +391,7 @@ export default class Selection { const anchorNode = this.anchorNode; const anchorOffset = this.anchorOffset; - const newRange = new this.#ownerDocument[PropertySymbol.ownerWindow].Range(); + const newRange = new this.#ownerDocument[PropertySymbol.window].Range(); newRange[PropertySymbol.start].node = node; newRange[PropertySymbol.start].offset = 0; newRange[PropertySymbol.end].node = node; @@ -425,7 +435,7 @@ export default class Selection { */ public selectAllChildren(node: Node): void { if (node[PropertySymbol.nodeType] === NodeTypeEnum.documentTypeNode) { - throw new DOMException( + throw new this.#ownerDocument[PropertySymbol.window].DOMException( "DocumentType Node can't be used as boundary point.", DOMExceptionNameEnum.invalidNodeTypeError ); @@ -436,7 +446,7 @@ export default class Selection { } const length = node[PropertySymbol.nodeArray].length; - const newRange = new this.#ownerDocument[PropertySymbol.ownerWindow].Range(); + const newRange = new this.#ownerDocument[PropertySymbol.window].Range(); newRange[PropertySymbol.start].node = node; newRange[PropertySymbol.start].offset = 0; @@ -465,7 +475,7 @@ export default class Selection { anchorOffset > NodeUtility.getNodeLength(anchorNode) || focusOffset > NodeUtility.getNodeLength(focusNode) ) { - throw new DOMException( + throw new this.#ownerDocument[PropertySymbol.window].DOMException( 'Invalid anchor or focus offset.', DOMExceptionNameEnum.indexSizeError ); @@ -480,7 +490,7 @@ export default class Selection { const anchor = { node: anchorNode, offset: anchorOffset }; const focus = { node: focusNode, offset: focusOffset }; - const newRange = new this.#ownerDocument[PropertySymbol.ownerWindow].Range(); + const newRange = new this.#ownerDocument[PropertySymbol.window].Range(); if (RangeUtility.compareBoundaryPointsPosition(anchor, focus) === -1) { newRange[PropertySymbol.start] = anchor; diff --git a/packages/happy-dom/src/window/BrowserWindow.ts b/packages/happy-dom/src/window/BrowserWindow.ts index 1ef95fab0..6239fbcc1 100644 --- a/packages/happy-dom/src/window/BrowserWindow.ts +++ b/packages/happy-dom/src/window/BrowserWindow.ts @@ -6,7 +6,6 @@ import { URLSearchParams } from 'url'; import VM from 'vm'; import ClassMethodBinder from '../ClassMethodBinder.js'; import * as PropertySymbol from '../PropertySymbol.js'; -import AsyncTaskManager from '../async-task-manager/AsyncTaskManager.js'; import Base64 from '../base64/Base64.js'; import BrowserErrorCaptureEnum from '../browser/enums/BrowserErrorCaptureEnum.js'; import IBrowserFrame from '../browser/types/IBrowserFrame.js'; @@ -25,7 +24,7 @@ import CSSMediaRule from '../css/rules/CSSMediaRule.js'; import CSSStyleRule from '../css/rules/CSSStyleRule.js'; import CSSSupportsRule from '../css/rules/CSSSupportsRule.js'; import CustomElementRegistry from '../custom-element/CustomElementRegistry.js'; -import DOMParserImplementation from '../dom-parser/DOMParser.js'; +import DOMParser from '../dom-parser/DOMParser.js'; import DataTransfer from '../event/DataTransfer.js'; import DataTransferItem from '../event/DataTransferItem.js'; import DataTransferItemList from '../event/DataTransferItemList.js'; @@ -57,15 +56,13 @@ import AbortController from '../fetch/AbortController.js'; import AbortSignal from '../fetch/AbortSignal.js'; import Fetch from '../fetch/Fetch.js'; import Headers from '../fetch/Headers.js'; -import RequestImplementation from '../fetch/Request.js'; -import { default as Response, default as ResponseImplementation } from '../fetch/Response.js'; +import Request from '../fetch/Request.js'; +import Response from '../fetch/Response.js'; import IRequestInfo from '../fetch/types/IRequestInfo.js'; import IRequestInit from '../fetch/types/IRequestInit.js'; -import IResponseBody from '../fetch/types/IResponseBody.js'; -import IResponseInit from '../fetch/types/IResponseInit.js'; import Blob from '../file/Blob.js'; import File from '../file/File.js'; -import FileReaderImplementation from '../file/FileReader.js'; +import FileReader from '../file/FileReader.js'; import FormData from '../form-data/FormData.js'; import History from '../history/History.js'; import IntersectionObserver from '../intersection-observer/IntersectionObserver.js'; @@ -81,10 +78,10 @@ import Plugin from '../navigator/Plugin.js'; import PluginArray from '../navigator/PluginArray.js'; import Attr from '../nodes/attr/Attr.js'; import CharacterData from '../nodes/character-data/CharacterData.js'; -import CommentImplementation from '../nodes/comment/Comment.js'; -import DocumentFragmentImplementation from '../nodes/document-fragment/DocumentFragment.js'; +import Comment from '../nodes/comment/Comment.js'; +import DocumentFragment from '../nodes/document-fragment/DocumentFragment.js'; import DocumentType from '../nodes/document-type/DocumentType.js'; -import DocumentImplementation from '../nodes/document/Document.js'; +import Document from '../nodes/document/Document.js'; import DocumentReadyStateEnum from '../nodes/document/DocumentReadyStateEnum.js'; import DocumentReadyStateManager from '../nodes/document/DocumentReadyStateManager.js'; import DOMRect from '../nodes/element/DOMRect.js'; @@ -94,7 +91,7 @@ import HTMLCollection from '../nodes/element/HTMLCollection.js'; import NamedNodeMap from '../nodes/element/NamedNodeMap.js'; import HTMLAnchorElement from '../nodes/html-anchor-element/HTMLAnchorElement.js'; import HTMLAreaElement from '../nodes/html-area-element/HTMLAreaElement.js'; -import AudioImplementation from '../nodes/html-audio-element/Audio.js'; +import Audio from '../nodes/html-audio-element/Audio.js'; import HTMLAudioElement from '../nodes/html-audio-element/HTMLAudioElement.js'; import HTMLBaseElement from '../nodes/html-base-element/HTMLBaseElement.js'; import HTMLBodyElement from '../nodes/html-body-element/HTMLBodyElement.js'; @@ -107,26 +104,26 @@ import HTMLDataListElement from '../nodes/html-data-list-element/HTMLDataListEle import HTMLDetailsElement from '../nodes/html-details-element/HTMLDetailsElement.js'; import HTMLDialogElement from '../nodes/html-dialog-element/HTMLDialogElement.js'; import HTMLDivElement from '../nodes/html-div-element/HTMLDivElement.js'; -import HTMLDocumentImplementation from '../nodes/html-document/HTMLDocument.js'; +import HTMLDocument from '../nodes/html-document/HTMLDocument.js'; import HTMLElement from '../nodes/html-element/HTMLElement.js'; import HTMLEmbedElement from '../nodes/html-embed-element/HTMLEmbedElement.js'; import HTMLFieldSetElement from '../nodes/html-field-set-element/HTMLFieldSetElement.js'; import HTMLFormControlsCollection from '../nodes/html-form-element/HTMLFormControlsCollection.js'; -import HTMLFormElementImplementation from '../nodes/html-form-element/HTMLFormElement.js'; +import HTMLFormElement from '../nodes/html-form-element/HTMLFormElement.js'; import RadioNodeList from '../nodes/html-form-element/RadioNodeList.js'; import HTMLHeadElement from '../nodes/html-head-element/HTMLHeadElement.js'; import HTMLHeadingElement from '../nodes/html-heading-element/HTMLHeadingElement.js'; import HTMLHRElement from '../nodes/html-hr-element/HTMLHRElement.js'; import HTMLHtmlElement from '../nodes/html-html-element/HTMLHtmlElement.js'; -import HTMLIFrameElementImplementation from '../nodes/html-iframe-element/HTMLIFrameElement.js'; +import HTMLIFrameElement from '../nodes/html-iframe-element/HTMLIFrameElement.js'; import HTMLImageElement from '../nodes/html-image-element/HTMLImageElement.js'; -import ImageImplementation from '../nodes/html-image-element/Image.js'; +import Image from '../nodes/html-image-element/Image.js'; import FileList from '../nodes/html-input-element/FileList.js'; import HTMLInputElement from '../nodes/html-input-element/HTMLInputElement.js'; import HTMLLabelElement from '../nodes/html-label-element/HTMLLabelElement.js'; import HTMLLegendElement from '../nodes/html-legend-element/HTMLLegendElement.js'; import HTMLLIElement from '../nodes/html-li-element/HTMLLIElement.js'; -import HTMLLinkElementImplementation from '../nodes/html-link-element/HTMLLinkElement.js'; +import HTMLLinkElement from '../nodes/html-link-element/HTMLLinkElement.js'; import HTMLMapElement from '../nodes/html-map-element/HTMLMapElement.js'; import HTMLMediaElement from '../nodes/html-media-element/HTMLMediaElement.js'; import MediaStream from '../nodes/html-media-element/MediaStream.js'; @@ -137,7 +134,7 @@ import TextTrackCue from '../nodes/html-media-element/TextTrackCue.js'; import TextTrackCueList from '../nodes/html-media-element/TextTrackCueList.js'; import TextTrackList from '../nodes/html-media-element/TextTrackList.js'; import TimeRanges from '../nodes/html-media-element/TimeRanges.js'; -import VTTCueImplementation from '../nodes/html-media-element/VTTCue.js'; +import VTTCue from '../nodes/html-media-element/VTTCue.js'; import HTMLMenuElement from '../nodes/html-menu-element/HTMLMenuElement.js'; import HTMLMetaElement from '../nodes/html-meta-element/HTMLMetaElement.js'; import HTMLMeterElement from '../nodes/html-meter-element/HTMLMeterElement.js'; @@ -153,7 +150,7 @@ import HTMLPictureElement from '../nodes/html-picture-element/HTMLPictureElement import HTMLPreElement from '../nodes/html-pre-element/HTMLPreElement.js'; import HTMLProgressElement from '../nodes/html-progress-element/HTMLProgressElement.js'; import HTMLQuoteElement from '../nodes/html-quote-element/HTMLQuoteElement.js'; -import HTMLScriptElementImplementation from '../nodes/html-script-element/HTMLScriptElement.js'; +import HTMLScriptElement from '../nodes/html-script-element/HTMLScriptElement.js'; import HTMLSelectElement from '../nodes/html-select-element/HTMLSelectElement.js'; import HTMLSlotElement from '../nodes/html-slot-element/HTMLSlotElement.js'; import HTMLSourceElement from '../nodes/html-source-element/HTMLSourceElement.js'; @@ -177,15 +174,15 @@ import Node from '../nodes/node/Node.js'; import NodeList from '../nodes/node/NodeList.js'; import ProcessingInstruction from '../nodes/processing-instruction/ProcessingInstruction.js'; import ShadowRoot from '../nodes/shadow-root/ShadowRoot.js'; -import SVGDocumentImplementation from '../nodes/svg-document/SVGDocument.js'; +import SVGDocument from '../nodes/svg-document/SVGDocument.js'; import SVGElement from '../nodes/svg-element/SVGElement.js'; import SVGGraphicsElement from '../nodes/svg-element/SVGGraphicsElement.js'; import SVGSVGElement from '../nodes/svg-element/SVGSVGElement.js'; -import TextImplementation from '../nodes/text/Text.js'; -import XMLDocumentImplementation from '../nodes/xml-document/XMLDocument.js'; +import Text from '../nodes/text/Text.js'; +import XMLDocument from '../nodes/xml-document/XMLDocument.js'; import PermissionStatus from '../permissions/PermissionStatus.js'; import Permissions from '../permissions/Permissions.js'; -import RangeImplementation from '../range/Range.js'; +import Range from '../range/Range.js'; import ResizeObserver from '../resize-observer/ResizeObserver.js'; import Screen from '../screen/Screen.js'; import Selection from '../selection/Selection.js'; @@ -195,17 +192,25 @@ import NodeIterator from '../tree-walker/NodeIterator.js'; import TreeWalker from '../tree-walker/TreeWalker.js'; import URL from '../url/URL.js'; import ValidityState from '../validity-state/ValidityState.js'; -import XMLHttpRequestImplementation from '../xml-http-request/XMLHttpRequest.js'; +import XMLHttpRequest from '../xml-http-request/XMLHttpRequest.js'; import XMLHttpRequestEventTarget from '../xml-http-request/XMLHttpRequestEventTarget.js'; import XMLHttpRequestUpload from '../xml-http-request/XMLHttpRequestUpload.js'; import XMLSerializer from '../xml-serializer/XMLSerializer.js'; import CrossOriginBrowserWindow from './CrossOriginBrowserWindow.js'; import INodeJSGlobal from './INodeJSGlobal.js'; import VMGlobalPropertyScript from './VMGlobalPropertyScript.js'; -import WindowBrowserSettingsReader from './WindowBrowserSettingsReader.js'; import WindowErrorUtility from './WindowErrorUtility.js'; import WindowPageOpenUtility from './WindowPageOpenUtility.js'; - +import { + PerformanceObserver, + PerformanceEntry, + PerformanceObserverEntryList as IPerformanceObserverEntryList +} from 'node:perf_hooks'; +import EventPhaseEnum from '../event/EventPhaseEnum.js'; +import HTMLOptionsCollection from '../nodes/html-select-element/HTMLOptionsCollection.js'; +import WindowClassExtender from './WindowClassExtender.js'; +import WindowBrowserContext from './WindowBrowserContext.js'; +import CanvasCaptureMediaStreamTrack from '../nodes/html-canvas-element/CanvasCaptureMediaStreamTrack.js'; const TIMER = { setTimeout: globalThis.setTimeout.bind(globalThis), clearTimeout: globalThis.clearTimeout.bind(globalThis), @@ -216,6 +221,18 @@ const TIMER = { clearImmediate: globalThis.clearImmediate.bind(globalThis) }; const IS_NODE_JS_TIMEOUT_ENVIRONMENT = setTimeout.toString().includes('new Timeout'); + +/** + * Class for PerformanceObserverEntryList as it is only available as an interface from Node.js. + */ +class PerformanceObserverEntryList { + /** + * Constructor. + */ + constructor() { + throw new TypeError('Illegal constructor'); + } +} /** * Zero Timeout. */ @@ -238,93 +255,95 @@ class Timeout { */ export default class BrowserWindow extends EventTarget implements INodeJSGlobal { // Nodes - public readonly Node: typeof Node = Node; - public readonly Attr: typeof Attr = Attr; - public readonly SVGSVGElement: typeof SVGSVGElement = SVGSVGElement; - public readonly SVGElement: typeof SVGElement = SVGElement; - public readonly SVGGraphicsElement: typeof SVGGraphicsElement = SVGGraphicsElement; - public readonly ShadowRoot: typeof ShadowRoot = ShadowRoot; - public readonly ProcessingInstruction: typeof ProcessingInstruction = ProcessingInstruction; - public readonly Element: typeof Element = Element; - public readonly CharacterData: typeof CharacterData = CharacterData; - public readonly DocumentType: typeof DocumentType = DocumentType; - public readonly Document: new () => DocumentImplementation; - public readonly HTMLDocument: new () => HTMLDocumentImplementation; - public readonly XMLDocument: new () => XMLDocumentImplementation; - public readonly SVGDocument: new () => SVGDocumentImplementation; - public readonly Text: typeof TextImplementation; - public readonly Comment: typeof CommentImplementation; - public readonly Image: typeof ImageImplementation; - public readonly DocumentFragment: typeof DocumentFragmentImplementation; - public readonly Audio: typeof AudioImplementation; + public readonly Node = Node; + public readonly Attr = Attr; + public readonly SVGSVGElement = SVGSVGElement; + public readonly SVGElement = SVGElement; + public readonly SVGGraphicsElement = SVGGraphicsElement; + public readonly ShadowRoot = ShadowRoot; + public readonly ProcessingInstruction = ProcessingInstruction; + public readonly Element = Element; + public readonly CharacterData = CharacterData; + public readonly DocumentType = DocumentType; + public readonly Document = Document; + public readonly HTMLDocument = HTMLDocument; + public readonly XMLDocument = XMLDocument; + public readonly SVGDocument = SVGDocument; + + // Nodes that can be created using "new" keyword (populated by WindowClassExtender) + public declare readonly DocumentFragment: typeof DocumentFragment; + public declare readonly Text: typeof Text; + public declare readonly Comment: typeof Comment; + public declare readonly Image: typeof Image; + public declare readonly Audio: typeof Audio; // Element classes - public readonly HTMLAnchorElement: typeof HTMLAnchorElement = HTMLAnchorElement; - public readonly HTMLButtonElement: typeof HTMLButtonElement = HTMLButtonElement; - public readonly HTMLOptGroupElement: typeof HTMLOptGroupElement = HTMLOptGroupElement; - public readonly HTMLOptionElement: typeof HTMLOptionElement = HTMLOptionElement; - public readonly HTMLElement: typeof HTMLElement = HTMLElement; - public readonly HTMLUnknownElement: typeof HTMLUnknownElement = HTMLUnknownElement; - public readonly HTMLTemplateElement: typeof HTMLTemplateElement = HTMLTemplateElement; - public readonly HTMLInputElement: typeof HTMLInputElement = HTMLInputElement; - public readonly HTMLSelectElement: typeof HTMLSelectElement = HTMLSelectElement; - public readonly HTMLTextAreaElement: typeof HTMLTextAreaElement = HTMLTextAreaElement; - public readonly HTMLImageElement: typeof HTMLImageElement = HTMLImageElement; - public readonly HTMLStyleElement: typeof HTMLStyleElement = HTMLStyleElement; - public readonly HTMLLabelElement: typeof HTMLLabelElement = HTMLLabelElement; - public readonly HTMLSlotElement: typeof HTMLSlotElement = HTMLSlotElement; - public readonly HTMLMetaElement: typeof HTMLMetaElement = HTMLMetaElement; - public readonly HTMLMediaElement: typeof HTMLMediaElement = HTMLMediaElement; - public readonly HTMLAudioElement: typeof HTMLAudioElement = HTMLAudioElement; - public readonly HTMLVideoElement: typeof HTMLVideoElement = HTMLVideoElement; - public readonly HTMLBaseElement: typeof HTMLBaseElement = HTMLBaseElement; - public readonly HTMLDialogElement: typeof HTMLDialogElement = HTMLDialogElement; - public readonly HTMLScriptElement: typeof HTMLScriptElementImplementation; - public readonly HTMLLinkElement: typeof HTMLLinkElementImplementation; - public readonly HTMLIFrameElement: typeof HTMLIFrameElementImplementation; - public readonly HTMLFormElement: typeof HTMLFormElementImplementation; - public readonly HTMLUListElement: typeof HTMLUListElement = HTMLUListElement; - public readonly HTMLTrackElement: typeof HTMLTrackElement = HTMLTrackElement; - public readonly HTMLTableRowElement: typeof HTMLTableRowElement = HTMLTableRowElement; - public readonly HTMLTitleElement: typeof HTMLTitleElement = HTMLTitleElement; - public readonly HTMLTimeElement: typeof HTMLTimeElement = HTMLTimeElement; - public readonly HTMLTableSectionElement: typeof HTMLTableSectionElement = HTMLTableSectionElement; - public readonly HTMLTableCellElement: typeof HTMLTableCellElement = HTMLTableCellElement; - public readonly HTMLTableElement: typeof HTMLTableElement = HTMLTableElement; - public readonly HTMLSpanElement: typeof HTMLSpanElement = HTMLSpanElement; - public readonly HTMLSourceElement: typeof HTMLSourceElement = HTMLSourceElement; - public readonly HTMLQuoteElement: typeof HTMLQuoteElement = HTMLQuoteElement; - public readonly HTMLProgressElement: typeof HTMLProgressElement = HTMLProgressElement; - public readonly HTMLPreElement: typeof HTMLPreElement = HTMLPreElement; - public readonly HTMLPictureElement: typeof HTMLPictureElement = HTMLPictureElement; - public readonly HTMLParamElement: typeof HTMLParamElement = HTMLParamElement; - public readonly HTMLParagraphElement: typeof HTMLParagraphElement = HTMLParagraphElement; - public readonly HTMLOutputElement: typeof HTMLOutputElement = HTMLOutputElement; - public readonly HTMLOListElement: typeof HTMLOListElement = HTMLOListElement; - public readonly HTMLObjectElement: typeof HTMLObjectElement = HTMLObjectElement; - public readonly HTMLMeterElement: typeof HTMLMeterElement = HTMLMeterElement; - public readonly HTMLMenuElement: typeof HTMLMenuElement = HTMLMenuElement; - public readonly HTMLMapElement: typeof HTMLMapElement = HTMLMapElement; - public readonly HTMLLIElement: typeof HTMLLIElement = HTMLLIElement; - public readonly HTMLLegendElement: typeof HTMLLegendElement = HTMLLegendElement; - public readonly HTMLModElement: typeof HTMLModElement = HTMLModElement; - public readonly HTMLHtmlElement: typeof HTMLHtmlElement = HTMLHtmlElement; - public readonly HTMLHRElement: typeof HTMLHRElement = HTMLHRElement; - public readonly HTMLHeadElement: typeof HTMLHeadElement = HTMLHeadElement; - public readonly HTMLHeadingElement: typeof HTMLHeadingElement = HTMLHeadingElement; - public readonly HTMLFieldSetElement: typeof HTMLFieldSetElement = HTMLFieldSetElement; - public readonly HTMLEmbedElement: typeof HTMLEmbedElement = HTMLEmbedElement; - public readonly HTMLDListElement: typeof HTMLDListElement = HTMLDListElement; - public readonly HTMLDivElement: typeof HTMLDivElement = HTMLDivElement; - public readonly HTMLDetailsElement: typeof HTMLDetailsElement = HTMLDetailsElement; - public readonly HTMLDataListElement: typeof HTMLDataListElement = HTMLDataListElement; - public readonly HTMLDataElement: typeof HTMLDataElement = HTMLDataElement; - public readonly HTMLTableColElement: typeof HTMLTableColElement = HTMLTableColElement; - public readonly HTMLTableCaptionElement: typeof HTMLTableCaptionElement = HTMLTableCaptionElement; - public readonly HTMLCanvasElement: typeof HTMLCanvasElement = HTMLCanvasElement; - public readonly HTMLBRElement: typeof HTMLBRElement = HTMLBRElement; - public readonly HTMLBodyElement: typeof HTMLBodyElement = HTMLBodyElement; - public readonly HTMLAreaElement: typeof HTMLAreaElement = HTMLAreaElement; + public readonly HTMLAnchorElement = HTMLAnchorElement; + public readonly HTMLButtonElement = HTMLButtonElement; + public readonly HTMLOptGroupElement = HTMLOptGroupElement; + public readonly HTMLOptionElement = HTMLOptionElement; + public readonly HTMLElement = HTMLElement; + public readonly HTMLUnknownElement = HTMLUnknownElement; + public readonly HTMLTemplateElement = HTMLTemplateElement; + public readonly HTMLInputElement = HTMLInputElement; + public readonly HTMLSelectElement = HTMLSelectElement; + public readonly HTMLTextAreaElement = HTMLTextAreaElement; + public readonly HTMLImageElement = HTMLImageElement; + public readonly HTMLStyleElement = HTMLStyleElement; + public readonly HTMLLabelElement = HTMLLabelElement; + public readonly HTMLSlotElement = HTMLSlotElement; + public readonly HTMLMetaElement = HTMLMetaElement; + public readonly HTMLMediaElement = HTMLMediaElement; + public readonly HTMLAudioElement = HTMLAudioElement; + public readonly HTMLVideoElement = HTMLVideoElement; + public readonly HTMLBaseElement = HTMLBaseElement; + public readonly HTMLDialogElement = HTMLDialogElement; + public readonly HTMLScriptElement = HTMLScriptElement; + public readonly HTMLLinkElement = HTMLLinkElement; + public readonly HTMLIFrameElement = HTMLIFrameElement; + public readonly HTMLFormElement = HTMLFormElement; + public readonly HTMLUListElement = HTMLUListElement; + public readonly HTMLTrackElement = HTMLTrackElement; + public readonly HTMLTableRowElement = HTMLTableRowElement; + public readonly HTMLTitleElement = HTMLTitleElement; + public readonly HTMLTimeElement = HTMLTimeElement; + public readonly HTMLTableSectionElement = HTMLTableSectionElement; + public readonly HTMLTableCellElement = HTMLTableCellElement; + public readonly HTMLTableElement = HTMLTableElement; + public readonly HTMLSpanElement = HTMLSpanElement; + public readonly HTMLSourceElement = HTMLSourceElement; + public readonly HTMLQuoteElement = HTMLQuoteElement; + public readonly HTMLProgressElement = HTMLProgressElement; + public readonly HTMLPreElement = HTMLPreElement; + public readonly HTMLPictureElement = HTMLPictureElement; + public readonly HTMLParamElement = HTMLParamElement; + public readonly HTMLParagraphElement = HTMLParagraphElement; + public readonly HTMLOutputElement = HTMLOutputElement; + public readonly HTMLOListElement = HTMLOListElement; + public readonly HTMLObjectElement = HTMLObjectElement; + public readonly HTMLMeterElement = HTMLMeterElement; + public readonly HTMLMenuElement = HTMLMenuElement; + public readonly HTMLMapElement = HTMLMapElement; + public readonly HTMLLIElement = HTMLLIElement; + public readonly HTMLLegendElement = HTMLLegendElement; + public readonly HTMLModElement = HTMLModElement; + public readonly HTMLHtmlElement = HTMLHtmlElement; + public readonly HTMLHRElement = HTMLHRElement; + public readonly HTMLHeadElement = HTMLHeadElement; + public readonly HTMLHeadingElement = HTMLHeadingElement; + public readonly HTMLFieldSetElement = HTMLFieldSetElement; + public readonly HTMLEmbedElement = HTMLEmbedElement; + public readonly HTMLDListElement = HTMLDListElement; + public readonly HTMLDivElement = HTMLDivElement; + public readonly HTMLDetailsElement = HTMLDetailsElement; + public readonly HTMLDataListElement = HTMLDataListElement; + public readonly HTMLDataElement = HTMLDataElement; + public readonly HTMLTableColElement = HTMLTableColElement; + public readonly HTMLTableCaptionElement = HTMLTableCaptionElement; + public readonly HTMLCanvasElement = HTMLCanvasElement; + public readonly HTMLBRElement = HTMLBRElement; + public readonly HTMLBodyElement = HTMLBodyElement; + public readonly HTMLAreaElement = HTMLAreaElement; // Event classes public readonly Event = Event; @@ -388,39 +407,56 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal public readonly WebGLContextEvent = Event; public readonly TextEvent = Event; - // Other classes - public readonly NamedNodeMap = NamedNodeMap; - public readonly NodeFilter = NodeFilter; - public readonly NodeIterator = NodeIterator; - public readonly TreeWalker = TreeWalker; - public readonly MutationObserver = MutationObserver; + // Other classes that has to be bound to the Window context (populated by WindowClassExtender) + public declare readonly NodeIterator: typeof NodeIterator; + public declare readonly TreeWalker: typeof TreeWalker; + public declare readonly MutationObserver: typeof MutationObserver; + public declare readonly CSSStyleDeclaration: typeof CSSStyleDeclaration; + public declare readonly MessagePort: typeof MessagePort; + public declare readonly DataTransfer: typeof DataTransfer; + public declare readonly DataTransferItem: typeof DataTransferItem; + public declare readonly DataTransferItemList: typeof DataTransferItemList; + public declare readonly XMLSerializer: typeof XMLSerializer; + public declare readonly CSSStyleSheet: typeof CSSStyleSheet; + public declare readonly DOMException: typeof DOMException; + public declare readonly CSSUnitValue: typeof CSSUnitValue; + public declare readonly Selection: typeof Selection; + public declare readonly Headers: typeof Headers; + public declare readonly Request: typeof Request; + public declare readonly Response: typeof Response; + public declare readonly EventTarget: typeof EventTarget; + public declare readonly XMLHttpRequestUpload: typeof XMLHttpRequestUpload; + public declare readonly XMLHttpRequestEventTarget: typeof XMLHttpRequestEventTarget; + public declare readonly AbortController: typeof AbortController; + public declare readonly AbortSignal: typeof AbortSignal; + public declare readonly FormData: typeof FormData; + public declare readonly PermissionStatus: typeof PermissionStatus; + public declare readonly ClipboardItem: typeof ClipboardItem; + public declare readonly XMLHttpRequest: typeof XMLHttpRequest; + public declare readonly DOMParser: typeof DOMParser; + public declare readonly Range: typeof Range; + public declare readonly VTTCue: typeof VTTCue; + public declare readonly FileReader: typeof FileReader; + public declare readonly MediaStream: typeof MediaStream; + public declare readonly MediaStreamTrack: typeof MediaStreamTrack; + public declare readonly CanvasCaptureMediaStreamTrack: typeof CanvasCaptureMediaStreamTrack; + public declare readonly NamedNodeMap: typeof NamedNodeMap; + public declare readonly TextTrack: typeof TextTrack; + public declare readonly TextTrackList: typeof TextTrackList; + public declare readonly TextTrackCue: typeof TextTrackCue; + public declare readonly RemotePlayback: typeof RemotePlayback; + + // Other classes that don't have to be bound to the Window context + public readonly Permissions = Permissions; + public readonly History = History; + public readonly Navigator = Navigator; + public readonly Clipboard = Clipboard; + public readonly TimeRanges = TimeRanges; + public readonly TextTrackCueList = TextTrackCueList; + public readonly ValidityState = ValidityState; + public readonly MutationRecord = MutationRecord; public readonly IntersectionObserver = IntersectionObserver; public readonly IntersectionObserverEntry = IntersectionObserverEntry; - public readonly MutationRecord = MutationRecord; - public readonly CSSStyleDeclaration = CSSStyleDeclaration; - public readonly EventTarget = EventTarget; - public readonly MessagePort = MessagePort; - public readonly DataTransfer = DataTransfer; - public readonly DataTransferItem = DataTransferItem; - public readonly DataTransferItemList = DataTransferItemList; - public readonly URL = URL; - public readonly Location = Location; - public readonly CustomElementRegistry = CustomElementRegistry; - public readonly Window = this.constructor; - public readonly XMLSerializer = XMLSerializer; - public readonly ResizeObserver = ResizeObserver; - public readonly CSSStyleSheet = CSSStyleSheet; - public readonly Blob = Blob; - public readonly File = File; - public readonly DOMException = DOMException; - public readonly History = History; - public readonly Screen = Screen; - public readonly Storage = Storage; - public readonly URLSearchParams = URLSearchParams; - public readonly HTMLCollection = HTMLCollection; - public readonly HTMLFormControlsCollection = HTMLFormControlsCollection; - public readonly NodeList = NodeList; - public readonly CSSUnitValue = CSSUnitValue; public readonly CSSRule = CSSRule; public readonly CSSContainerRule = CSSContainerRule; public readonly CSSFontFaceRule = CSSFontFaceRule; @@ -429,61 +465,46 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal public readonly CSSMediaRule = CSSMediaRule; public readonly CSSStyleRule = CSSStyleRule; public readonly CSSSupportsRule = CSSSupportsRule; - public readonly Selection = Selection; - public readonly Navigator = Navigator; - public readonly MimeType = MimeType; - public readonly MimeTypeArray = MimeTypeArray; - public readonly Plugin = Plugin; - public readonly PluginArray = PluginArray; - public readonly FileList = FileList; public readonly DOMRect = DOMRect; public readonly DOMRectReadOnly = DOMRectReadOnly; + public readonly Plugin = Plugin; + public readonly PluginArray = PluginArray; + public readonly Location = Location; + public readonly CustomElementRegistry = CustomElementRegistry; + public readonly ResizeObserver = ResizeObserver; + public readonly URL = URL; + public readonly Blob = Blob; + public readonly File = File; + public readonly Storage = Storage; + public readonly MimeType = MimeType; + public readonly MimeTypeArray = MimeTypeArray; + public readonly NodeFilter = NodeFilter; + public readonly HTMLCollection = HTMLCollection; + public readonly HTMLFormControlCollection = HTMLFormControlsCollection; + public readonly HTMLOptionsCollection = HTMLOptionsCollection; + public readonly NodeList = NodeList; public readonly RadioNodeList = RadioNodeList; - public readonly ValidityState = ValidityState; - public readonly Headers = Headers; - public readonly Request: new (input: IRequestInfo, init?: IRequestInit) => RequestImplementation; - public readonly Response: { - redirect: (url: string, status?: number) => ResponseImplementation; - error: () => ResponseImplementation; - json: (data: object, init?: IResponseInit) => ResponseImplementation; - new (body?: IResponseBody, init?: IResponseInit): ResponseImplementation; - }; - public readonly XMLHttpRequestUpload = XMLHttpRequestUpload; - public readonly XMLHttpRequestEventTarget = XMLHttpRequestEventTarget; - public readonly ReadableStream = ReadableStream; + public readonly FileList = FileList; + public readonly Screen = Screen; + public readonly Window = this.constructor; + + // Node.js Classes + public readonly URLSearchParams = URLSearchParams; public readonly WritableStream = Stream.Writable; + public readonly ReadableStream = ReadableStream; public readonly TransformStream = Stream.Transform; - public readonly AbortController = AbortController; - public readonly AbortSignal = AbortSignal; - public readonly FormData = FormData; - public readonly Permissions = Permissions; - public readonly PermissionStatus = PermissionStatus; - public readonly Clipboard = Clipboard; - public readonly ClipboardItem = ClipboardItem; - public readonly XMLHttpRequest: new () => XMLHttpRequestImplementation; - public readonly DOMParser: new () => DOMParserImplementation; - public readonly Range: new () => RangeImplementation; - public readonly VTTCue: new ( - startTime: number, - endTime: number, - text: string - ) => VTTCueImplementation; - public readonly TextTrack = TextTrack; - public readonly TextTrackCue = TextTrackCue; - public readonly TextTrackCueList = TextTrackCueList; - public readonly FileReader: new () => FileReaderImplementation; - public readonly TimeRanges = TimeRanges; - public readonly RemotePlayback = RemotePlayback; - public readonly MediaStream = MediaStream; - public readonly MediaStreamTrack = MediaStreamTrack; - public readonly TextTrackList = TextTrackList; + public readonly PerformanceObserver = PerformanceObserver; + public readonly PerformanceEntry = PerformanceEntry; + public readonly PerformanceObserverEntryList: new () => IPerformanceObserverEntryList = < + new () => IPerformanceObserverEntryList + >PerformanceObserverEntryList; // Events public onload: ((event: Event) => void) | null = null; public onerror: ((event: ErrorEvent) => void) | null = null; // Public properties. - public readonly document: DocumentImplementation; + public readonly document: Document; public readonly customElements: CustomElementRegistry; public readonly window: BrowserWindow = this; public readonly globalThis: BrowserWindow = this; @@ -497,7 +518,7 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal public console: Console; public name = ''; - // Node.js Globals + // Node.js Globals (populated by VMGlobalPropertyScript) public declare Array: typeof Array; public declare ArrayBuffer: typeof ArrayBuffer; public declare Boolean: typeof Boolean; @@ -562,10 +583,8 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal // Used for tracking capture event listeners to improve performance when they are not used. // See EventTarget class. - public [PropertySymbol.captureEventListenerCount]: { [eventType: string]: number } = {}; public [PropertySymbol.mutationObservers]: MutationObserver[] = []; public readonly [PropertySymbol.readyStateManager] = new DocumentReadyStateManager(this); - public [PropertySymbol.asyncTaskManager]: AsyncTaskManager | null = null; public [PropertySymbol.location]: Location; public [PropertySymbol.history]: History; public [PropertySymbol.navigator]: Navigator; @@ -575,6 +594,8 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal public [PropertySymbol.self]: BrowserWindow = this; public [PropertySymbol.top]: BrowserWindow = this; public [PropertySymbol.parent]: BrowserWindow = this; + public [PropertySymbol.window]: BrowserWindow = this; + public [PropertySymbol.internalId]: number = -1; // Private properties #browserFrame: IBrowserFrame; @@ -584,6 +605,7 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal #outerHeight: number | null = null; #devicePixelRatio: number | null = null; #zeroDelayTimeout: { timeouts: Array | null } = { timeouts: null }; + #timerLoopStacks: string[] = []; /** * Constructor. @@ -595,11 +617,10 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal constructor(browserFrame: IBrowserFrame, options?: { url?: string }) { super(); - const asyncTaskManager = browserFrame[PropertySymbol.asyncTaskManager]; - this.#browserFrame = browserFrame; this.customElements = new CustomElementRegistry(this); + this.console = browserFrame.page.console; this[PropertySymbol.navigator] = new Navigator(this); this[PropertySymbol.screen] = new Screen(); @@ -607,143 +628,34 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal this[PropertySymbol.localStorage] = new Storage(); this[PropertySymbol.location] = new Location(this.#browserFrame, options?.url ?? 'about:blank'); this[PropertySymbol.history] = new History(this.#browserFrame, this); - this[PropertySymbol.asyncTaskManager] = asyncTaskManager; - this.console = browserFrame.page.console; - - WindowBrowserSettingsReader.setSettings(this, this.#browserFrame.page.context.browser.settings); - - const window = this; + WindowBrowserContext.setWindowBrowserFrameRelation(this, this.#browserFrame); this[PropertySymbol.setupVMContext](); - // Class overrides - // For classes that need to be bound to the correct context to be instantiable using the "new" keyword. - - /* eslint-disable jsdoc/require-jsdoc */ - - class Request extends RequestImplementation { - constructor(input: IRequestInfo, init?: IRequestInit) { - super({ window, asyncTaskManager }, input, init); - } - } - class Response extends ResponseImplementation { - protected static [PropertySymbol.window] = window; - constructor(body?: IResponseBody, init?: IResponseInit) { - super({ window, browserFrame }, body, init); - } - } - class XMLHttpRequest extends XMLHttpRequestImplementation { - constructor() { - super({ window, browserFrame }); - } - } - class FileReader extends FileReaderImplementation { - constructor() { - super(window); - } - } - class DOMParser extends DOMParserImplementation { - constructor() { - super(window); - } - } - class Range extends RangeImplementation { - constructor() { - super(window); - } - } - class VTTCue extends VTTCueImplementation { - constructor(startTime: number, endTime: number, text: string) { - super(window, startTime, endTime, text); - } - } - class HTMLScriptElement extends HTMLScriptElementImplementation { - constructor() { - super(browserFrame); - } - } - class HTMLLinkElement extends HTMLLinkElementImplementation { - constructor() { - super(browserFrame); - } - } - class HTMLIFrameElement extends HTMLIFrameElementImplementation { - constructor() { - super(browserFrame); - } - } - class HTMLFormElement extends HTMLFormElementImplementation { - constructor() { - super(browserFrame); - } - } - class Document extends DocumentImplementation { - constructor() { - super({ window, browserFrame }); - } - } - class HTMLDocument extends HTMLDocumentImplementation { - constructor() { - super({ window, browserFrame }); - } - } - class XMLDocument extends XMLDocumentImplementation { - constructor() { - super({ window, browserFrame }); - } - } - class SVGDocument extends SVGDocumentImplementation { - constructor() { - super({ window, browserFrame }); - } - } - - class Audio extends AudioImplementation {} - class Image extends ImageImplementation {} - class DocumentFragment extends DocumentFragmentImplementation {} - class Text extends TextImplementation {} - class Comment extends CommentImplementation {} - - /* eslint-enable jsdoc/require-jsdoc */ - - this.Response = Response; - this.Request = Request; - this.Image = Image; - this.Text = Text; - this.Comment = Comment; - this.DocumentFragment = DocumentFragment; - this.FileReader = FileReader; - this.DOMParser = DOMParser; - this.VTTCue = VTTCue; - this.XMLHttpRequest = XMLHttpRequest; - this.Range = Range; - this.Audio = Audio; - this.HTMLScriptElement = HTMLScriptElement; - this.HTMLLinkElement = HTMLLinkElement; - this.HTMLIFrameElement = HTMLIFrameElement; - this.HTMLFormElement = HTMLFormElement; - this.Document = Document; - this.HTMLDocument = HTMLDocument; - this.XMLDocument = XMLDocument; - this.SVGDocument = SVGDocument; + WindowClassExtender.extendClass(this); // Document - this.document = new HTMLDocument(); + this.document = new this.HTMLDocument(); this.document[PropertySymbol.defaultView] = this; - // Override owner document - this.Audio[PropertySymbol.ownerDocument] = this.document; - this.Image[PropertySymbol.ownerDocument] = this.document; - this.DocumentFragment[PropertySymbol.ownerDocument] = this.document; - this.Text[PropertySymbol.ownerDocument] = this.document; - this.Comment[PropertySymbol.ownerDocument] = this.document; - // Ready state manager this[PropertySymbol.readyStateManager].waitUntilComplete().then(() => { this.document[PropertySymbol.readyState] = DocumentReadyStateEnum.complete; this.document.dispatchEvent(new Event('readystatechange')); - this.document.dispatchEvent(new Event('load', { bubbles: true })); + + // Not sure why target is set to document here, but this is how it works in the browser + const loadEvent = new Event('load'); + + loadEvent[PropertySymbol.currentTarget] = this.document; + loadEvent[PropertySymbol.target] = this.document; + loadEvent[PropertySymbol.eventPhase] = EventPhaseEnum.atTarget; + + this.dispatchEvent(loadEvent); + + loadEvent[PropertySymbol.target] = null; + loadEvent[PropertySymbol.currentTarget] = null; + loadEvent[PropertySymbol.eventPhase] = EventPhaseEnum.none; }); ClassMethodBinder.bindMethods(this, [EventTarget, BrowserWindow]); @@ -1125,7 +1037,7 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal * @returns A new MediaQueryList. */ public matchMedia(mediaQueryString: string): MediaQueryList { - return new MediaQueryList({ ownerWindow: this, media: mediaQueryString }); + return new MediaQueryList({ window: this, media: mediaQueryString }); } /** @@ -1137,6 +1049,21 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal * @returns Timeout ID. */ public setTimeout(callback: Function, delay = 0, ...args: unknown[]): NodeJS.Timeout { + if (this.closed) { + return; + } + + const settings = this.#browserFrame.page?.context?.browser?.settings; + + if (settings.timer.preventTimerLoops) { + const stack = new Error().stack; + const timerLoopStacks = this.#timerLoopStacks; + if (timerLoopStacks.includes(stack)) { + return; + } + timerLoopStacks.push(stack); + } + // We can group timeouts with a delay of 0 into one timeout to improve performance. // Grouping timeouts will also improve the performance of the async task manager. // It also makes the async task manager more stable as many timeouts may cause waitUntilComplete() to be resolved too early. @@ -1144,13 +1071,14 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal const zeroDelayTimeout = this.#zeroDelayTimeout; if (!zeroDelayTimeout.timeouts) { - const settings = this.#browserFrame.page?.context?.browser?.settings; const useTryCatch = !settings || (!settings.disableErrorCapturing && settings.errorCapture === BrowserErrorCaptureEnum.tryAndCatch); const id = TIMER.setTimeout(() => { + // We need to call endTimer() before the callback as the callback might throw an error. + this.#browserFrame[PropertySymbol.asyncTaskManager].endTimer(id); const timeouts = zeroDelayTimeout.timeouts; zeroDelayTimeout.timeouts = null; for (const timeout of timeouts) { @@ -1160,7 +1088,6 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal timeout.callback(); } } - this.#browserFrame[PropertySymbol.asyncTaskManager].endTimer(id); }); zeroDelayTimeout.timeouts = []; @@ -1174,7 +1101,6 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal return (timeout); } - const settings = this.#browserFrame.page?.context?.browser?.settings; const useTryCatch = !settings || (!settings.disableErrorCapturing && @@ -1182,12 +1108,13 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal const id = TIMER.setTimeout( () => { + // We need to call endTimer() before the callback as the callback might throw an error. + this.#browserFrame[PropertySymbol.asyncTaskManager].endTimer(id); if (useTryCatch) { WindowErrorUtility.captureError(this, () => callback(...args)); } else { callback(...args); } - this.#browserFrame[PropertySymbol.asyncTaskManager].endTimer(id); }, settings?.timer.maxTimeout !== -1 && delay && delay > settings?.timer.maxTimeout ? settings?.timer.maxTimeout @@ -1232,6 +1159,9 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal * @returns Interval ID. */ public setInterval(callback: Function, delay = 0, ...args: unknown[]): NodeJS.Timeout { + if (this.closed) { + return; + } const settings = this.#browserFrame.page?.context?.browser?.settings; const useTryCatch = !settings || @@ -1286,18 +1216,32 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal * @returns ID. */ public requestAnimationFrame(callback: (timestamp: number) => void): NodeJS.Immediate { + if (this.closed) { + return; + } const settings = this.#browserFrame.page?.context?.browser?.settings; + + if (settings.timer.preventTimerLoops) { + const stack = new Error().stack; + const timerLoopStacks = this.#timerLoopStacks; + if (timerLoopStacks.includes(stack)) { + return; + } + timerLoopStacks.push(stack); + } + const useTryCatch = !settings || (!settings.disableErrorCapturing && settings.errorCapture === BrowserErrorCaptureEnum.tryAndCatch); const id = TIMER.setImmediate(() => { + // We need to call endImmediate() before the callback as the callback might throw an error. + this.#browserFrame[PropertySymbol.asyncTaskManager].endImmediate(id); if (useTryCatch) { WindowErrorUtility.captureError(this, () => callback(this.performance.now())); } else { callback(this.performance.now()); } - this.#browserFrame[PropertySymbol.asyncTaskManager].endImmediate(id); }); this.#browserFrame[PropertySymbol.asyncTaskManager].startImmediate(id); return id; @@ -1324,6 +1268,9 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal * @param callback Function to be executed. */ public queueMicrotask(callback: Function): void { + if (this.closed) { + return; + } let isAborted = false; const taskId = this.#browserFrame[PropertySymbol.asyncTaskManager].startTask( () => (isAborted = true) @@ -1335,12 +1282,13 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal settings.errorCapture === BrowserErrorCaptureEnum.tryAndCatch); TIMER.queueMicrotask(() => { if (!isAborted) { + // We need to call endTask() before the callback as the callback might throw an error. + this.#browserFrame[PropertySymbol.asyncTaskManager].endTask(taskId); if (useTryCatch) { WindowErrorUtility.captureError(this, <() => unknown>callback); } else { callback(); } - this.#browserFrame[PropertySymbol.asyncTaskManager].endTask(taskId); } }); } @@ -1353,6 +1301,14 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal * @returns Promise. */ public async fetch(url: IRequestInfo, init?: IRequestInit): Promise { + if (this.closed) { + return Promise.reject( + new this.DOMException( + "Failed to execute 'fetch' on 'Window': The window is closed.", + DOMExceptionNameEnum.invalidStateError + ) + ); + } return await new Fetch({ browserFrame: this.#browserFrame, window: this, @@ -1395,8 +1351,12 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal public postMessage(message: unknown, targetOrigin = '*', _transfer?: unknown[]): void { // TODO: Implement transfer. + if (this.closed) { + return; + } + if (targetOrigin && targetOrigin !== '*' && this.location.origin !== targetOrigin) { - throw new DOMException( + throw new this.DOMException( `Failed to execute 'postMessage' on 'Window': The target origin provided ('${targetOrigin}') does not match the recipient window\'s origin ('${this.location.origin}').`, DOMExceptionNameEnum.securityError ); @@ -1405,7 +1365,7 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal try { JSON.stringify(message); } catch (error) { - throw new DOMException( + throw new this.DOMException( `Failed to execute 'postMessage' on 'Window': The provided message cannot be serialized.`, DOMExceptionNameEnum.invalidStateError ); @@ -1434,8 +1394,12 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal * @param height Height. */ public resizeTo(width: number, height: number): void { + if (this.closed) { + return; + } + if (!width || !height) { - throw new DOMException( + throw new this.DOMException( `Failed to execute 'resizeTo' on 'Window': 2 arguments required, but only ${arguments.length} present.` ); } @@ -1453,8 +1417,12 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal * @param height Height. */ public resizeBy(width: number, height: number): void { + if (this.closed) { + return; + } + if (!width || !height) { - throw new DOMException( + throw new this.DOMException( `Failed to execute 'resizeBy' on 'Window': 2 arguments required, but only ${arguments.length} present.` ); } @@ -1502,12 +1470,6 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal this[PropertySymbol.asyncTaskManager] = null; this[PropertySymbol.mutationObservers] = []; - this.Audio[PropertySymbol.ownerDocument] = null; - this.Image[PropertySymbol.ownerDocument] = null; - this.DocumentFragment[PropertySymbol.ownerDocument] = null; - this.Text[PropertySymbol.ownerDocument] = null; - this.Comment[PropertySymbol.ownerDocument] = null; - // Disconnects nodes from the document, so that they can be garbage collected. const childNodes = this.document[PropertySymbol.nodeArray]; @@ -1519,6 +1481,18 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal this.document.removeChild(childNodes[0]); } + // Create some empty elements for scripts that are still running. + const htmlElement = this.document.createElement('html'); + const headElement = this.document.createElement('head'); + const bodyElement = this.document.createElement('body'); + htmlElement.appendChild(headElement); + htmlElement.appendChild(bodyElement); + this.document.appendChild(htmlElement); + + if (this.location[PropertySymbol.destroy]) { + this.location[PropertySymbol.destroy](); + } + if (this.customElements[PropertySymbol.destroy]) { this.customElements[PropertySymbol.destroy](); } @@ -1532,6 +1506,6 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal this.document[PropertySymbol.currentScript] = null; this.document[PropertySymbol.selection] = null; - WindowBrowserSettingsReader.removeSettings(this); + WindowBrowserContext.removeWindowBrowserFrameRelation(this); } } diff --git a/packages/happy-dom/src/window/VMGlobalPropertyScript.ts b/packages/happy-dom/src/window/VMGlobalPropertyScript.ts index 774dc85e6..54242902e 100644 --- a/packages/happy-dom/src/window/VMGlobalPropertyScript.ts +++ b/packages/happy-dom/src/window/VMGlobalPropertyScript.ts @@ -48,7 +48,6 @@ this.isFinite = globalThis.isFinite; this.isNaN = globalThis.isNaN; this.parseFloat = globalThis.parseFloat; this.parseInt = globalThis.parseInt; -this.process = null; this.root = globalThis.root; this.undefined = globalThis.undefined; this.unescape = globalThis.unescape; diff --git a/packages/happy-dom/src/window/WindowBrowserContext.ts b/packages/happy-dom/src/window/WindowBrowserContext.ts new file mode 100644 index 000000000..a7a3e1727 --- /dev/null +++ b/packages/happy-dom/src/window/WindowBrowserContext.ts @@ -0,0 +1,114 @@ +import AsyncTaskManager from '../async-task-manager/AsyncTaskManager.js'; +import IBrowser from '../browser/types/IBrowser.js'; +import IBrowserContext from '../browser/types/IBrowserContext.js'; +import IBrowserFrame from '../browser/types/IBrowserFrame.js'; +import IBrowserPage from '../browser/types/IBrowserPage.js'; +import IBrowserSettings from '../browser/types/IBrowserSettings.js'; +import * as PropertySymbol from '../PropertySymbol.js'; +import BrowserWindow from './BrowserWindow.js'; + +/** + * API for accessing the Browser in a Window context without exposing the Browser as accessible properties. + * + * The Browser should never be exposed to scripts, as the scripts could then manipulate it, which would lead to security issues. + */ +export default class WindowBrowserContext { + private static [PropertySymbol.browserFrames]: Map = new Map(); + private static [PropertySymbol.windowInternalId] = 0; + #window: BrowserWindow; + + /** + * Browser window. + * + * @param window Window. + */ + constructor(window: BrowserWindow) { + this.#window = window; + } + + /** + * Returns the browser settings of the window. + * + * @returns Browser settings. + */ + public getSettings(): IBrowserSettings | null { + return this.getBrowserFrame()?.page?.context?.browser?.settings || null; + } + + /** + * Returns the browser. + * + * @returns Browser. + */ + public getBrowser(): IBrowser | null { + return this.getBrowserFrame()?.page?.context?.browser || null; + } + + /** + * Returns the browser page. + * + * @returns Browser page. + */ + public getBrowserPage(): IBrowserPage | null { + return this.getBrowserFrame()?.page || null; + } + + /** + * Returns the browser context. + * + * @returns Browser context. + */ + public getBrowserContext(): IBrowserContext | null { + return this.getBrowserFrame()?.page?.context || null; + } + + /** + * Returns the browser frame of the window. + * + * @returns Browser frame. + */ + public getBrowserFrame(): IBrowserFrame | null { + return ( + (this.constructor)[PropertySymbol.browserFrames].get( + this.#window[PropertySymbol.internalId] + ) || null + ); + } + + /** + * Returns the async task manager of the window. + * + * @returns Async task manager. + */ + public getAsyncTaskManager(): AsyncTaskManager | null { + return this.getBrowserFrame()?.[PropertySymbol.asyncTaskManager] || null; + } + + /** + * Assigns the window to a browser frame. + * + * @param window Window. + * @param browserFrame Browser frame. + */ + public static setWindowBrowserFrameRelation( + window: BrowserWindow, + browserFrame: IBrowserFrame + ): void { + const browserFrames = this[PropertySymbol.browserFrames]; + if (window[PropertySymbol.internalId] === -1) { + window[PropertySymbol.internalId] = this[PropertySymbol.windowInternalId]; + this[PropertySymbol.windowInternalId]++; + } + browserFrames.set(window[PropertySymbol.internalId], browserFrame); + } + + /** + * Assigns the window to a browser frame. + * + * @param window Window. + * @param browserFrame Browser frame. + */ + public static removeWindowBrowserFrameRelation(window: BrowserWindow): void { + this[PropertySymbol.browserFrames].delete(window[PropertySymbol.internalId]); + } +} diff --git a/packages/happy-dom/src/window/WindowBrowserSettingsReader.ts b/packages/happy-dom/src/window/WindowBrowserSettingsReader.ts deleted file mode 100644 index 06a8fa9bf..000000000 --- a/packages/happy-dom/src/window/WindowBrowserSettingsReader.ts +++ /dev/null @@ -1,55 +0,0 @@ -import IBrowserSettings from '../browser/types/IBrowserSettings.js'; -import * as PropertySymbol from '../PropertySymbol.js'; -import BrowserWindow from './BrowserWindow.js'; - -/** - * Browser settings reader that will allow to read settings more securely as it is not possible to override a settings object to make DOM functionality act on it. - */ -export default class WindowBrowserSettingsReader { - static #settings: IBrowserSettings[] = []; - - /** - * Returns browser settings. - * - * @param window Window. - * @returns Settings. - */ - public static getSettings(window: BrowserWindow): IBrowserSettings | null { - const id = window[PropertySymbol.happyDOMSettingsID]; - - if (id === undefined || !this.#settings[id]) { - return null; - } - - return this.#settings[id]; - } - - /** - * Sets browser settings. - * - * @param window Window. - * @param settings Settings. - */ - public static setSettings(window: BrowserWindow, settings: IBrowserSettings): void { - if (window[PropertySymbol.happyDOMSettingsID] !== undefined) { - return; - } - window[PropertySymbol.happyDOMSettingsID] = this.#settings.length; - this.#settings.push(settings); - } - - /** - * Removes browser settings. - * - * @param window Window. - */ - public static removeSettings(window: BrowserWindow): void { - const id = window[PropertySymbol.happyDOMSettingsID]; - - if (id !== undefined && this.#settings[id]) { - delete this.#settings[id]; - } - - delete window[PropertySymbol.happyDOMSettingsID]; - } -} diff --git a/packages/happy-dom/src/window/WindowClassExtender.ts b/packages/happy-dom/src/window/WindowClassExtender.ts new file mode 100644 index 000000000..b83d163cb --- /dev/null +++ b/packages/happy-dom/src/window/WindowClassExtender.ts @@ -0,0 +1,312 @@ +import BrowserWindow from './BrowserWindow.js'; +import * as PropertySymbol from '../PropertySymbol.js'; + +import ElementImplementation from '../nodes/element/Element.js'; +import DocumentImplementation from '../nodes/document/Document.js'; +import HTMLDocumentImplementation from '../nodes/html-document/HTMLDocument.js'; +import XMLDocumentImplementation from '../nodes/xml-document/XMLDocument.js'; +import SVGDocumentImplementation from '../nodes/svg-document/SVGDocument.js'; +import DocumentFragmentImplementation from '../nodes/document-fragment/DocumentFragment.js'; +import TextImplementation from '../nodes/text/Text.js'; +import CommentImplementation from '../nodes/comment/Comment.js'; +import ImageImplementation from '../nodes/html-image-element/Image.js'; +import AudioImplementation from '../nodes/html-audio-element/Audio.js'; +import NodeIteratorImplementation from '../tree-walker/NodeIterator.js'; +import TreeWalkerImplementation from '../tree-walker/TreeWalker.js'; +import MutationObserverImplementation from '../mutation-observer/MutationObserver.js'; +import CSSStyleDeclarationImplementation from '../css/declaration/CSSStyleDeclaration.js'; +import MessagePortImplementation from '../event/MessagePort.js'; +import DataTransferImplementation from '../event/DataTransfer.js'; +import DataTransferItemImplementation from '../event/DataTransferItem.js'; +import DataTransferItemListImplementation from '../event/DataTransferItemList.js'; +import XMLSerializerImplementation from '../xml-serializer/XMLSerializer.js'; +import CSSStyleSheetImplementation from '../css/CSSStyleSheet.js'; +import DOMExceptionImplementation from '../exception/DOMException.js'; +import CSSUnitValueImplementation from '../css/CSSUnitValue.js'; +import SelectionImplementation from '../selection/Selection.js'; +import HeadersImplementation from '../fetch/Headers.js'; +import RequestImplementation from '../fetch/Request.js'; +import ResponseImplementation from '../fetch/Response.js'; +import EventTargetImplementation from '../event/EventTarget.js'; +import XMLHttpRequestUploadImplementation from '../xml-http-request/XMLHttpRequestUpload.js'; +import XMLHttpRequestEventTargetImplementation from '../xml-http-request/XMLHttpRequestEventTarget.js'; +import AbortControllerImplementation from '../fetch/AbortController.js'; +import AbortSignalImplementation from '../fetch/AbortSignal.js'; +import FormDataImplementation from '../form-data/FormData.js'; +import PermissionsImplementation from '../permissions/Permissions.js'; +import PermissionStatusImplementation from '../permissions/PermissionStatus.js'; +import ClipboardItemImplementation from '../clipboard/ClipboardItem.js'; +import XMLHttpRequestImplementation from '../xml-http-request/XMLHttpRequest.js'; +import DOMParserImplementation from '../dom-parser/DOMParser.js'; +import RangeImplementation from '../range/Range.js'; +import VTTCueImplementation from '../nodes/html-media-element/VTTCue.js'; +import TextTrackImplementation from '../nodes/html-media-element/TextTrack.js'; +import TextTrackListImplementation from '../nodes/html-media-element/TextTrackList.js'; +import TextTrackCueImplementation from '../nodes/html-media-element/TextTrackCue.js'; +import RemotePlaybackImplementation from '../nodes/html-media-element/RemotePlayback.js'; +import FileReaderImplementation from '../file/FileReader.js'; +import MediaStreamImplementation from '../nodes/html-media-element/MediaStream.js'; +import MediaStreamTrackImplementation from '../nodes/html-media-element/MediaStreamTrack.js'; +import CanvasCaptureMediaStreamTrackImplementation from '../nodes/html-canvas-element/CanvasCaptureMediaStreamTrack.js'; +import NamedNodeMapImplementation from '../nodes/element/NamedNodeMap.js'; + +/** + * Extends classes with a "window" property, so that they internally can access it's Window context. + * + * By using WindowBrowserContext, the classes can get access to their Browser context, for accessing settings or navigating the browser. + */ +export default class WindowClassExtender { + /** + * Extends classes with a "window" property. + * + * @param window Window. + */ + public static extendClass(window: BrowserWindow): void { + /* eslint-disable jsdoc/require-jsdoc */ + + // Element + class Element extends ElementImplementation {} + Element.prototype[PropertySymbol.window] = window; + (window.Element) = Element; + + // Document + class Document extends DocumentImplementation {} + Document.prototype[PropertySymbol.window] = window; + (window.Document) = Document; + + // HTMLDocument + class HTMLDocument extends HTMLDocumentImplementation {} + HTMLDocument.prototype[PropertySymbol.window] = window; + (window.HTMLDocument) = HTMLDocument; + + // XMLDocument + class XMLDocument extends XMLDocumentImplementation {} + XMLDocument.prototype[PropertySymbol.window] = window; + (window.XMLDocument) = XMLDocument; + + // SVGDocument + class SVGDocument extends SVGDocumentImplementation {} + SVGDocument.prototype[PropertySymbol.window] = window; + (window.SVGDocument) = SVGDocument; + + // DocumentFragment + class DocumentFragment extends DocumentFragmentImplementation {} + DocumentFragment.prototype[PropertySymbol.window] = window; + (window.DocumentFragment) = DocumentFragment; + + // Text + class Text extends TextImplementation {} + Text.prototype[PropertySymbol.window] = window; + (window.Text) = Text; + + // Comment + class Comment extends CommentImplementation {} + Comment.prototype[PropertySymbol.window] = window; + (window.Comment) = Comment; + + // Image + class Image extends ImageImplementation {} + Image.prototype[PropertySymbol.window] = window; + (window.Image) = Image; + + // Audio + class Audio extends AudioImplementation {} + Audio.prototype[PropertySymbol.window] = window; + (window.Audio) = Audio; + + // NodeIterator + class NodeIterator extends NodeIteratorImplementation {} + NodeIterator.prototype[PropertySymbol.window] = window; + (window.NodeIterator) = NodeIterator; + + // TreeWalker + class TreeWalker extends TreeWalkerImplementation {} + TreeWalker.prototype[PropertySymbol.window] = window; + (window.TreeWalker) = TreeWalker; + + // MutationObserver + class MutationObserver extends MutationObserverImplementation {} + MutationObserver.prototype[PropertySymbol.window] = window; + (window.MutationObserver) = MutationObserver; + + // CSSStyleDeclaration + class CSSStyleDeclaration extends CSSStyleDeclarationImplementation {} + CSSStyleDeclaration.prototype[PropertySymbol.window] = window; + (window.CSSStyleDeclaration) = CSSStyleDeclaration; + + // MessagePort + class MessagePort extends MessagePortImplementation {} + MessagePort.prototype[PropertySymbol.window] = window; + (window.MessagePort) = MessagePort; + + // DataTransfer + class DataTransfer extends DataTransferImplementation {} + DataTransfer.prototype[PropertySymbol.window] = window; + (window.DataTransfer) = DataTransfer; + + // DataTransferItem + class DataTransferItem extends DataTransferItemImplementation {} + DataTransferItem.prototype[PropertySymbol.window] = window; + (window.DataTransferItem) = DataTransferItem; + + // DataTransferItemList + class DataTransferItemList extends DataTransferItemListImplementation {} + DataTransferItemList.prototype[PropertySymbol.window] = window; + (window.DataTransferItemList) = DataTransferItemList; + + // XMLSerializer + class XMLSerializer extends XMLSerializerImplementation {} + XMLSerializer.prototype[PropertySymbol.window] = window; + (window.XMLSerializer) = XMLSerializer; + + // CSSStyleSheet + class CSSStyleSheet extends CSSStyleSheetImplementation {} + CSSStyleSheet.prototype[PropertySymbol.window] = window; + (window.CSSStyleSheet) = CSSStyleSheet; + + // DOMException + class DOMException extends DOMExceptionImplementation {} + (window.DOMException) = DOMException; + + // CSSUnitValue + class CSSUnitValue extends CSSUnitValueImplementation {} + CSSUnitValue.prototype[PropertySymbol.window] = window; + (window.CSSUnitValue) = CSSUnitValue; + + // Selection + class Selection extends SelectionImplementation {} + Selection.prototype[PropertySymbol.window] = window; + (window.Selection) = Selection; + + // Headers + class Headers extends HeadersImplementation {} + Headers.prototype[PropertySymbol.window] = window; + (window.Headers) = Headers; + + // Request + class Request extends RequestImplementation {} + Request.prototype[PropertySymbol.window] = window; + (window.Request) = Request; + + // Response + class Response extends ResponseImplementation {} + Response.prototype[PropertySymbol.window] = window; + Response[PropertySymbol.window] = window; + (window.Response) = Response; + + // XMLHttpRequestEventTarget + class EventTarget extends EventTargetImplementation {} + EventTarget.prototype[PropertySymbol.window] = window; + (window.EventTarget) = EventTarget; + + // XMLHttpRequestUpload + class XMLHttpRequestUpload extends XMLHttpRequestUploadImplementation {} + XMLHttpRequestUpload.prototype[PropertySymbol.window] = window; + (window.XMLHttpRequestUpload) = XMLHttpRequestUpload; + + // XMLHttpRequestEventTarget + class XMLHttpRequestEventTarget extends XMLHttpRequestEventTargetImplementation {} + XMLHttpRequestEventTarget.prototype[PropertySymbol.window] = window; + (window.XMLHttpRequestEventTarget) = + XMLHttpRequestEventTarget; + + // AbortController + class AbortController extends AbortControllerImplementation {} + AbortController.prototype[PropertySymbol.window] = window; + (window.AbortController) = AbortController; + + // AbortSignal + class AbortSignal extends AbortSignalImplementation {} + AbortSignal.prototype[PropertySymbol.window] = window; + AbortSignal[PropertySymbol.window] = window; + (window.AbortSignal) = AbortSignal; + + // FormData + class FormData extends FormDataImplementation {} + FormData.prototype[PropertySymbol.window] = window; + (window.FormData) = FormData; + + // Permissions + class Permissions extends PermissionsImplementation {} + Permissions.prototype[PropertySymbol.window] = window; + (window.Permissions) = Permissions; + + // PermissionStatus + class PermissionStatus extends PermissionStatusImplementation {} + PermissionStatus.prototype[PropertySymbol.window] = window; + (window.PermissionStatus) = PermissionStatus; + + // ClipboardItem + class ClipboardItem extends ClipboardItemImplementation {} + ClipboardItem.prototype[PropertySymbol.window] = window; + (window.ClipboardItem) = ClipboardItem; + + // XMLHttpRequest + class XMLHttpRequest extends XMLHttpRequestImplementation {} + XMLHttpRequest.prototype[PropertySymbol.window] = window; + (window.XMLHttpRequest) = XMLHttpRequest; + + // DOMParser + class DOMParser extends DOMParserImplementation {} + DOMParser.prototype[PropertySymbol.window] = window; + (window.DOMParser) = DOMParser; + + // Range + class Range extends RangeImplementation {} + Range.prototype[PropertySymbol.window] = window; + (window.Range) = Range; + + // VTTCue + class VTTCue extends VTTCueImplementation {} + VTTCue.prototype[PropertySymbol.window] = window; + (window.VTTCue) = VTTCue; + + // TextTrack + class TextTrack extends TextTrackImplementation {} + TextTrack.prototype[PropertySymbol.window] = window; + (window.TextTrack) = TextTrack; + + // TextTrackList + class TextTrackList extends TextTrackListImplementation {} + TextTrackList.prototype[PropertySymbol.window] = window; + (window.TextTrackList) = TextTrackList; + + // TextTrackCue + class TextTrackCue extends TextTrackCueImplementation {} + TextTrackCue.prototype[PropertySymbol.window] = window; + (window.TextTrackCue) = TextTrackCue; + + // RemotePlayback + class RemotePlayback extends RemotePlaybackImplementation {} + RemotePlayback.prototype[PropertySymbol.window] = window; + (window.RemotePlayback) = RemotePlayback; + + // FileReader + class FileReader extends FileReaderImplementation {} + FileReader.prototype[PropertySymbol.window] = window; + (window.FileReader) = FileReader; + + // MediaStream + class MediaStream extends MediaStreamImplementation {} + MediaStream.prototype[PropertySymbol.window] = window; + (window.MediaStream) = MediaStream; + + // MediaStreamTrack + class MediaStreamTrack extends MediaStreamTrackImplementation {} + MediaStreamTrack.prototype[PropertySymbol.window] = window; + (window.MediaStreamTrack) = MediaStreamTrack; + + // MediaStreamTrack + class CanvasCaptureMediaStreamTrack extends CanvasCaptureMediaStreamTrackImplementation {} + CanvasCaptureMediaStreamTrack.prototype[PropertySymbol.window] = window; + (window.CanvasCaptureMediaStreamTrack) = + CanvasCaptureMediaStreamTrack; + + // NamedNodeMap + class NamedNodeMap extends NamedNodeMapImplementation {} + NamedNodeMap.prototype[PropertySymbol.window] = window; + (window.NamedNodeMap) = NamedNodeMap; + + /* eslint-enable jsdoc/require-jsdoc */ + } +} diff --git a/packages/happy-dom/src/window/WindowPageOpenUtility.ts b/packages/happy-dom/src/window/WindowPageOpenUtility.ts index 26e977c5c..dedc1de6c 100644 --- a/packages/happy-dom/src/window/WindowPageOpenUtility.ts +++ b/packages/happy-dom/src/window/WindowPageOpenUtility.ts @@ -34,6 +34,10 @@ export default class WindowPageOpenUtility { const oldWindow = browserFrame.window; let targetFrame: IBrowserFrame; + if (browserFrame.window !== oldWindow) { + return null; + } + switch (target) { case '_self': targetFrame = browserFrame; @@ -57,7 +61,13 @@ export default class WindowPageOpenUtility { referrer: features.noreferrer ? undefined : browserFrame.window.location.origin, referrerPolicy: features.noreferrer ? 'no-referrer' : undefined }) - .catch((error) => targetFrame.page.console.error(error)); + .catch((error) => { + if (targetFrame.page?.console) { + targetFrame.page.console.error(error); + } else { + throw error; + } + }); if (targetURL.protocol === 'javascript:') { return targetFrame.window; diff --git a/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts b/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts index b018bda94..b49887308 100644 --- a/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts +++ b/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts @@ -9,8 +9,6 @@ import DOMException from '../exception/DOMException.js'; import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum.js'; import XMLHttpResponseTypeEnum from './XMLHttpResponseTypeEnum.js'; import ErrorEvent from '../event/events/ErrorEvent.js'; -import IBrowserFrame from '../browser/types/IBrowserFrame.js'; -import BrowserWindow from '../window/BrowserWindow.js'; import Headers from '../fetch/Headers.js'; import Fetch from '../fetch/Fetch.js'; import SyncFetch from '../fetch/SyncFetch.js'; @@ -23,6 +21,7 @@ import IRequestBody from '../fetch/types/IRequestBody.js'; import XMLHttpRequestResponseDataParser from './XMLHttpRequestResponseDataParser.js'; import FetchRequestHeaderUtility from '../fetch/utilities/FetchRequestHeaderUtility.js'; import Response from '../fetch/Response.js'; +import WindowBrowserContext from '../window/WindowBrowserContext.js'; /** * XMLHttpRequest. @@ -39,12 +38,10 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { public static DONE = XMLHttpRequestReadyStateEnum.done; // Public properties - public upload: XMLHttpRequestUpload = new XMLHttpRequestUpload(); + public upload: XMLHttpRequestUpload = new this[PropertySymbol.window].XMLHttpRequestUpload(); public withCredentials = false; // Private properties - #browserFrame: IBrowserFrame; - #window: BrowserWindow; #async = true; #abortController: AbortController | null = null; #aborted = false; @@ -56,15 +53,15 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { /** * Constructor. - * - * @param injected Injected properties. - * @param injected.browserFrame Browser frame. - * @param injected.window Window. */ - constructor(injected: { browserFrame: IBrowserFrame; window: BrowserWindow }) { + constructor() { super(); - this.#browserFrame = injected.browserFrame; - this.#window = injected.window; + + if (!this[PropertySymbol.window]) { + throw new TypeError( + `Failed to construct '${this.constructor.name}': '${this.constructor.name}' was constructed outside a Window context.` + ); + } } /** @@ -106,7 +103,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { */ public get responseText(): string { if (this.responseType !== XMLHttpResponseTypeEnum.text && this.responseType !== '') { - throw new DOMException( + throw new this[PropertySymbol.window].DOMException( `Failed to read the 'responseText' property from 'XMLHttpRequest': The value is only accessible if the object's 'responseType' is '' or 'text' (was '${this.responseType}').`, DOMExceptionNameEnum.invalidStateError ); @@ -122,7 +119,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { */ public get responseXML(): Document { if (this.responseType !== XMLHttpResponseTypeEnum.document && this.responseType !== '') { - throw new DOMException( + throw new this[PropertySymbol.window].DOMException( `Failed to read the 'responseXML' property from 'XMLHttpRequest': The value is only accessible if the object's 'responseType' is '' or 'document' (was '${this.responseType}').`, DOMExceptionNameEnum.invalidStateError ); @@ -161,14 +158,14 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { this.readyState !== XMLHttpRequestReadyStateEnum.opened && this.readyState !== XMLHttpRequestReadyStateEnum.unsent ) { - throw new DOMException( + throw new this[PropertySymbol.window].DOMException( `Failed to set the 'responseType' property on 'XMLHttpRequest': The object's state must be OPENED or UNSENT.`, DOMExceptionNameEnum.invalidStateError ); } // Sync requests can only have empty string or 'text' as response type. if (!this.#async) { - throw new DOMException( + throw new this[PropertySymbol.window].DOMException( `Failed to set the 'responseType' property on 'XMLHttpRequest': The response type cannot be changed for synchronous requests made from a document.`, DOMExceptionNameEnum.invalidStateError ); @@ -195,8 +192,10 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { * @param [password] Password for basic authentication (optional). */ public open(method: string, url: string, async = true, user?: string, password?: string): void { + const window = this[PropertySymbol.window]; + if (!async && !!this.responseType && this.responseType !== XMLHttpResponseTypeEnum.text) { - throw new DOMException( + throw new window.DOMException( `Failed to execute 'open' on 'XMLHttpRequest': Synchronous requests from a document must not set a response type.`, DOMExceptionNameEnum.invalidAccessError ); @@ -212,8 +211,8 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { this.#aborted = false; this.#response = null; this.#responseBody = null; - this.#abortController = new AbortController(); - this.#request = new this.#window.Request(url, { + this.#abortController = new window.AbortController(); + this.#request = new window.Request(url, { method, headers, signal: this.#abortController.signal, @@ -232,7 +231,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { */ public setRequestHeader(name: string, value: string): boolean { if (this.readyState !== XMLHttpRequestReadyStateEnum.opened) { - throw new DOMException( + throw new this[PropertySymbol.window].DOMException( `Failed to execute 'setRequestHeader' on 'XMLHttpRequest': The object's state must be OPENED.`, DOMExceptionNameEnum.invalidStateError ); @@ -287,7 +286,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { */ public send(body?: Document | IRequestBody): void { if (this.readyState != XMLHttpRequestReadyStateEnum.opened) { - throw new DOMException( + throw new this[PropertySymbol.window].DOMException( `Failed to execute 'send' on 'XMLHttpRequest': Connection must be opened before send() is called.`, DOMExceptionNameEnum.invalidStateError ); @@ -328,9 +327,10 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { * @param body Optional data to send as request body. */ async #sendAsync(body?: IRequestBody): Promise { - const taskID = this.#browserFrame[PropertySymbol.asyncTaskManager].startTask(() => - this.abort() - ); + const window = this[PropertySymbol.window]; + const browserFrame = new WindowBrowserContext(window).getBrowserFrame(); + const asyncTaskManager = browserFrame[PropertySymbol.asyncTaskManager]; + const taskID = asyncTaskManager.startTask(() => this.abort()); this.#readyState = XMLHttpRequestReadyStateEnum.loading; @@ -338,7 +338,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { this.dispatchEvent(new Event('loadstart')); if (body) { - this.#request = new this.#window.Request(this.#request.url, { + this.#request = new window.Request(this.#request.url, { method: this.#request.method, headers: this.#request.headers, signal: this.#abortController.signal, @@ -353,7 +353,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { this.dispatchEvent(new Event('abort')); this.dispatchEvent(new Event('loadend')); this.dispatchEvent(new Event('readystatechange')); - this.#browserFrame[PropertySymbol.asyncTaskManager].endTask(taskID); + asyncTaskManager.endTask(taskID); }); const onError = (error: Error): void => { @@ -369,12 +369,12 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { } this.dispatchEvent(new Event('loadend')); this.dispatchEvent(new Event('readystatechange')); - this.#browserFrame[PropertySymbol.asyncTaskManager].endTask(taskID); + asyncTaskManager.endTask(taskID); }; const fetch = new Fetch({ - browserFrame: this.#browserFrame, - window: this.#window, + browserFrame: browserFrame, + window: window, url: this.#request.url, init: this.#request }); @@ -426,7 +426,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { } this.#responseBody = XMLHttpRequestResponseDataParser.parse({ - window: this.#window, + window: window, responseType: this.#responseType, data, contentType: @@ -434,11 +434,11 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { }); this.#readyState = XMLHttpRequestReadyStateEnum.done; + asyncTaskManager.endTask(taskID); + this.dispatchEvent(new Event('readystatechange')); this.dispatchEvent(new Event('load')); this.dispatchEvent(new Event('loadend')); - - this.#browserFrame[PropertySymbol.asyncTaskManager].endTask(taskID); } /** @@ -447,8 +447,11 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { * @param body Optional data to send as request body. */ #sendSync(body?: IRequestBody): void { + const window = this[PropertySymbol.window]; + const browserFrame = new WindowBrowserContext(window).getBrowserFrame(); + if (body) { - this.#request = new this.#window.Request(this.#request.url, { + this.#request = new window.Request(this.#request.url, { method: this.#request.method, headers: this.#request.headers, signal: this.#abortController.signal, @@ -460,8 +463,8 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { this.#readyState = XMLHttpRequestReadyStateEnum.loading; const fetch = new SyncFetch({ - browserFrame: this.#browserFrame, - window: this.#window, + browserFrame, + window: window, url: this.#request.url, init: this.#request }); @@ -479,7 +482,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { this.#readyState = XMLHttpRequestReadyStateEnum.headersRecieved; this.#responseBody = XMLHttpRequestResponseDataParser.parse({ - window: this.#window, + window: window, responseType: this.#responseType, data: this.#response.body, contentType: diff --git a/packages/happy-dom/test/event/Event.test.ts b/packages/happy-dom/test/event/Event.test.ts index d273e83c2..00d17a69d 100644 --- a/packages/happy-dom/test/event/Event.test.ts +++ b/packages/happy-dom/test/event/Event.test.ts @@ -23,6 +23,7 @@ describe('Event', () => { describe('get target()', () => { it('Returns target.', () => { const event = new Event('click', { bubbles: true }); + let target: EventTarget | null = null; expect(event.target === null).toBe(true); const div = document.createElement('div'); @@ -30,15 +31,20 @@ describe('Event', () => { div.appendChild(span); + span.addEventListener('click', (e: Event) => { + target = e.target; + }); span.dispatchEvent(event); - expect(event.target === span).toBe(true); + expect(event.target).toBe(null); + expect(target).toBe(span); }); }); describe('get currentTarget()', () => { it('Returns current target.', () => { const event = new Event('click', { bubbles: true }); + let currentTarget: EventTarget | null = null; expect(event.currentTarget === null).toBe(true); const div = document.createElement('div'); @@ -46,9 +52,13 @@ describe('Event', () => { div.appendChild(span); + span.addEventListener('click', (e: Event) => { + currentTarget = e.currentTarget; + }); span.dispatchEvent(event); - expect(event.currentTarget === div).toBe(true); + expect(event.currentTarget).toBe(null); + expect(currentTarget).toBe(span); }); }); @@ -202,6 +212,33 @@ describe('Event', () => { expect(((composedPath))[5] === window).toBe(true); }); + it('Excludes Window from the composed path if the event type is "load".', () => { + const div = document.createElement('div'); + const span = document.createElement('span'); + let composedPath: EventTarget[] | null = null; + + div.appendChild(span); + document.body.appendChild(div); + + div.addEventListener('load', (event: Event) => { + composedPath = event.composedPath(); + }); + + span.dispatchEvent( + new Event('load', { + bubbles: true + }) + ); + + expect(((composedPath)).length).toBe(5); + expect(((composedPath))[0] === span).toBe(true); + expect(((composedPath))[1] === div).toBe(true); + expect(((composedPath))[2] === document.body).toBe(true); + expect(((composedPath))[3] === document.documentElement).toBe(true); + expect(((composedPath))[4] === document).toBe(true); + expect(((composedPath))[5] === undefined).toBe(true); + }); + it('Goes through shadow roots if composed is set to "true".', () => { const div = document.createElement('div'); const customELement = document.createElement('custom-element'); diff --git a/packages/happy-dom/test/event/EventTarget.test.ts b/packages/happy-dom/test/event/EventTarget.test.ts index 8322ea4f2..4bceb98ed 100644 --- a/packages/happy-dom/test/event/EventTarget.test.ts +++ b/packages/happy-dom/test/event/EventTarget.test.ts @@ -1,33 +1,37 @@ +import BrowserWindow from '../../src/window/BrowserWindow.js'; +import Window from '../../src/window/Window.js'; import EventTarget from '../../src/event/EventTarget.js'; import Event from '../../src/event/Event.js'; import CustomEvent from '../../src/event/events/CustomEvent.js'; -import { beforeEach, afterEach, describe, it, expect } from 'vitest'; +import { beforeEach, describe, it, expect } from 'vitest'; const EVENT_TYPE = 'click'; -/** - * - */ -class TestEventTarget extends EventTarget {} describe('EventTarget', () => { + let window: BrowserWindow; let eventTarget: EventTarget; beforeEach(() => { - eventTarget = new TestEventTarget(); + window = new Window(); + eventTarget = new window.EventTarget(); }); describe('addEventListener()', () => { it('Adds an event listener and triggers it when calling dispatchEvent().', () => { let recievedEvent: Event | null = null; + let recievedTarget: EventTarget | null = null; + let recievedCurrentTarget: EventTarget | null = null; const listener = (event: Event): void => { recievedEvent = event; + recievedTarget = event.target; + recievedCurrentTarget = event.currentTarget; }; const dispatchedEvent = new Event(EVENT_TYPE); eventTarget.addEventListener(EVENT_TYPE, listener); eventTarget.dispatchEvent(dispatchedEvent); expect(recievedEvent).toBe(dispatchedEvent); - expect(((recievedEvent)).target).toBe(eventTarget); - expect(((recievedEvent)).currentTarget).toBe(eventTarget); + expect(recievedTarget).toBe(eventTarget); + expect(recievedCurrentTarget).toBe(eventTarget); }); it('Adds an event listener and set options once', () => { @@ -65,32 +69,40 @@ describe('EventTarget', () => { it('Adds a custom event listener and triggers it when calling dispatchEvent().', () => { let recievedEvent: CustomEvent | null = null; + let recievedTarget: EventTarget | null = null; + let recievedCurrentTarget: EventTarget | null = null; const DETAIL = {}; const listener = (event): void => { recievedEvent = event; + recievedTarget = event.target; + recievedCurrentTarget = event.currentTarget; }; const dispatchedEvent = new CustomEvent(EVENT_TYPE, { detail: DETAIL }); eventTarget.addEventListener(EVENT_TYPE, listener); eventTarget.dispatchEvent(dispatchedEvent); expect(recievedEvent).toBe(dispatchedEvent); expect(((recievedEvent)).detail).toBe(DETAIL); - expect(((recievedEvent)).target).toBe(eventTarget); - expect(((recievedEvent)).currentTarget).toBe(eventTarget); + expect(recievedTarget).toBe(eventTarget); + expect(recievedCurrentTarget).toBe(eventTarget); }); it('Adds an event listener using object with handleEvent as property and triggers it when calling dispatchEvent().', () => { let recievedEvent: CustomEvent | null = null; + let recievedTarget: EventTarget | null = null; + let recievedCurrentTarget: EventTarget | null = null; const listener = { handleEvent: (event: CustomEvent): void => { recievedEvent = event; + recievedTarget = event.target; + recievedCurrentTarget = event.currentTarget; } }; const dispatchedEvent = new Event(EVENT_TYPE); eventTarget.addEventListener(EVENT_TYPE, listener); eventTarget.dispatchEvent(dispatchedEvent); expect(recievedEvent).toBe(dispatchedEvent); - expect(((recievedEvent)).target).toBe(eventTarget); - expect(((recievedEvent)).currentTarget).toBe(eventTarget); + expect(recievedTarget).toBe(eventTarget); + expect(recievedCurrentTarget).toBe(eventTarget); }); it('Event listener is called in the scope of the EventTarget when calling dispatchEvent().', () => { @@ -135,26 +147,39 @@ describe('EventTarget', () => { describe('dispatchEvent()', () => { it('Triggers listener properties with "on" as prefix.', () => { let recievedEvent: Event | null = null; + let recievedTarget: EventTarget | null = null; + let recievedCurrentTarget: EventTarget | null = null; const listener = (event: Event): void => { recievedEvent = event; + recievedTarget = event.target; + recievedCurrentTarget = event.currentTarget; }; const dispatchedEvent = new Event(EVENT_TYPE); eventTarget[`on${EVENT_TYPE}`] = listener; eventTarget.dispatchEvent(dispatchedEvent); expect(recievedEvent).toBe(dispatchedEvent); - expect(((recievedEvent)).target).toBe(eventTarget); - expect(((recievedEvent)).currentTarget).toBe(eventTarget); + expect(recievedTarget).toBe(eventTarget); + expect(recievedCurrentTarget).toBe(eventTarget); }); it('Triggers all listeners, even though listeners are removed while dispatching.', () => { let recievedEvent1: Event | null = null; + let recievedTarget1: EventTarget | null = null; + let recievedCurrentTarget1: EventTarget | null = null; let recievedEvent2: Event | null = null; + let recievedTarget2: EventTarget | null = null; + let recievedCurrentTarget2: EventTarget | null = null; + const listener1 = (event: Event): void => { recievedEvent1 = event; + recievedTarget1 = event.target; + recievedCurrentTarget1 = event.currentTarget; eventTarget.removeEventListener(EVENT_TYPE, listener1); }; const listener2 = (event: Event): void => { recievedEvent2 = event; + recievedTarget2 = event.target; + recievedCurrentTarget2 = event.currentTarget; eventTarget.removeEventListener(EVENT_TYPE, listener2); }; const dispatchedEvent = new Event(EVENT_TYPE); @@ -167,24 +192,30 @@ describe('EventTarget', () => { expect(recievedEvent1).toBe(dispatchedEvent); expect(recievedEvent2).toBe(dispatchedEvent); - expect(dispatchedEvent.target).toBe(eventTarget); - expect(dispatchedEvent.currentTarget).toBe(eventTarget); + expect(recievedTarget1).toBe(eventTarget); + expect(recievedCurrentTarget1).toBe(eventTarget); + expect(recievedTarget2).toBe(eventTarget); + expect(recievedCurrentTarget2).toBe(eventTarget); }); }); describe('attachEvent()', () => { it('Adds an event listener in older browsers for backward compatibility.', () => { let recievedEvent: Event | null = null; + let recievedTarget: EventTarget | null = null; + let recievedCurrentTarget: EventTarget | null = null; const listener = (event: Event): void => { recievedEvent = event; + recievedTarget = event.target; + recievedCurrentTarget = event.currentTarget; }; const dispatchedEvent = new Event(EVENT_TYPE); eventTarget.attachEvent(`on${EVENT_TYPE}`, listener); eventTarget.dispatchEvent(dispatchedEvent); expect(recievedEvent).toBe(dispatchedEvent); expect(((recievedEvent)).type).toBe(EVENT_TYPE); - expect(((recievedEvent)).target).toBe(eventTarget); - expect(((recievedEvent)).currentTarget).toBe(eventTarget); + expect(recievedTarget).toBe(eventTarget); + expect(recievedCurrentTarget).toBe(eventTarget); }); }); diff --git a/packages/happy-dom/test/fetch/AbortController.test.ts b/packages/happy-dom/test/fetch/AbortController.test.ts index 162988466..489c44c66 100644 --- a/packages/happy-dom/test/fetch/AbortController.test.ts +++ b/packages/happy-dom/test/fetch/AbortController.test.ts @@ -1,11 +1,19 @@ import Event from '../../src/event/Event.js'; import AbortController from '../../src/fetch/AbortController.js'; -import { describe, it, expect } from 'vitest'; +import { describe, it, expect, beforeEach } from 'vitest'; +import BrowserWindow from '../../src/window/BrowserWindow.js'; +import Window from '../../src/window/Window.js'; describe('AbortController', () => { + let window: BrowserWindow; + + beforeEach(() => { + window = new Window(); + }); + describe('abort()', () => { it('Aborts the signal.', () => { - const controller = new AbortController(); + const controller = new window.AbortController(); const signal = controller.signal; const reason = new Error('abort reason'); let triggeredEvent: Event | null = null; diff --git a/packages/happy-dom/test/fetch/AbortSignal.test.ts b/packages/happy-dom/test/fetch/AbortSignal.test.ts index 10b086d36..7c0e30c88 100644 --- a/packages/happy-dom/test/fetch/AbortSignal.test.ts +++ b/packages/happy-dom/test/fetch/AbortSignal.test.ts @@ -1,12 +1,19 @@ -import AbortSignal from '../../src/fetch/AbortSignal.js'; import Event from '../../src/event/Event.js'; -import { describe, it, expect } from 'vitest'; +import { describe, it, expect, beforeEach } from 'vitest'; import * as PropertySymbol from '../../src/PropertySymbol.js'; +import BrowserWindow from '../../src/window/BrowserWindow.js'; +import Window from '../../src/window/Window.js'; describe('AbortSignal', () => { + let window: BrowserWindow; + + beforeEach(() => { + window = new Window(); + }); + describe('[PropertySymbol.abort]()', () => { it('Aborts the signal.', () => { - const signal = new AbortSignal(); + const signal = new window.AbortSignal(); const reason = new Error('abort reason'); let triggeredEvent: Event | null = null; @@ -22,7 +29,7 @@ describe('AbortSignal', () => { describe('throwIfAborted()', () => { it('Throws an "AbortError" if the signal has been aborted.', () => { - const signal = new AbortSignal(); + const signal = new window.AbortSignal(); const reason = new Error('abort reason'); expect(() => signal.throwIfAborted()).not.toThrow(reason); @@ -36,7 +43,7 @@ describe('AbortSignal', () => { describe('AbortSignal.abort()', () => { it('Returns a new instance of AbortSignal.', () => { const reason = new Error('abort reason'); - const signal = AbortSignal.abort(reason); + const signal = window.AbortSignal.abort(reason); expect(signal.aborted).toBe(true); expect(signal.reason).toBe(reason); @@ -46,7 +53,7 @@ describe('AbortSignal', () => { describe('AbortSignal[Symbol.toStringTag]', () => { it('Returns AbortSignal string.', () => { const description = 'AbortSignal'; - const signal = new AbortSignal(); + const signal = new window.AbortSignal(); expect(signal[Symbol.toStringTag]).toBe(description); }); diff --git a/packages/happy-dom/test/fetch/Fetch.test.ts b/packages/happy-dom/test/fetch/Fetch.test.ts index ed97c5c81..5078fe75e 100644 --- a/packages/happy-dom/test/fetch/Fetch.test.ts +++ b/packages/happy-dom/test/fetch/Fetch.test.ts @@ -17,6 +17,7 @@ import { ReadableStream } from 'stream/web'; import { afterEach, describe, it, expect, vi } from 'vitest'; import FetchHTTPSCertificate from '../../src/fetch/certificate/FetchHTTPSCertificate.js'; import * as PropertySymbol from '../../src/PropertySymbol.js'; +import Event from '../../src/event/Event.js'; const LAST_CHUNK = Buffer.from('0\r\n\r\n'); @@ -47,7 +48,7 @@ describe('Fetch', () => { expect(error).toEqual( new DOMException( - `Failed to construct 'Request. Invalid URL "${url}" on document location 'about:blank'. Relative URLs are not permitted on current document location.`, + `Failed to construct 'Request': Invalid URL "${url}" on document location 'about:blank'. Relative URLs are not permitted on current document location.`, DOMExceptionNameEnum.notSupportedError ) ); @@ -66,7 +67,7 @@ describe('Fetch', () => { expect(error).toEqual( new DOMException( - `Failed to construct 'Request. Invalid URL "${url}" on document location 'about:blank'. Relative URLs are not permitted on current document location.`, + `Failed to construct 'Request': Invalid URL "${url}" on document location 'about:blank'. Relative URLs are not permitted on current document location.`, DOMExceptionNameEnum.notSupportedError ) ); @@ -1426,7 +1427,7 @@ describe('Fetch', () => { expect(error).toEqual( new DOMException( - `Fetch to "${url}" failed. Error: connect ECONNREFUSED ::1:8080`, + `Failed to execute "fetch()" on "Window" with URL "${url}": connect ECONNREFUSED ::1:8080`, DOMExceptionNameEnum.networkError ) ); @@ -2138,7 +2139,7 @@ describe('Fetch', () => { } }); - const abortController = new AbortController(); + const abortController = new window.AbortController(); const abortSignal = abortController.signal; let error: Error | null = null; @@ -2196,7 +2197,7 @@ describe('Fetch', () => { } }); - const abortController = new AbortController(); + const abortController = new window.AbortController(); const abortSignal = abortController.signal; let error: Error | null = null; @@ -2249,7 +2250,7 @@ describe('Fetch', () => { } }); - const abortController = new AbortController(); + const abortController = new window.AbortController(); const abortSignal = abortController.signal; let error1: Error | null = null; let error2: Error | null = null; @@ -2292,7 +2293,7 @@ describe('Fetch', () => { const window = new Window({ url: 'https://localhost:8080/' }); const url = 'https://localhost:8080/test/'; - const abortController = new AbortController(); + const abortController = new window.AbortController(); const abortSignal = abortController.signal; abortController.abort(); @@ -2338,7 +2339,7 @@ describe('Fetch', () => { } }); - const abortController = new AbortController(); + const abortController = new window.AbortController(); const abortSignal = abortController.signal; const response = await window.fetch(url, { method: 'GET', signal: abortSignal }); let error: Error | null = null; @@ -2402,7 +2403,7 @@ describe('Fetch', () => { } }); - const abortController = new AbortController(); + const abortController = new window.AbortController(); const abortSignal = abortController.signal; const response = await window.fetch(url, { method: 'GET', signal: abortSignal }); let error: Error | null = null; @@ -2476,7 +2477,7 @@ describe('Fetch', () => { } }); - const abortController = new AbortController(); + const abortController = new window.AbortController(); const abortSignal = abortController.signal; let error: Error | null = null; @@ -2524,13 +2525,13 @@ describe('Fetch', () => { } }); - const abortController = new AbortController(); + const abortController = new window.AbortController(); const abortSignal = abortController.signal; const response = await window.fetch(url, { method: 'GET', signal: abortSignal }); await response.text(); - expect(abortSignal[PropertySymbol.listeners]['abort']).toEqual([]); + expect(abortSignal[PropertySymbol.listeners].bubbling.get('abort')).toEqual([]); expect(() => abortController.abort()).not.toThrow(); }); @@ -3276,7 +3277,7 @@ describe('Fetch', () => { expect(error).toEqual( new DOMException( - `Fetch to "https://localhost:8080/test/" failed. Error: test`, + `Failed to execute "fetch()" on "Window" with URL "https://localhost:8080/test/": test`, DOMExceptionNameEnum.networkError ) ); diff --git a/packages/happy-dom/test/fetch/Request.test.ts b/packages/happy-dom/test/fetch/Request.test.ts index e7378cfb8..d7c5e91f6 100644 --- a/packages/happy-dom/test/fetch/Request.test.ts +++ b/packages/happy-dom/test/fetch/Request.test.ts @@ -91,7 +91,7 @@ describe('Request', () => { expect(error).toEqual( new DOMException( - `Failed to construct 'Request. Invalid URL "/path/" on document location 'about:blank'. Relative URLs are not permitted on current document location.`, + `Failed to construct 'Request': Invalid URL "/path/" on document location 'about:blank'. Relative URLs are not permitted on current document location.`, DOMExceptionNameEnum.notSupportedError ) ); @@ -285,13 +285,13 @@ describe('Request', () => { }); it('Supports signal from Request object.', () => { - const signal = new AbortSignal(); + const signal = new window.AbortSignal(); const request = new window.Request(new window.Request(TEST_URL, { signal })); expect(request.signal).toBe(signal); }); it('Supports signal from init object.', () => { - const signal = new AbortSignal(); + const signal = new window.AbortSignal(); const request = new window.Request(TEST_URL, { signal }); expect(request.signal).toBe(signal); }); @@ -804,7 +804,7 @@ describe('Request', () => { it('Returns a clone.', async () => { window.happyDOM?.setURL('https://example.com/other/path/'); - const signal = new AbortSignal(); + const signal = new window.AbortSignal(); const request = new window.Request(TEST_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, diff --git a/packages/happy-dom/test/fetch/SyncFetch.test.ts b/packages/happy-dom/test/fetch/SyncFetch.test.ts index def747b47..03cdf7cc7 100644 --- a/packages/happy-dom/test/fetch/SyncFetch.test.ts +++ b/packages/happy-dom/test/fetch/SyncFetch.test.ts @@ -61,7 +61,7 @@ describe('SyncFetch', () => { expect(error).toEqual( new DOMException( - `Failed to construct 'Request. Invalid URL "${url}" on document location 'about:blank'. Relative URLs are not permitted on current document location.`, + `Failed to construct 'Request': Invalid URL "${url}" on document location 'about:blank'. Relative URLs are not permitted on current document location.`, DOMExceptionNameEnum.notSupportedError ) ); @@ -86,7 +86,7 @@ describe('SyncFetch', () => { expect(error).toEqual( new DOMException( - `Failed to construct 'Request. Invalid URL "${url}" on document location 'about:blank'. Relative URLs are not permitted on current document location.`, + `Failed to construct 'Request': Invalid URL "${url}" on document location 'about:blank'. Relative URLs are not permitted on current document location.`, DOMExceptionNameEnum.notSupportedError ) ); @@ -1548,7 +1548,7 @@ describe('SyncFetch', () => { const url = 'https://localhost:8080/test/'; - const abortController = new AbortController(); + const abortController = new window.AbortController(); const abortSignal = abortController.signal; abortController.abort(); diff --git a/packages/happy-dom/test/history/History.test.ts b/packages/happy-dom/test/history/History.test.ts index 6aaccd999..e326cf816 100644 --- a/packages/happy-dom/test/history/History.test.ts +++ b/packages/happy-dom/test/history/History.test.ts @@ -1,6 +1,5 @@ import IBrowserFrame from '../../src/browser/types/IBrowserFrame.js'; import Browser from '../../src/browser/Browser.js'; -import History from '../../src/history/History.js'; import HistoryScrollRestorationEnum from '../../src/history/HistoryScrollRestorationEnum.js'; import { beforeEach, describe, it, expect, vi } from 'vitest'; import * as PropertySymbol from '../../src/PropertySymbol.js'; @@ -10,11 +9,9 @@ import Response from '../../src/fetch/Response'; describe('History', () => { let browserFrame: IBrowserFrame; - let history: History; beforeEach(() => { browserFrame = new Browser().newPage().mainFrame; - history = new History(browserFrame, browserFrame.window); }); describe('get length()', () => { @@ -40,47 +37,51 @@ describe('History', () => { }); // 3 as the first item is added as "about:blank" in the constructor. - expect(history.length).toBe(3); + expect(browserFrame.window.history.length).toBe(3); }); }); describe('get state()', () => { it('Returns "null" if no state as been set.', () => { - expect(history.state).toBe(null); + expect(browserFrame.window.history.state).toBe(null); }); it('Returns the state if set by pushState().', () => { - history.pushState({ key: 'value' }, '', ''); - expect(history.state).toEqual({ key: 'value' }); + browserFrame.window.history.pushState({ key: 'value' }, '', ''); + expect(browserFrame.window.history.state).toEqual({ key: 'value' }); }); it('Returns the state if set by replaceState().', () => { - history.replaceState({ key: 'value' }, '', ''); - expect(history.state).toEqual({ key: 'value' }); + browserFrame.window.history.replaceState({ key: 'value' }, '', ''); + expect(browserFrame.window.history.state).toEqual({ key: 'value' }); }); }); describe('get scrollRestoration()', () => { it('Returns "auto" by default.', () => { - expect(history.scrollRestoration).toBe(HistoryScrollRestorationEnum.auto); + expect(browserFrame.window.history.scrollRestoration).toBe(HistoryScrollRestorationEnum.auto); }); it('Returns set scroll restoration.', () => { - history.scrollRestoration = HistoryScrollRestorationEnum.manual; - expect(history.scrollRestoration).toBe(HistoryScrollRestorationEnum.manual); + browserFrame.window.history.scrollRestoration = HistoryScrollRestorationEnum.manual; + expect(browserFrame.window.history.scrollRestoration).toBe( + HistoryScrollRestorationEnum.manual + ); }); }); describe('set scrollRestoration()', () => { it('Is not possible to set an invalid value.', () => { // @ts-ignore - history.scrollRestoration = 'invalid'; - expect(history.scrollRestoration).toBe(HistoryScrollRestorationEnum.auto); + browserFrame.window.history.scrollRestoration = 'invalid'; + expect(browserFrame.window.history.scrollRestoration).toBe(HistoryScrollRestorationEnum.auto); }); it('Is possible to set to "manual".', () => { - history.scrollRestoration = HistoryScrollRestorationEnum.manual; - expect(history.scrollRestoration).toBe(HistoryScrollRestorationEnum.manual); + browserFrame.window.history.scrollRestoration = HistoryScrollRestorationEnum.manual; + expect(browserFrame.window.history.scrollRestoration).toBe( + HistoryScrollRestorationEnum.manual + ); }); }); @@ -139,25 +140,24 @@ describe('History', () => { isCurrent: true }); - history.back(); + browserFrame.window.history.back(); await browserFrame.waitForNavigation(); expect(browserFrame.window.location.href).toBe('https://www.example.com/'); - history.back(); - + browserFrame.window.history.back(); await browserFrame.waitForNavigation(); expect(browserFrame.window.location.href).toBe('https://www.github.com/'); - history.back(); + browserFrame.window.history.back(); await browserFrame.waitForNavigation(); expect(browserFrame.window.location.href).toBe('about:blank'); - history.back(); + browserFrame.window.history.back(); await browserFrame.waitForNavigation(); @@ -220,31 +220,31 @@ describe('History', () => { isCurrent: true }); - history.back(); + browserFrame.window.history.back(); await browserFrame.waitForNavigation(); expect(browserFrame.window.location.href).toBe('https://www.example.com/'); - history.back(); + browserFrame.window.history.back(); await browserFrame.waitForNavigation(); expect(browserFrame.window.location.href).toBe('https://www.github.com/'); - history.forward(); + browserFrame.window.history.forward(); await browserFrame.waitForNavigation(); expect(browserFrame.window.location.href).toBe('https://www.example.com/'); - history.forward(); + browserFrame.window.history.forward(); await browserFrame.waitForNavigation(); expect(browserFrame.window.location.href).toBe('https://localhost:3000/'); - history.forward(); + browserFrame.window.history.forward(); await browserFrame.waitForNavigation(); @@ -307,26 +307,26 @@ describe('History', () => { isCurrent: true }); - history.go(-2); + browserFrame.window.history.go(-2); await browserFrame.waitForNavigation(); expect(browserFrame.window.location.href).toBe('https://www.github.com/'); - history.go(1); + browserFrame.window.history.go(1); await browserFrame.waitForNavigation(); expect(browserFrame.window.location.href).toBe('https://www.example.com/'); // Shouldn't navigate as there is no history item at this position. - history.go(2); + browserFrame.window.history.go(2); await browserFrame.waitForNavigation(); expect(browserFrame.window.location.href).toBe('https://www.example.com/'); - history.go(1); + browserFrame.window.history.go(1); await browserFrame.waitForNavigation(); @@ -336,9 +336,9 @@ describe('History', () => { describe('pushState()', () => { it('Pushes a new state to the history.', () => { - history.pushState({ key: 'value' }, '', ''); + browserFrame.window.history.pushState({ key: 'value' }, '', ''); - expect(history.state).toEqual({ key: 'value' }); + expect(browserFrame.window.history.state).toEqual({ key: 'value' }); expect(browserFrame[PropertySymbol.history]).toEqual([ { @@ -365,9 +365,9 @@ describe('History', () => { describe('replaceState()', () => { it('Replaces the current state in the history.', () => { - history.pushState({ key: 'value' }, '', ''); + browserFrame.window.history.pushState({ key: 'value' }, '', ''); - expect(history.state).toEqual({ key: 'value' }); + expect(browserFrame.window.history.state).toEqual({ key: 'value' }); expect(browserFrame[PropertySymbol.history]).toEqual([ { @@ -390,9 +390,9 @@ describe('History', () => { } ]); - history.replaceState({ key: 'value2' }, '', ''); + browserFrame.window.history.replaceState({ key: 'value2' }, '', ''); - expect(history.state).toEqual({ key: 'value2' }); + expect(browserFrame.window.history.state).toEqual({ key: 'value2' }); expect(browserFrame[PropertySymbol.history]).toEqual([ { diff --git a/packages/happy-dom/test/match-media/MediaQueryList.test.ts b/packages/happy-dom/test/match-media/MediaQueryList.test.ts index a0cb994d5..f9be18d8b 100644 --- a/packages/happy-dom/test/match-media/MediaQueryList.test.ts +++ b/packages/happy-dom/test/match-media/MediaQueryList.test.ts @@ -12,57 +12,54 @@ describe('MediaQueryList', () => { describe('get media()', () => { it('Returns media string.', () => { - expect(new MediaQueryList({ ownerWindow: window, media: '(min-width: 1023px)' }).media).toBe( + expect(new MediaQueryList({ window: window, media: '(min-width: 1023px)' }).media).toBe( '(min-width: 1023px)' ); expect( - new MediaQueryList({ ownerWindow: window, media: 'PRINT and (MAX-width: 1024px)' }).media + new MediaQueryList({ window: window, media: 'PRINT and (MAX-width: 1024px)' }).media ).toBe('print and (max-width: 1024px)'); expect( - new MediaQueryList({ ownerWindow: window, media: 'NOT all AND (prefers-COLOR-scheme)' }) - .media + new MediaQueryList({ window: window, media: 'NOT all AND (prefers-COLOR-scheme)' }).media ).toBe('not all and (prefers-color-scheme)'); - expect(new MediaQueryList({ ownerWindow: window, media: 'all and (hover: none' }).media).toBe( + expect(new MediaQueryList({ window: window, media: 'all and (hover: none' }).media).toBe( 'all and (hover: none)' ); expect( new MediaQueryList({ - ownerWindow: window, + window: window, media: 'all and (400px <= height <= 2000px) and (400px <= width <= 2000px)' }).media ).toBe('all and (400px <= height <= 2000px) and (400px <= width <= 2000px)'); expect( new MediaQueryList({ - ownerWindow: window, + window: window, media: 'all and (400px <= height <= 2000px) and (400px <= width <= 2000px) and (min-width: 400px)' }).media ).toBe( 'all and (400px <= height <= 2000px) and (400px <= width <= 2000px) and (min-width: 400px)' ); - expect(new MediaQueryList({ ownerWindow: window, media: 'prefers-color-scheme' }).media).toBe( - '' + expect(new MediaQueryList({ window: window, media: 'prefers-color-scheme' }).media).toBe(''); + expect(new MediaQueryList({ window: window, media: '(prefers-color-scheme' }).media).toBe( + 'not all' + ); + expect(new MediaQueryList({ window: window, media: '(prefers-color-scheme)' }).media).toBe( + '(prefers-color-scheme)' ); - expect( - new MediaQueryList({ ownerWindow: window, media: '(prefers-color-scheme' }).media - ).toBe('not all'); - expect( - new MediaQueryList({ ownerWindow: window, media: '(prefers-color-scheme)' }).media - ).toBe('(prefers-color-scheme)'); }); }); describe('get matches()', () => { it('Handles media type with name "all".', () => { expect( - new MediaQueryList({ ownerWindow: window, media: 'all and (min-width: 1024px)' }).matches + new MediaQueryList({ window: window, media: 'all and (min-width: 1024px)' }).matches ).toBe(true); }); it('Handles media type with name "print".', () => { - expect(new MediaQueryList({ ownerWindow: window, media: 'print' }).matches).toBe(false); + expect(new MediaQueryList({ window: window, media: 'print' }).matches).toBe(false); expect( - new MediaQueryList({ ownerWindow: window, media: 'print and (min-width: 1024px)' }).matches + new MediaQueryList({ window: window, media: 'print and (min-width: 1024px)' }).matches ).toBe(false); window = new Window({ @@ -71,205 +68,190 @@ describe('MediaQueryList', () => { settings: { device: { mediaType: 'print' } } }); - expect(new MediaQueryList({ ownerWindow: window, media: 'print' }).matches).toBe(true); + expect(new MediaQueryList({ window: window, media: 'print' }).matches).toBe(true); expect( - new MediaQueryList({ ownerWindow: window, media: 'print and (min-width: 1024px)' }).matches + new MediaQueryList({ window: window, media: 'print and (min-width: 1024px)' }).matches ).toBe(true); }); it('Handles media type with name "screen".', () => { - expect(new MediaQueryList({ ownerWindow: window, media: 'screen' }).matches).toBe(true); + expect(new MediaQueryList({ window: window, media: 'screen' }).matches).toBe(true); expect( - new MediaQueryList({ ownerWindow: window, media: 'screen and (min-width: 1024px)' }).matches + new MediaQueryList({ window: window, media: 'screen and (min-width: 1024px)' }).matches ).toBe(true); }); it('Handles "not" keyword.', () => { - expect(new MediaQueryList({ ownerWindow: window, media: 'not all' }).matches).toBe(false); - expect(new MediaQueryList({ ownerWindow: window, media: 'not print' }).matches).toBe(true); - expect( - new MediaQueryList({ ownerWindow: window, media: 'not (min-width: 1025px)' }).matches - ).toBe(true); - expect( - new MediaQueryList({ ownerWindow: window, media: 'not (min-width: 1024px)' }).matches - ).toBe(false); + expect(new MediaQueryList({ window: window, media: 'not all' }).matches).toBe(false); + expect(new MediaQueryList({ window: window, media: 'not print' }).matches).toBe(true); + expect(new MediaQueryList({ window: window, media: 'not (min-width: 1025px)' }).matches).toBe( + true + ); + expect(new MediaQueryList({ window: window, media: 'not (min-width: 1024px)' }).matches).toBe( + false + ); }); it('Handles "only" keyword.', () => { - expect(new MediaQueryList({ ownerWindow: window, media: 'only all' }).matches).toBe(true); - expect(new MediaQueryList({ ownerWindow: window, media: 'only print' }).matches).toBe(false); + expect(new MediaQueryList({ window: window, media: 'only all' }).matches).toBe(true); + expect(new MediaQueryList({ window: window, media: 'only print' }).matches).toBe(false); expect( - new MediaQueryList({ ownerWindow: window, media: 'only screen and (min-width: 1024px)' }) - .matches + new MediaQueryList({ window: window, media: 'only screen and (min-width: 1024px)' }).matches ).toBe(true); }); it('Handles "min-width".', () => { - expect(new MediaQueryList({ ownerWindow: window, media: '(min-width)' }).matches).toBe(true); - expect( - new MediaQueryList({ ownerWindow: window, media: '(min-width: 1025px)' }).matches - ).toBe(false); - expect( - new MediaQueryList({ ownerWindow: window, media: '(min-width: 1024px)' }).matches - ).toBe(true); + expect(new MediaQueryList({ window: window, media: '(min-width)' }).matches).toBe(true); + expect(new MediaQueryList({ window: window, media: '(min-width: 1025px)' }).matches).toBe( + false + ); + expect(new MediaQueryList({ window: window, media: '(min-width: 1024px)' }).matches).toBe( + true + ); expect( - new MediaQueryList({ ownerWindow: window, media: `(min-width: ${1025 / 16}rem)` }).matches + new MediaQueryList({ window: window, media: `(min-width: ${1025 / 16}rem)` }).matches ).toBe(false); expect( - new MediaQueryList({ ownerWindow: window, media: `(min-width: ${1024 / 16}rem)` }).matches + new MediaQueryList({ window: window, media: `(min-width: ${1024 / 16}rem)` }).matches ).toBe(true); expect( - new MediaQueryList({ ownerWindow: window, media: `(min-width: ${1025 / 16}em)` }).matches + new MediaQueryList({ window: window, media: `(min-width: ${1025 / 16}em)` }).matches ).toBe(false); expect( - new MediaQueryList({ ownerWindow: window, media: `(min-width: ${1024 / 16}em)` }).matches + new MediaQueryList({ window: window, media: `(min-width: ${1024 / 16}em)` }).matches ).toBe(true); - expect(new MediaQueryList({ ownerWindow: window, media: '(min-width: 101vw)' }).matches).toBe( + expect(new MediaQueryList({ window: window, media: '(min-width: 101vw)' }).matches).toBe( false ); - expect(new MediaQueryList({ ownerWindow: window, media: '(min-width: 100vw)' }).matches).toBe( + expect(new MediaQueryList({ window: window, media: '(min-width: 100vw)' }).matches).toBe( true ); // Percentages should never match - expect(new MediaQueryList({ ownerWindow: window, media: '(min-width: 0%)' }).matches).toBe( - false - ); + expect(new MediaQueryList({ window: window, media: '(min-width: 0%)' }).matches).toBe(false); window.document.documentElement.style.fontSize = '10px'; expect( - new MediaQueryList({ ownerWindow: window, media: `(min-width: ${1025 / 10}rem)` }).matches + new MediaQueryList({ window: window, media: `(min-width: ${1025 / 10}rem)` }).matches ).toBe(false); expect( - new MediaQueryList({ ownerWindow: window, media: `(min-width: ${1024 / 10}rem)` }).matches + new MediaQueryList({ window: window, media: `(min-width: ${1024 / 10}rem)` }).matches ).toBe(true); expect( - new MediaQueryList({ ownerWindow: window, media: `(min-width: ${1025 / 10}em)` }).matches + new MediaQueryList({ window: window, media: `(min-width: ${1025 / 10}em)` }).matches ).toBe(false); expect( - new MediaQueryList({ ownerWindow: window, media: `(min-width: ${1024 / 10}em)` }).matches + new MediaQueryList({ window: window, media: `(min-width: ${1024 / 10}em)` }).matches ).toBe(true); }); it('Handles "max-width".', () => { - expect(new MediaQueryList({ ownerWindow: window, media: '(max-width)' }).matches).toBe(true); - expect( - new MediaQueryList({ ownerWindow: window, media: '(max-width: 1023px)' }).matches - ).toBe(false); - expect( - new MediaQueryList({ ownerWindow: window, media: '(max-width: 1024px)' }).matches - ).toBe(true); + expect(new MediaQueryList({ window: window, media: '(max-width)' }).matches).toBe(true); + expect(new MediaQueryList({ window: window, media: '(max-width: 1023px)' }).matches).toBe( + false + ); + expect(new MediaQueryList({ window: window, media: '(max-width: 1024px)' }).matches).toBe( + true + ); expect( - new MediaQueryList({ ownerWindow: window, media: `(max-width: ${1023 / 16}rem)` }).matches + new MediaQueryList({ window: window, media: `(max-width: ${1023 / 16}rem)` }).matches ).toBe(false); expect( - new MediaQueryList({ ownerWindow: window, media: `(max-width: ${1024 / 16}rem)` }).matches + new MediaQueryList({ window: window, media: `(max-width: ${1024 / 16}rem)` }).matches ).toBe(true); }); it('Handles "min-height".', () => { - expect(new MediaQueryList({ ownerWindow: window, media: '(min-height)' }).matches).toBe(true); - expect( - new MediaQueryList({ ownerWindow: window, media: '(min-height: 769px)' }).matches - ).toBe(false); - expect( - new MediaQueryList({ ownerWindow: window, media: '(min-height: 768px)' }).matches - ).toBe(true); - - expect( - new MediaQueryList({ ownerWindow: window, media: '(min-height: 101vh)' }).matches - ).toBe(false); - expect( - new MediaQueryList({ ownerWindow: window, media: '(min-height: 100vh)' }).matches - ).toBe(true); + expect(new MediaQueryList({ window: window, media: '(min-height)' }).matches).toBe(true); + expect(new MediaQueryList({ window: window, media: '(min-height: 769px)' }).matches).toBe( + false + ); + expect(new MediaQueryList({ window: window, media: '(min-height: 768px)' }).matches).toBe( + true + ); - // Percentages should never match - expect(new MediaQueryList({ ownerWindow: window, media: '(min-height: 0%)' }).matches).toBe( + expect(new MediaQueryList({ window: window, media: '(min-height: 101vh)' }).matches).toBe( false ); + expect(new MediaQueryList({ window: window, media: '(min-height: 100vh)' }).matches).toBe( + true + ); + + // Percentages should never match + expect(new MediaQueryList({ window: window, media: '(min-height: 0%)' }).matches).toBe(false); expect( - new MediaQueryList({ ownerWindow: window, media: `(min-height: ${769 / 16}rem)` }).matches + new MediaQueryList({ window: window, media: `(min-height: ${769 / 16}rem)` }).matches ).toBe(false); expect( - new MediaQueryList({ ownerWindow: window, media: `(min-height: ${768 / 16}rem)` }).matches + new MediaQueryList({ window: window, media: `(min-height: ${768 / 16}rem)` }).matches ).toBe(true); }); it('Handles "max-height".', () => { - expect(new MediaQueryList({ ownerWindow: window, media: '(max-height)' }).matches).toBe(true); - expect( - new MediaQueryList({ ownerWindow: window, media: '(max-height: 767px)' }).matches - ).toBe(false); - expect( - new MediaQueryList({ ownerWindow: window, media: '(max-height: 768px)' }).matches - ).toBe(true); + expect(new MediaQueryList({ window: window, media: '(max-height)' }).matches).toBe(true); + expect(new MediaQueryList({ window: window, media: '(max-height: 767px)' }).matches).toBe( + false + ); + expect(new MediaQueryList({ window: window, media: '(max-height: 768px)' }).matches).toBe( + true + ); expect( - new MediaQueryList({ ownerWindow: window, media: `(max-height: ${767 / 16}rem)` }).matches + new MediaQueryList({ window: window, media: `(max-height: ${767 / 16}rem)` }).matches ).toBe(false); expect( - new MediaQueryList({ ownerWindow: window, media: `(max-height: ${768 / 16}rem)` }).matches + new MediaQueryList({ window: window, media: `(max-height: ${768 / 16}rem)` }).matches ).toBe(true); }); it('Handles "width".', () => { - expect(new MediaQueryList({ ownerWindow: window, media: '(width)' }).matches).toBe(true); - expect(new MediaQueryList({ ownerWindow: window, media: '(width: 1023px)' }).matches).toBe( - false - ); - expect(new MediaQueryList({ ownerWindow: window, media: '(width: 1024px)' }).matches).toBe( - true - ); + expect(new MediaQueryList({ window: window, media: '(width)' }).matches).toBe(true); + expect(new MediaQueryList({ window: window, media: '(width: 1023px)' }).matches).toBe(false); + expect(new MediaQueryList({ window: window, media: '(width: 1024px)' }).matches).toBe(true); }); it('Handles "height".', () => { - expect(new MediaQueryList({ ownerWindow: window, media: '(height)' }).matches).toBe(true); - expect(new MediaQueryList({ ownerWindow: window, media: '(height: 767px)' }).matches).toBe( - false - ); - expect(new MediaQueryList({ ownerWindow: window, media: '(height: 768px)' }).matches).toBe( - true - ); + expect(new MediaQueryList({ window: window, media: '(height)' }).matches).toBe(true); + expect(new MediaQueryList({ window: window, media: '(height: 767px)' }).matches).toBe(false); + expect(new MediaQueryList({ window: window, media: '(height: 768px)' }).matches).toBe(true); }); it('Handles "orientation".', () => { - expect(new MediaQueryList({ ownerWindow: window, media: '(orientation)' }).matches).toBe( - true + expect(new MediaQueryList({ window: window, media: '(orientation)' }).matches).toBe(true); + expect(new MediaQueryList({ window: window, media: '(orientation: portrait)' }).matches).toBe( + false ); expect( - new MediaQueryList({ ownerWindow: window, media: '(orientation: portrait)' }).matches - ).toBe(false); - expect( - new MediaQueryList({ ownerWindow: window, media: '(orientation: landscape)' }).matches + new MediaQueryList({ window: window, media: '(orientation: landscape)' }).matches ).toBe(true); window.happyDOM?.setInnerWidth(500); window.happyDOM?.setInnerHeight(1000); + expect(new MediaQueryList({ window: window, media: '(orientation: portrait)' }).matches).toBe( + true + ); expect( - new MediaQueryList({ ownerWindow: window, media: '(orientation: portrait)' }).matches - ).toBe(true); - expect( - new MediaQueryList({ ownerWindow: window, media: '(orientation: landscape)' }).matches + new MediaQueryList({ window: window, media: '(orientation: landscape)' }).matches ).toBe(false); }); it('Handles "prefers-color-scheme".', () => { + expect(new MediaQueryList({ window: window, media: '(prefers-color-scheme)' }).matches).toBe( + true + ); expect( - new MediaQueryList({ ownerWindow: window, media: '(prefers-color-scheme)' }).matches - ).toBe(true); - expect( - new MediaQueryList({ ownerWindow: window, media: '(prefers-color-scheme: dark)' }).matches + new MediaQueryList({ window: window, media: '(prefers-color-scheme: dark)' }).matches ).toBe(false); expect( - new MediaQueryList({ ownerWindow: window, media: '(prefers-color-scheme: light)' }).matches + new MediaQueryList({ window: window, media: '(prefers-color-scheme: light)' }).matches ).toBe(true); window = new Window({ @@ -279,195 +261,159 @@ describe('MediaQueryList', () => { }); expect( - new MediaQueryList({ ownerWindow: window, media: '(prefers-color-scheme: dark)' }).matches + new MediaQueryList({ window: window, media: '(prefers-color-scheme: dark)' }).matches ).toBe(true); expect( - new MediaQueryList({ ownerWindow: window, media: '(prefers-color-scheme: light)' }).matches + new MediaQueryList({ window: window, media: '(prefers-color-scheme: light)' }).matches ).toBe(false); }); it('Handles "hover".', () => { - expect(new MediaQueryList({ ownerWindow: window, media: '(hover)' }).matches).toBe(true); - expect(new MediaQueryList({ ownerWindow: window, media: '(hover: invalid)' }).matches).toBe( + expect(new MediaQueryList({ window: window, media: '(hover)' }).matches).toBe(true); + expect(new MediaQueryList({ window: window, media: '(hover: invalid)' }).matches).toBe(false); + expect(new MediaQueryList({ window: window, media: '(hover: none)' }).matches).toBe(false); + expect(new MediaQueryList({ window: window, media: '(hover: hover)' }).matches).toBe(true); + }); + + it('Handles "pointer".', () => { + expect(new MediaQueryList({ window: window, media: '(pointer)' }).matches).toBe(true); + expect(new MediaQueryList({ window: window, media: '(pointer: invalid)' }).matches).toBe( false ); - expect(new MediaQueryList({ ownerWindow: window, media: '(hover: none)' }).matches).toBe( + expect(new MediaQueryList({ window: window, media: '(pointer: none)' }).matches).toBe(false); + expect(new MediaQueryList({ window: window, media: '(pointer: coarse)' }).matches).toBe( false ); - expect(new MediaQueryList({ ownerWindow: window, media: '(hover: hover)' }).matches).toBe( - true - ); + expect(new MediaQueryList({ window: window, media: '(pointer: fine)' }).matches).toBe(true); }); - it('Handles "pointer".', () => { - expect(new MediaQueryList({ ownerWindow: window, media: '(pointer)' }).matches).toBe(true); - expect(new MediaQueryList({ ownerWindow: window, media: '(pointer: invalid)' }).matches).toBe( + it('Handles "any-pointer".', () => { + expect(new MediaQueryList({ window: window, media: '(any-pointer)' }).matches).toBe(true); + expect(new MediaQueryList({ window: window, media: '(any-pointer: invalid)' }).matches).toBe( false ); - expect(new MediaQueryList({ ownerWindow: window, media: '(pointer: none)' }).matches).toBe( + expect(new MediaQueryList({ window: window, media: '(any-pointer: none)' }).matches).toBe( false ); - expect(new MediaQueryList({ ownerWindow: window, media: '(pointer: coarse)' }).matches).toBe( + expect(new MediaQueryList({ window: window, media: '(any-pointer: coarse)' }).matches).toBe( false ); - expect(new MediaQueryList({ ownerWindow: window, media: '(pointer: fine)' }).matches).toBe( + expect(new MediaQueryList({ window: window, media: '(any-pointer: fine)' }).matches).toBe( true ); }); - it('Handles "any-pointer".', () => { - expect(new MediaQueryList({ ownerWindow: window, media: '(any-pointer)' }).matches).toBe( - true - ); - expect( - new MediaQueryList({ ownerWindow: window, media: '(any-pointer: invalid)' }).matches - ).toBe(false); - expect( - new MediaQueryList({ ownerWindow: window, media: '(any-pointer: none)' }).matches - ).toBe(false); - expect( - new MediaQueryList({ ownerWindow: window, media: '(any-pointer: coarse)' }).matches - ).toBe(false); - expect( - new MediaQueryList({ ownerWindow: window, media: '(any-pointer: fine)' }).matches - ).toBe(true); - }); - it('Handles "display-mode".', () => { - expect(new MediaQueryList({ ownerWindow: window, media: '(display-mode)' }).matches).toBe( + expect(new MediaQueryList({ window: window, media: '(display-mode)' }).matches).toBe(true); + expect(new MediaQueryList({ window: window, media: '(display-mode: invalid)' }).matches).toBe( + false + ); + expect(new MediaQueryList({ window: window, media: '(display-mode: browser)' }).matches).toBe( true ); - expect( - new MediaQueryList({ ownerWindow: window, media: '(display-mode: invalid)' }).matches - ).toBe(false); - expect( - new MediaQueryList({ ownerWindow: window, media: '(display-mode: browser)' }).matches - ).toBe(true); }); it('Handles "min-aspect-ratio".', () => { - expect(new MediaQueryList({ ownerWindow: window, media: '(min-aspect-ratio)' }).matches).toBe( + expect(new MediaQueryList({ window: window, media: '(min-aspect-ratio)' }).matches).toBe( true ); expect( - new MediaQueryList({ ownerWindow: window, media: '(min-aspect-ratio: 1024/770)' }).matches + new MediaQueryList({ window: window, media: '(min-aspect-ratio: 1024/770)' }).matches ).toBe(true); expect( - new MediaQueryList({ ownerWindow: window, media: '(min-aspect-ratio: 1024/760)' }).matches + new MediaQueryList({ window: window, media: '(min-aspect-ratio: 1024/760)' }).matches ).toBe(false); }); it('Handles "max-aspect-ratio".', () => { - expect(new MediaQueryList({ ownerWindow: window, media: '(max-aspect-ratio)' }).matches).toBe( + expect(new MediaQueryList({ window: window, media: '(max-aspect-ratio)' }).matches).toBe( true ); expect( - new MediaQueryList({ ownerWindow: window, media: '(max-aspect-ratio: 1024/760)' }).matches + new MediaQueryList({ window: window, media: '(max-aspect-ratio: 1024/760)' }).matches ).toBe(true); expect( - new MediaQueryList({ ownerWindow: window, media: '(max-aspect-ratio: 1024/770)' }).matches + new MediaQueryList({ window: window, media: '(max-aspect-ratio: 1024/770)' }).matches ).toBe(false); }); it('Handles "aspect-ratio".', () => { - expect(new MediaQueryList({ ownerWindow: window, media: '(aspect-ratio)' }).matches).toBe( - true - ); + expect(new MediaQueryList({ window: window, media: '(aspect-ratio)' }).matches).toBe(true); expect( - new MediaQueryList({ ownerWindow: window, media: '(aspect-ratio: 1024/768)' }).matches + new MediaQueryList({ window: window, media: '(aspect-ratio: 1024/768)' }).matches ).toBe(true); expect( - new MediaQueryList({ ownerWindow: window, media: '(aspect-ratio: 1024/769)' }).matches + new MediaQueryList({ window: window, media: '(aspect-ratio: 1024/769)' }).matches ).toBe(false); expect( - new MediaQueryList({ ownerWindow: window, media: '(aspect-ratio: 1024/767)' }).matches + new MediaQueryList({ window: window, media: '(aspect-ratio: 1024/767)' }).matches ).toBe(false); }); it('Handles defining a resolution range using the range syntax.', () => { - expect(new MediaQueryList({ ownerWindow: window, media: '(400px <= width)' }).matches).toBe( - true - ); - expect(new MediaQueryList({ ownerWindow: window, media: '(400px < width)' }).matches).toBe( - true - ); - expect(new MediaQueryList({ ownerWindow: window, media: '(2000px < width)' }).matches).toBe( - false - ); + expect(new MediaQueryList({ window: window, media: '(400px <= width)' }).matches).toBe(true); + expect(new MediaQueryList({ window: window, media: '(400px < width)' }).matches).toBe(true); + expect(new MediaQueryList({ window: window, media: '(2000px < width)' }).matches).toBe(false); expect( - new MediaQueryList({ ownerWindow: window, media: '(400px <= width <= 2000px)' }).matches + new MediaQueryList({ window: window, media: '(400px <= width <= 2000px)' }).matches ).toBe(true); expect( - new MediaQueryList({ ownerWindow: window, media: '(400px <= width <= 1023px)' }).matches + new MediaQueryList({ window: window, media: '(400px <= width <= 1023px)' }).matches ).toBe(false); expect( - new MediaQueryList({ ownerWindow: window, media: '(400px <= width <= 1024px)' }).matches + new MediaQueryList({ window: window, media: '(400px <= width <= 1024px)' }).matches ).toBe(true); - expect(new MediaQueryList({ ownerWindow: window, media: '(2000px => width)' }).matches).toBe( - true - ); - expect(new MediaQueryList({ ownerWindow: window, media: '(2000px > width)' }).matches).toBe( - true - ); - expect(new MediaQueryList({ ownerWindow: window, media: '(700px > width)' }).matches).toBe( - false - ); + expect(new MediaQueryList({ window: window, media: '(2000px => width)' }).matches).toBe(true); + expect(new MediaQueryList({ window: window, media: '(2000px > width)' }).matches).toBe(true); + expect(new MediaQueryList({ window: window, media: '(700px > width)' }).matches).toBe(false); expect( - new MediaQueryList({ ownerWindow: window, media: `(${1024 / 16}rem <= width)` }).matches + new MediaQueryList({ window: window, media: `(${1024 / 16}rem <= width)` }).matches ).toBe(true); expect( - new MediaQueryList({ ownerWindow: window, media: `(${1024 / 16}em <= width)` }).matches + new MediaQueryList({ window: window, media: `(${1024 / 16}em <= width)` }).matches ).toBe(true); expect( - new MediaQueryList({ ownerWindow: window, media: `(${1024 / 16}rem < width)` }).matches + new MediaQueryList({ window: window, media: `(${1024 / 16}rem < width)` }).matches ).toBe(false); expect( - new MediaQueryList({ ownerWindow: window, media: `(${1024 / 16}em < width)` }).matches + new MediaQueryList({ window: window, media: `(${1024 / 16}em < width)` }).matches ).toBe(false); - expect(new MediaQueryList({ ownerWindow: window, media: '(400px <= height)' }).matches).toBe( - true - ); - expect(new MediaQueryList({ ownerWindow: window, media: '(400px < height)' }).matches).toBe( - true - ); - expect(new MediaQueryList({ ownerWindow: window, media: '(2000px < height)' }).matches).toBe( + expect(new MediaQueryList({ window: window, media: '(400px <= height)' }).matches).toBe(true); + expect(new MediaQueryList({ window: window, media: '(400px < height)' }).matches).toBe(true); + expect(new MediaQueryList({ window: window, media: '(2000px < height)' }).matches).toBe( false ); expect( - new MediaQueryList({ ownerWindow: window, media: '(400px <= height <= 2000px)' }).matches + new MediaQueryList({ window: window, media: '(400px <= height <= 2000px)' }).matches ).toBe(true); expect( - new MediaQueryList({ ownerWindow: window, media: '(400px <= height <= 767px)' }).matches + new MediaQueryList({ window: window, media: '(400px <= height <= 767px)' }).matches ).toBe(false); expect( - new MediaQueryList({ ownerWindow: window, media: '(400px <= height <= 768px)' }).matches + new MediaQueryList({ window: window, media: '(400px <= height <= 768px)' }).matches ).toBe(true); - expect(new MediaQueryList({ ownerWindow: window, media: '(2000px => height)' }).matches).toBe( + expect(new MediaQueryList({ window: window, media: '(2000px => height)' }).matches).toBe( true ); - expect(new MediaQueryList({ ownerWindow: window, media: '(2000px > height)' }).matches).toBe( - true - ); - expect(new MediaQueryList({ ownerWindow: window, media: '(700px > height)' }).matches).toBe( - false - ); + expect(new MediaQueryList({ window: window, media: '(2000px > height)' }).matches).toBe(true); + expect(new MediaQueryList({ window: window, media: '(700px > height)' }).matches).toBe(false); expect( - new MediaQueryList({ ownerWindow: window, media: `(${768 / 16}rem <= height)` }).matches + new MediaQueryList({ window: window, media: `(${768 / 16}rem <= height)` }).matches ).toBe(true); expect( - new MediaQueryList({ ownerWindow: window, media: `(${768 / 16}em <= height)` }).matches + new MediaQueryList({ window: window, media: `(${768 / 16}em <= height)` }).matches ).toBe(true); expect( - new MediaQueryList({ ownerWindow: window, media: `(${768 / 16}rem < height)` }).matches + new MediaQueryList({ window: window, media: `(${768 / 16}rem < height)` }).matches ).toBe(false); expect( - new MediaQueryList({ ownerWindow: window, media: `(${768 / 16}em < height)` }).matches + new MediaQueryList({ window: window, media: `(${768 / 16}em < height)` }).matches ).toBe(false); expect( new MediaQueryList({ - ownerWindow: window, + window: window, media: '(400px <= height <= 2000px) and (400px <= width <= 2000px)' }).matches ).toBe(true); @@ -476,19 +422,19 @@ describe('MediaQueryList', () => { it('Handles multiple rules.', () => { expect( new MediaQueryList({ - ownerWindow: window, + window: window, media: '(min-width: 1024px) and (max-width: 2000px)' }).matches ).toBe(true); expect( new MediaQueryList({ - ownerWindow: window, + window: window, media: '(min-width: 768px) and (max-width: 1023px)' }).matches ).toBe(false); expect( new MediaQueryList({ - ownerWindow: window, + window: window, media: 'screen and (min-width: 1024px) and (max-width: 2000px)' }).matches ).toBe(true); @@ -498,10 +444,10 @@ describe('MediaQueryList', () => { window.document.documentElement.style.fontSize = '10px'; expect( - new MediaQueryList({ ownerWindow: window, media: `(max-width: ${1023 / 10}rem)` }).matches + new MediaQueryList({ window: window, media: `(max-width: ${1023 / 10}rem)` }).matches ).toBe(false); expect( - new MediaQueryList({ ownerWindow: window, media: `(max-width: ${1024 / 10}rem)` }).matches + new MediaQueryList({ window: window, media: `(max-width: ${1024 / 10}rem)` }).matches ).toBe(true); window = new Window({ @@ -511,10 +457,10 @@ describe('MediaQueryList', () => { }); expect( - new MediaQueryList({ ownerWindow: window, media: `(max-width: ${1023 / 16}rem)` }).matches + new MediaQueryList({ window: window, media: `(max-width: ${1023 / 16}rem)` }).matches ).toBe(false); expect( - new MediaQueryList({ ownerWindow: window, media: `(max-width: ${1024 / 16}rem)` }).matches + new MediaQueryList({ window: window, media: `(max-width: ${1024 / 16}rem)` }).matches ).toBe(true); }); }); @@ -523,7 +469,7 @@ describe('MediaQueryList', () => { it('Listens for window "resize" event when sending in a "change" event.', () => { let triggeredEvent: MediaQueryListEvent | null = null; const media = '(min-width: 1025px)'; - const mediaQueryList = new MediaQueryList({ ownerWindow: window, media: media }); + const mediaQueryList = new MediaQueryList({ window: window, media: media }); mediaQueryList.addEventListener('change', (event): void => { triggeredEvent = event; @@ -542,7 +488,7 @@ describe('MediaQueryList', () => { it('Removes listener for window "resize" event when sending in a "change" event.', () => { let triggeredEvent: MediaQueryListEvent | null = null; const mediaQueryList = new MediaQueryList({ - ownerWindow: window, + window: window, media: '(min-width: 1025px)' }); const listener = (event): void => { diff --git a/packages/happy-dom/test/mutation-observer/MutationObserver.test.ts b/packages/happy-dom/test/mutation-observer/MutationObserver.test.ts index 095c02dd0..2ca1c8e95 100644 --- a/packages/happy-dom/test/mutation-observer/MutationObserver.test.ts +++ b/packages/happy-dom/test/mutation-observer/MutationObserver.test.ts @@ -17,7 +17,7 @@ describe('MutationObserver', () => { it('Observes attributes.', async () => { let records: MutationRecord[] = []; const div = document.createElement('div'); - const observer = new MutationObserver((mutationRecords) => { + const observer = new window.MutationObserver((mutationRecords) => { records = mutationRecords; }); observer.observe(div, { attributes: true }); @@ -42,13 +42,17 @@ describe('MutationObserver', () => { it('Throws TypeError for invalid options.', async () => { const div = document.createElement('div'); - const observer = new MutationObserver(() => {}); - expect(() => observer.observe(div, {})).toThrow(TypeError); + const observer = new window.MutationObserver(() => {}); + expect(() => observer.observe(div, {})).toThrow( + new TypeError( + `Failed to execute 'observe' on 'MutationObserver': The options object must set at least one of 'attributes', 'characterData', or 'childList' to true.` + ) + ); }); it('Allows to omit "attributes" if "attributeOldValue" or "attributeFilter" is specified.', async () => { const div = document.createElement('div'); - const observer = new MutationObserver(() => {}); + const observer = new window.MutationObserver(() => {}); expect(() => observer.observe(div, { attributes: false, attributeOldValue: true })).toThrow(); expect(() => observer.observe(div, { attributes: false, attributeFilter: ['style', 'class'] }) @@ -60,7 +64,7 @@ describe('MutationObserver', () => { it('Allows to omit "characterData" if "characterDataOldValue" is specified.', async () => { const text = document.createTextNode('old'); - const observer = new MutationObserver(() => {}); + const observer = new window.MutationObserver(() => {}); expect(() => observer.observe(text, { characterData: false, characterDataOldValue: true }) ).toThrow(); @@ -70,7 +74,7 @@ describe('MutationObserver', () => { it('Observes attributes and old attribute values.', async () => { let records: MutationRecord[] = []; const div = document.createElement('div'); - const observer = new MutationObserver((mutationRecords) => { + const observer = new window.MutationObserver((mutationRecords) => { records = mutationRecords; }); div.setAttribute('attr', 'old'); @@ -97,7 +101,7 @@ describe('MutationObserver', () => { it('Only observes a list of filtered attributes if defined.', async () => { const records: MutationRecord[][] = []; const div = document.createElement('div'); - const observer = new MutationObserver((mutationRecords) => { + const observer = new window.MutationObserver((mutationRecords) => { records.push(mutationRecords); }); div.setAttribute('attr1', 'old'); @@ -132,7 +136,7 @@ describe('MutationObserver', () => { it('Observers character data changes on text node.', async () => { const records: MutationRecord[][] = []; const text = document.createTextNode('old'); - const observer = new MutationObserver((mutationRecords) => { + const observer = new window.MutationObserver((mutationRecords) => { records.push(mutationRecords); }); observer.observe(text, { characterData: true, characterDataOldValue: true }); @@ -161,7 +165,7 @@ describe('MutationObserver', () => { const records: MutationRecord[][] = []; const div = document.createElement('div'); const text = document.createTextNode('old'); - const observer = new MutationObserver((mutationRecords) => { + const observer = new window.MutationObserver((mutationRecords) => { records.push(mutationRecords); }); div.appendChild(text); @@ -193,7 +197,7 @@ describe('MutationObserver', () => { const span = document.createElement('span'); const article = document.createElement('article'); const text = document.createTextNode('old'); - const observer = new MutationObserver((mutationRecords) => { + const observer = new window.MutationObserver((mutationRecords) => { records.push(mutationRecords); }); observer.observe(div, { subtree: true, childList: true }); @@ -257,7 +261,7 @@ describe('MutationObserver', () => { it('Can observe document node.', async () => { let records: MutationRecord[] = []; const div = document.createElement('div'); - const observer = new MutationObserver((mutationRecords) => { + const observer = new window.MutationObserver((mutationRecords) => { records = mutationRecords; }); document.appendChild(div); @@ -286,7 +290,7 @@ describe('MutationObserver', () => { it('Disconnects the observer.', async () => { let records: MutationRecord[] = []; const div = document.createElement('div'); - const observer = new MutationObserver((mutationRecords) => { + const observer = new window.MutationObserver((mutationRecords) => { records = mutationRecords; }); @@ -304,7 +308,7 @@ describe('MutationObserver', () => { it('Disconnects the observer when closing window.', async () => { let records: MutationRecord[] = []; const div = document.createElement('div'); - const observer = new MutationObserver((mutationRecords) => { + const observer = new window.MutationObserver((mutationRecords) => { records = mutationRecords; }); const span = document.createElement('span'); @@ -346,7 +350,7 @@ describe('MutationObserver', () => { }); it('Ignores if triggered when it is not observing.', () => { - const observer = new MutationObserver(() => {}); + const observer = new window.MutationObserver(() => {}); expect(() => observer.disconnect()).not.toThrow(); }); }); @@ -355,7 +359,7 @@ describe('MutationObserver', () => { it('Returns all records and empties the record queue.', async () => { let records: MutationRecord[] = []; const div = document.createElement('div'); - const observer = new MutationObserver((mutationRecords) => { + const observer = new window.MutationObserver((mutationRecords) => { records = mutationRecords; }); diff --git a/packages/happy-dom/test/nodes/document/Document.test.ts b/packages/happy-dom/test/nodes/document/Document.test.ts index 47b3400ff..e02cf3b2e 100644 --- a/packages/happy-dom/test/nodes/document/Document.test.ts +++ b/packages/happy-dom/test/nodes/document/Document.test.ts @@ -36,6 +36,7 @@ import BrowserWindow from '../../../src/window/BrowserWindow.js'; import Fetch from '../../../src/fetch/Fetch.js'; import * as PropertySymbol from '../../../src/PropertySymbol.js'; import HTMLUnknownElement from '../../../src/nodes/html-unknown-element/HTMLUnknownElement.js'; +import EventTarget from '../../../src/event/EventTarget.js'; /* eslint-disable jsdoc/require-jsdoc */ @@ -1288,7 +1289,7 @@ describe('Document', () => { const clone = document.cloneNode(false); const clone2 = document.cloneNode(true); - expect(clone[PropertySymbol.ownerWindow] === window).toBe(true); + expect(clone[PropertySymbol.window] === window).toBe(true); expect(clone.defaultView === null).toBe(true); expect(clone.children.length).toBe(0); expect(clone2.children.length).toBe(1); @@ -1322,16 +1323,22 @@ describe('Document', () => { describe('addEventListener()', () => { it('Triggers "readystatechange" event if no resources needs to be loaded.', async () => { await new Promise((resolve) => { - let readyChangeEvent: Event | null = null; - - document.addEventListener('readystatechange', (event) => { - readyChangeEvent = event; + let event: Event | null = null; + let target: EventTarget | null = null; + let currentTarget: EventTarget | null = null; + + document.addEventListener('readystatechange', (e) => { + event = e; + target = e.target; + currentTarget = e.currentTarget; }); expect(document.readyState).toBe(DocumentReadyStateEnum.interactive); setTimeout(() => { - expect((readyChangeEvent).target).toBe(document); + expect((event).target).toBe(null); + expect(target).toBe(document); + expect(currentTarget).toBe(document); expect(document.readyState).toBe(DocumentReadyStateEnum.complete); resolve(null); }, 20); @@ -1348,7 +1355,9 @@ describe('Document', () => { let resourceFetchCSSURL: string | null = null; let resourceFetchJSWindow: BrowserWindow | null = null; let resourceFetchJSURL: string | null = null; - let readyChangeEvent: Event | null = null; + let event: Event | null = null; + let target: EventTarget | null = null; + let currentTarget: EventTarget | null = null; vi.spyOn(ResourceFetch.prototype, 'fetch').mockImplementation(async function (url: string) { if (url.endsWith('.css')) { @@ -1362,8 +1371,10 @@ describe('Document', () => { return jsResponse; }); - document.addEventListener('readystatechange', (event) => { - readyChangeEvent = event; + document.addEventListener('readystatechange', (e) => { + event = e; + target = e.target; + currentTarget = e.currentTarget; }); const script = document.createElement('script'); @@ -1384,7 +1395,9 @@ describe('Document', () => { expect(resourceFetchCSSURL).toBe(cssURL); expect(resourceFetchJSWindow).toBe(window); expect(resourceFetchJSURL).toBe(jsURL); - expect((readyChangeEvent).target).toBe(document); + expect((event).target).toBe(null); + expect(target).toBe(document); + expect(currentTarget).toBe(document); expect(document.readyState).toBe(DocumentReadyStateEnum.complete); expect(document.styleSheets.length).toBe(1); expect(document.styleSheets[0].cssRules[0].cssText).toBe(cssResponse); @@ -1394,7 +1407,7 @@ describe('Document', () => { delete window['test']; resolve(null); - }, 0); + }, 10); }); }); }); @@ -1435,6 +1448,16 @@ describe('Document', () => { expect(emittedEvent).toBe(event); }); + + it('Doesn\t bubble to Window if the event type is "load".', () => { + const event = new Event('load', { bubbles: true }); + let emittedEvent: Event | null = null; + + window.addEventListener('load', (event) => (emittedEvent = event)); + document.dispatchEvent(event); + + expect(emittedEvent).toBe(null); + }); }); describe('createProcessingInstruction()', () => { diff --git a/packages/happy-dom/test/nodes/html-button-element/HTMLButtonElement.test.ts b/packages/happy-dom/test/nodes/html-button-element/HTMLButtonElement.test.ts index 489d19111..aba535421 100644 --- a/packages/happy-dom/test/nodes/html-button-element/HTMLButtonElement.test.ts +++ b/packages/happy-dom/test/nodes/html-button-element/HTMLButtonElement.test.ts @@ -109,18 +109,21 @@ describe('HTMLButtonElement', () => { element.setAttribute('type', 'submit'); expect(element.type).toBe('submit'); - element.setAttribute('type', 'MeNu'); + element.setAttribute('type', 'menu'); expect(element.type).toBe('menu'); + element.setAttribute('type', 'MeNu'); + expect(element.type).toBe('submit'); + element.setAttribute('type', 'foobar'); expect(element.type).toBe('submit'); }); }); describe('set type()', () => { - it(`Sets the attribute "type" after sanitizing.`, () => { + it(`Sets the attribute "type".`, () => { element.type = 'SuBmIt'; - expect(element.getAttribute('type')).toBe('submit'); + expect(element.getAttribute('type')).toBe('SuBmIt'); element.type = 'reset'; expect(element.getAttribute('type')).toBe('reset'); @@ -132,7 +135,7 @@ describe('HTMLButtonElement', () => { expect(element.getAttribute('type')).toBe('menu'); ((element.type)) = null; - expect(element.getAttribute('type')).toBe('submit'); + expect(element.getAttribute('type')).toBe('null'); }); }); diff --git a/packages/happy-dom/test/nodes/html-canvas-element/CanvasCaptureMediaStreamTrack.test.ts b/packages/happy-dom/test/nodes/html-canvas-element/CanvasCaptureMediaStreamTrack.test.ts index 2d29c758b..33582025e 100644 --- a/packages/happy-dom/test/nodes/html-canvas-element/CanvasCaptureMediaStreamTrack.test.ts +++ b/packages/happy-dom/test/nodes/html-canvas-element/CanvasCaptureMediaStreamTrack.test.ts @@ -2,7 +2,6 @@ import HTMLCanvasElement from '../../../src/nodes/html-canvas-element/HTMLCanvas import Window from '../../../src/window/Window.js'; import Document from '../../../src/nodes/document/Document.js'; import { beforeEach, describe, it, expect } from 'vitest'; -import CanvasCaptureMediaStreamTrack from '../../../src/nodes/html-canvas-element/CanvasCaptureMediaStreamTrack.js'; import * as PropertySymbol from '../../../src/PropertySymbol.js'; import MediaStreamTrack from '../../../src/nodes/html-media-element/MediaStreamTrack.js'; @@ -19,27 +18,30 @@ describe('CanvasCaptureMediaStreamTrack', () => { describe('constructor()', () => { it('Should throw an error if the "illegalConstructor" symbol is not sent to the constructor', () => { - expect(() => new CanvasCaptureMediaStreamTrack()).toThrow( + expect(() => new window.CanvasCaptureMediaStreamTrack()).toThrow( new TypeError('Illegal constructor') ); }); it('Should not throw an error if the "illegalConstructor" symbol is provided', () => { expect( - () => new CanvasCaptureMediaStreamTrack(PropertySymbol.illegalConstructor) + () => new window.CanvasCaptureMediaStreamTrack(PropertySymbol.illegalConstructor) ).not.toThrow(); }); it('Is an instance of MediaStreamTrack', () => { - expect(new CanvasCaptureMediaStreamTrack(PropertySymbol.illegalConstructor)).toBeInstanceOf( - MediaStreamTrack - ); + expect( + new window.CanvasCaptureMediaStreamTrack(PropertySymbol.illegalConstructor) + ).toBeInstanceOf(MediaStreamTrack); }); }); describe('get canvas()', () => { it('Returns the canvas.', () => { - const track = new CanvasCaptureMediaStreamTrack(PropertySymbol.illegalConstructor, canvas); + const track = new window.CanvasCaptureMediaStreamTrack( + PropertySymbol.illegalConstructor, + canvas + ); track[PropertySymbol.kind] = 'video'; expect(track.canvas).toBe(canvas); }); @@ -47,7 +49,10 @@ describe('CanvasCaptureMediaStreamTrack', () => { describe('requestFrame()', () => { it('Does nothing.', () => { - const track = new CanvasCaptureMediaStreamTrack(PropertySymbol.illegalConstructor, canvas); + const track = new window.CanvasCaptureMediaStreamTrack( + PropertySymbol.illegalConstructor, + canvas + ); track[PropertySymbol.kind] = 'video'; expect(() => track.requestFrame()).not.toThrow(); }); @@ -55,7 +60,10 @@ describe('CanvasCaptureMediaStreamTrack', () => { describe('clone()', () => { it('Clones the track.', () => { - const track = new CanvasCaptureMediaStreamTrack(PropertySymbol.illegalConstructor, canvas); + const track = new window.CanvasCaptureMediaStreamTrack( + PropertySymbol.illegalConstructor, + canvas + ); track[PropertySymbol.kind] = 'video'; const clone = track.clone(); diff --git a/packages/happy-dom/test/nodes/html-element/HTMLElement.test.ts b/packages/happy-dom/test/nodes/html-element/HTMLElement.test.ts index ee30a17cb..ea4375257 100644 --- a/packages/happy-dom/test/nodes/html-element/HTMLElement.test.ts +++ b/packages/happy-dom/test/nodes/html-element/HTMLElement.test.ts @@ -8,6 +8,7 @@ import CustomElement from '../../CustomElement.js'; import * as PropertySymbol from '../../../src/PropertySymbol.js'; import CustomElementRegistry from '../../../src/custom-element/CustomElementRegistry.js'; import { beforeEach, afterEach, describe, it, expect, vi } from 'vitest'; +import EventTarget from '../../../src/event/EventTarget.js'; describe('HTMLElement', () => { let window: Window; @@ -451,20 +452,28 @@ describe('HTMLElement', () => { describe('click()', () => { it('Dispatches "click" event.', () => { - let triggeredEvent: PointerEvent | null = null; - - element.addEventListener('click', (event) => (triggeredEvent = event)); + let event: PointerEvent | null = null; + let target: EventTarget | null = null; + let currentTarget: EventTarget | null = null; + + element.addEventListener('click', (e) => { + event = e; + target = e.target; + currentTarget = e.currentTarget; + }); element.click(); - expect((triggeredEvent) instanceof PointerEvent).toBe(true); - expect(((triggeredEvent)).type).toBe('click'); - expect(((triggeredEvent)).bubbles).toBe(true); - expect(((triggeredEvent)).composed).toBe(true); - expect(((triggeredEvent)).target === element).toBe(true); - expect(((triggeredEvent)).currentTarget === element).toBe(true); - expect(((triggeredEvent)).width).toBe(1); - expect(((triggeredEvent)).height).toBe(1); + expect((event) instanceof PointerEvent).toBe(true); + expect(((event)).type).toBe('click'); + expect(((event)).bubbles).toBe(true); + expect(((event)).composed).toBe(true); + expect(((event)).width).toBe(1); + expect(((event)).height).toBe(1); + expect(((event)).target).toBe(null); + expect(((event)).currentTarget).toBe(null); + expect(target).toBe(element); + expect(currentTarget).toBe(element); }); }); @@ -528,7 +537,7 @@ describe('HTMLElement', () => { parent.appendChild(element); - expect(window.customElements[PropertySymbol.callbacks]['custom-element'].length).toBe(1); + expect(window.customElements[PropertySymbol.callbacks].get('custom-element')?.length).toBe(1); parent.removeChild(element); diff --git a/packages/happy-dom/test/nodes/html-element/HTMLElementUtility.test.ts b/packages/happy-dom/test/nodes/html-element/HTMLElementUtility.test.ts index 0883c0820..7a6849837 100644 --- a/packages/happy-dom/test/nodes/html-element/HTMLElementUtility.test.ts +++ b/packages/happy-dom/test/nodes/html-element/HTMLElementUtility.test.ts @@ -4,6 +4,7 @@ import Document from '../../../src/nodes/document/Document.js'; import HTMLElement from '../../../src/nodes/html-element/HTMLElement.js'; import Window from '../../../src/window/Window.js'; import { beforeEach, describe, it, expect } from 'vitest'; +import EventTarget from '../../../src/event/EventTarget.js'; describe('HTMLElementUtility', () => { let window: Window; @@ -20,31 +21,43 @@ describe('HTMLElementUtility', () => { document.createElement('div'), document.createElementNS('http://www.w3.org/2000/svg', 'svg') ]) { - let triggeredBlurEvent: FocusEvent | null = null; - let triggeredFocusOutEvent: FocusEvent | null = null; + let blurEvent: FocusEvent | null = null; + let blurTarget: EventTarget | null = null; + let blurCurrentTarget: EventTarget | null = null; + let focusOutEvent: FocusEvent | null = null; + let focusOutTarget: EventTarget | null = null; + let focusOutCurrentTarget: EventTarget | null = null; document.body.appendChild(element); element.addEventListener('blur', (event) => { - triggeredBlurEvent = event; + blurEvent = event; + blurTarget = event.target; + blurCurrentTarget = event.currentTarget; }); element.addEventListener('focusout', (event) => { - triggeredFocusOutEvent = event; + focusOutEvent = event; + focusOutTarget = event.target; + focusOutCurrentTarget = event.currentTarget; }); element.focus(); element.blur(); - expect(((triggeredBlurEvent)).type).toBe('blur'); - expect(((triggeredBlurEvent)).bubbles).toBe(false); - expect(((triggeredBlurEvent)).composed).toBe(true); - expect(((triggeredBlurEvent)).target === element).toBe(true); + expect(((blurEvent)).type).toBe('blur'); + expect(((blurEvent)).bubbles).toBe(false); + expect(((blurEvent)).composed).toBe(true); + expect(((blurEvent)).target).toBe(null); + expect(blurTarget).toBe(element); + expect(blurCurrentTarget).toBe(element); - expect(((triggeredFocusOutEvent)).type).toBe('focusout'); - expect(((triggeredFocusOutEvent)).bubbles).toBe(true); - expect(((triggeredFocusOutEvent)).composed).toBe(true); - expect(((triggeredFocusOutEvent)).target === element).toBe(true); + expect(((focusOutEvent)).type).toBe('focusout'); + expect(((focusOutEvent)).bubbles).toBe(true); + expect(((focusOutEvent)).composed).toBe(true); + expect(((focusOutEvent)).target).toBe(null); + expect(focusOutTarget).toBe(element); + expect(focusOutCurrentTarget).toBe(element); expect(document.activeElement === document.body).toBe(true); } @@ -94,32 +107,44 @@ describe('HTMLElementUtility', () => { document.createElement('div'), document.createElementNS('http://www.w3.org/2000/svg', 'svg') ]) { - let triggeredFocusEvent: FocusEvent | null = null; - let triggeredFocusInEvent: FocusEvent | null = null; + let focusEvent: FocusEvent | null = null; + let focusTarget: EventTarget | null = null; + let focusCurrentTarget: EventTarget | null = null; + let focusInEvent: FocusEvent | null = null; + let focusInTarget: EventTarget | null = null; + let focusInCurrentTarget: EventTarget | null = null; document.body.appendChild(element); element.addEventListener('focus', (event) => { - triggeredFocusEvent = event; + focusEvent = event; + focusTarget = event.target; + focusCurrentTarget = event.currentTarget; }); element.addEventListener('focusin', (event) => { - triggeredFocusInEvent = event; + focusInEvent = event; + focusInTarget = event.target; + focusInCurrentTarget = event.currentTarget; }); element.focus(); - expect(((triggeredFocusEvent)).type).toBe('focus'); - expect(((triggeredFocusEvent)).bubbles).toBe(false); - expect(((triggeredFocusEvent)).composed).toBe(true); - expect(((triggeredFocusEvent)).target === element).toBe(true); - - expect(((triggeredFocusInEvent)).type).toBe('focusin'); - expect(((triggeredFocusInEvent)).bubbles).toBe(true); - expect(((triggeredFocusInEvent)).composed).toBe(true); - expect(((triggeredFocusInEvent)).target === element).toBe(true); - - expect(document.activeElement === element).toBe(true); + expect(((focusEvent)).type).toBe('focus'); + expect(((focusEvent)).bubbles).toBe(false); + expect(((focusEvent)).composed).toBe(true); + expect(((focusEvent)).target).toBe(null); + expect(focusTarget).toBe(element); + expect(focusCurrentTarget).toBe(element); + + expect(((focusInEvent)).type).toBe('focusin'); + expect(((focusInEvent)).bubbles).toBe(true); + expect(((focusInEvent)).composed).toBe(true); + expect(((focusInEvent)).target).toBe(null); + expect(focusInTarget).toBe(element); + expect(focusInCurrentTarget).toBe(element); + + expect(document.activeElement).toBe(element); } }); @@ -167,23 +192,29 @@ describe('HTMLElementUtility', () => { document.createElementNS('http://www.w3.org/2000/svg', 'svg') ]) { const previousElement = document.createElement('div'); - let triggeredEvent: FocusEvent | null = null; + let event: FocusEvent | null = null; + let target: EventTarget | null = null; + let currentTarget: EventTarget | null = null; document.body.appendChild(element); document.body.appendChild(previousElement); previousElement.focus(); - previousElement.addEventListener('blur', (event) => { - triggeredEvent = event; + previousElement.addEventListener('blur', (e) => { + event = e; + target = e.target; + currentTarget = e.currentTarget; }); element.focus(); - expect(((triggeredEvent)).type).toBe('blur'); - expect(((triggeredEvent)).bubbles).toBe(false); - expect(((triggeredEvent)).composed).toBe(true); - expect(((triggeredEvent)).target === previousElement).toBe(true); + expect(((event)).type).toBe('blur'); + expect(((event)).bubbles).toBe(false); + expect(((event)).composed).toBe(true); + expect(((event)).target).toBe(null); + expect(target).toBe(previousElement); + expect(currentTarget).toBe(previousElement); } }); }); diff --git a/packages/happy-dom/test/nodes/html-image-element/Image.test.ts b/packages/happy-dom/test/nodes/html-image-element/Image.test.ts index fb9c22bbf..23c9107ac 100644 --- a/packages/happy-dom/test/nodes/html-image-element/Image.test.ts +++ b/packages/happy-dom/test/nodes/html-image-element/Image.test.ts @@ -1,6 +1,7 @@ import Window from '../../../src/window/Window.js'; import HTMLImageElement from '../../../src/nodes/html-image-element/HTMLImageElement.js'; import { beforeEach, describe, it, expect } from 'vitest'; +import NamespaceURI from '../../../src/config/NamespaceURI.js'; describe('Image', () => { let window: Window; @@ -15,6 +16,9 @@ describe('Image', () => { expect(image.width).toBe(0); expect(image.height).toBe(0); expect(image.tagName).toBe('IMG'); + expect(image.localName).toBe('img'); + expect(image.namespaceURI).toBe(NamespaceURI.html); + expect(image.ownerDocument).toBe(window.document); expect(image instanceof HTMLImageElement).toBe(true); }); @@ -23,6 +27,10 @@ describe('Image', () => { const image = new window.Image(100, 200); expect(image.width).toBe(100); expect(image.height).toBe(200); + expect(image.tagName).toBe('IMG'); + expect(image.localName).toBe('img'); + expect(image.namespaceURI).toBe(NamespaceURI.html); + expect(image.ownerDocument).toBe(window.document); }); }); }); diff --git a/packages/happy-dom/test/nodes/html-input-element/HTMLInputElement.test.ts b/packages/happy-dom/test/nodes/html-input-element/HTMLInputElement.test.ts index cf14498dc..67825885b 100644 --- a/packages/happy-dom/test/nodes/html-input-element/HTMLInputElement.test.ts +++ b/packages/happy-dom/test/nodes/html-input-element/HTMLInputElement.test.ts @@ -1312,6 +1312,8 @@ describe('HTMLInputElement', () => { expect(isChangeTriggered).toBe(false); expect(element.checked).toBe(true); + element.checked = false; + document.body.appendChild(element); element.dispatchEvent(new PointerEvent('click')); @@ -1326,6 +1328,24 @@ describe('HTMLInputElement', () => { expect(element.checked).toBe(true); }); + it('Doesn\'t trigger "change" and "input" event if type is "radio" it is already checked when dispatching a "click" event.', () => { + let isInputTriggered = false; + let isChangeTriggered = false; + + element.addEventListener('input', () => (isInputTriggered = true)); + element.addEventListener('change', () => (isChangeTriggered = true)); + + element.type = 'radio'; + element.checked = true; + + document.body.appendChild(element); + + element.dispatchEvent(new PointerEvent('click')); + + expect(isInputTriggered).toBe(false); + expect(isChangeTriggered).toBe(false); + }); + it('Sets "checked" to "true" before triggering listeners if type is "checkbox".', () => { let isInputChecked = false; let isChangeChecked = false; diff --git a/packages/happy-dom/test/nodes/html-label-element/HTMLLabelElement.test.ts b/packages/happy-dom/test/nodes/html-label-element/HTMLLabelElement.test.ts index 3f0a04a12..9f7302738 100644 --- a/packages/happy-dom/test/nodes/html-label-element/HTMLLabelElement.test.ts +++ b/packages/happy-dom/test/nodes/html-label-element/HTMLLabelElement.test.ts @@ -117,7 +117,7 @@ describe('HTMLLabelElement', () => { expect(input.checked).toBe(false); - element.dispatchEvent(new PointerEvent('click')); + element.click(); expect(input.checked).toBe(true); expect(labelClickCount).toBe(2); @@ -147,5 +147,29 @@ describe('HTMLLabelElement', () => { expect(labelClickCount).toBe(2); expect(inputClickCount).toBe(1); }); + + it("Doesn't trigger when preventDefault() has been called.", () => { + const input = document.createElement('input'); + const span = document.createElement('span'); + + input.type = 'checkbox'; + + span.appendChild(input); + element.appendChild(span); + + let inputClickCount = 0; + + element.addEventListener('click', (event) => { + event.preventDefault(); + }); + input.addEventListener('click', () => inputClickCount++); + + expect(input.checked).toBe(false); + + element.click(); + + expect(input.checked).toBe(false); + expect(inputClickCount).toBe(0); + }); }); }); diff --git a/packages/happy-dom/test/nodes/html-link-element/HTMLLinkElement.test.ts b/packages/happy-dom/test/nodes/html-link-element/HTMLLinkElement.test.ts index 7f4aa3e8d..b715dd7fe 100644 --- a/packages/happy-dom/test/nodes/html-link-element/HTMLLinkElement.test.ts +++ b/packages/happy-dom/test/nodes/html-link-element/HTMLLinkElement.test.ts @@ -6,6 +6,7 @@ import ResourceFetch from '../../../src/fetch/ResourceFetch.js'; import Event from '../../../src/event/Event.js'; import ErrorEvent from '../../../src/event/events/ErrorEvent.js'; import { beforeEach, afterEach, describe, it, expect, vi } from 'vitest'; +import EventTarget from '../../../src/event/EventTarget.js'; describe('HTMLLinkElement', () => { let window: Window; @@ -90,6 +91,8 @@ describe('HTMLLinkElement', () => { let loadedWindow: BrowserWindow | null = null; let loadedURL: string | null = null; let loadEvent: Event | null = null; + let loadEventTarget: EventTarget | null = null; + let loadEventCurrentTarget: EventTarget | null = null; vi.spyOn(ResourceFetch.prototype, 'fetch').mockImplementation(async function (url: string) { loadedWindow = this.window; @@ -101,6 +104,8 @@ describe('HTMLLinkElement', () => { element.addEventListener('load', (event) => { loadEvent = event; + loadEventTarget = event.target; + loadEventCurrentTarget = event.currentTarget; }); element.rel = 'stylesheet'; @@ -112,7 +117,9 @@ describe('HTMLLinkElement', () => { expect(loadedURL).toBe('https://localhost:8080/test/path/file.css'); expect(element.sheet.cssRules.length).toBe(1); expect(element.sheet.cssRules[0].cssText).toBe('div { background: red; }'); - expect(((loadEvent)).target).toBe(element); + expect(((loadEvent)).target).toBe(null); + expect(loadEventTarget).toBe(element); + expect(loadEventCurrentTarget).toBe(element); }); it('Triggers error event when fetching a CSS file fails during setting the "href" and "rel" attributes.', async () => { @@ -164,6 +171,8 @@ describe('HTMLLinkElement', () => { const element = document.createElement('link'); const css = 'div { background: red; }'; let loadEvent: Event | null = null; + let loadEventTarget: EventTarget | null = null; + let loadEventCurrentTarget: EventTarget | null = null; let loadedWindow: BrowserWindow | null = null; let loadedURL: string | null = null; @@ -177,6 +186,8 @@ describe('HTMLLinkElement', () => { element.href = 'https://localhost:8080/test/path/file.css'; element.addEventListener('load', (event) => { loadEvent = event; + loadEventTarget = event.target; + loadEventCurrentTarget = event.currentTarget; }); document.body.appendChild(element); @@ -187,7 +198,9 @@ describe('HTMLLinkElement', () => { expect(loadedURL).toBe('https://localhost:8080/test/path/file.css'); expect(element.sheet.cssRules.length).toBe(1); expect(element.sheet.cssRules[0].cssText).toBe('div { background: red; }'); - expect(((loadEvent)).target).toBe(element); + expect(((loadEvent)).target).toBe(null); + expect(loadEventTarget).toBe(element); + expect(loadEventCurrentTarget).toBe(element); }); it('Triggers error event when fetching a CSS file fails while appending the element to the document.', async () => { diff --git a/packages/happy-dom/test/nodes/html-media-element/HTMLMediaElement.test.ts b/packages/happy-dom/test/nodes/html-media-element/HTMLMediaElement.test.ts index 57c2e24f9..70d97bd75 100644 --- a/packages/happy-dom/test/nodes/html-media-element/HTMLMediaElement.test.ts +++ b/packages/happy-dom/test/nodes/html-media-element/HTMLMediaElement.test.ts @@ -14,6 +14,7 @@ import TextTrackKindEnum from '../../../src/nodes/html-media-element/TextTrackKi import TextTrack from '../../../src/nodes/html-media-element/TextTrack.js'; import MediaStream from '../../../src/nodes/html-media-element/MediaStream.js'; import * as PropertySymbol from '../../../src/PropertySymbol.js'; +import NamespaceURI from '../../../src/config/NamespaceURI.js'; describe('HTMLMediaElement', () => { let window: Window; @@ -32,6 +33,16 @@ describe('HTMLMediaElement', () => { expect(document.createElement('audio')).toBeInstanceOf(HTMLAudioElement); expect(document.createElement('video')).toBeInstanceOf(HTMLMediaElement); expect(document.createElement('video')).toBeInstanceOf(HTMLVideoElement); + + const audio = new window.Audio(); + expect(audio).toBeInstanceOf(HTMLMediaElement); + expect(audio).toBeInstanceOf(HTMLAudioElement); + expect(audio.ownerDocument).toBe(document); + expect(audio.tagName).toBe('AUDIO'); + expect(audio.localName).toBe('audio'); + expect(audio.namespaceURI).toBe(NamespaceURI.html); + + expect(window['Video']).toBe(undefined); }); }); diff --git a/packages/happy-dom/test/nodes/html-media-element/MediaStream.test.ts b/packages/happy-dom/test/nodes/html-media-element/MediaStream.test.ts index ce8cabbe5..8993a22da 100644 --- a/packages/happy-dom/test/nodes/html-media-element/MediaStream.test.ts +++ b/packages/happy-dom/test/nodes/html-media-element/MediaStream.test.ts @@ -1,26 +1,33 @@ -import { describe, it, expect } from 'vitest'; -import MediaStreamTrack from '../../../src/nodes/html-media-element/MediaStreamTrack.js'; +import { describe, it, expect, beforeEach } from 'vitest'; import MediaStream from '../../../src/nodes/html-media-element/MediaStream.js'; import * as PropertySymbol from '../../../src/PropertySymbol.js'; +import BrowserWindow from '../../../src/window/BrowserWindow.js'; +import Window from '../../../src/window/Window.js'; describe('MediaStream', () => { + let window: BrowserWindow; + + beforeEach(() => { + window = new Window(); + }); + describe('constructor()', () => { it('Supports another MediaStream as argument', () => { - const track = new MediaStreamTrack(PropertySymbol.illegalConstructor); + const track = new window.MediaStreamTrack(PropertySymbol.illegalConstructor); track[PropertySymbol.kind] = 'video'; - const stream = new MediaStream(); + const stream = new window.MediaStream(); stream.addTrack(track); - const newStream = new MediaStream(stream); + const newStream = new window.MediaStream(stream); expect(newStream).toBeInstanceOf(MediaStream); expect(newStream.getVideoTracks()).toEqual([track]); }); it('Supports an array of MediaStreamTrack as argument', () => { - const track1 = new MediaStreamTrack(PropertySymbol.illegalConstructor); - const track2 = new MediaStreamTrack(PropertySymbol.illegalConstructor); + const track1 = new window.MediaStreamTrack(PropertySymbol.illegalConstructor); + const track2 = new window.MediaStreamTrack(PropertySymbol.illegalConstructor); track1[PropertySymbol.kind] = 'video'; track2[PropertySymbol.kind] = 'video'; - const newStream = new MediaStream([track1, track2]); + const newStream = new window.MediaStream([track1, track2]); expect(newStream).toBeInstanceOf(MediaStream); expect(newStream.getVideoTracks()).toEqual([track1, track2]); }); @@ -28,16 +35,16 @@ describe('MediaStream', () => { describe('addTrack()', () => { it('Adds a track.', () => { - const stream = new MediaStream(); - const track = new MediaStreamTrack(PropertySymbol.illegalConstructor); + const stream = new window.MediaStream(); + const track = new window.MediaStreamTrack(PropertySymbol.illegalConstructor); track[PropertySymbol.kind] = 'audio'; stream.addTrack(track); expect(stream.getAudioTracks()).toEqual([track]); }); it('Does not add the same track twice.', () => { - const stream = new MediaStream(); - const track = new MediaStreamTrack(PropertySymbol.illegalConstructor); + const stream = new window.MediaStream(); + const track = new window.MediaStreamTrack(PropertySymbol.illegalConstructor); track[PropertySymbol.kind] = 'video'; stream.addTrack(track); stream.addTrack(track); @@ -47,9 +54,9 @@ describe('MediaStream', () => { describe('clone()', () => { it('Returns a clone.', () => { - const track = new MediaStreamTrack(PropertySymbol.illegalConstructor); + const track = new window.MediaStreamTrack(PropertySymbol.illegalConstructor); track[PropertySymbol.kind] = 'video'; - const stream = new MediaStream(); + const stream = new window.MediaStream(); stream.addTrack(track); const clone = stream.clone(); expect(clone).toBeInstanceOf(MediaStream); @@ -59,10 +66,10 @@ describe('MediaStream', () => { describe('getAudioTracks()', () => { it('Returns audio tracks.', () => { - const stream = new MediaStream(); - const audioTrack1 = new MediaStreamTrack(PropertySymbol.illegalConstructor); - const audioTrack2 = new MediaStreamTrack(PropertySymbol.illegalConstructor); - const videoTrack = new MediaStreamTrack(PropertySymbol.illegalConstructor); + const stream = new window.MediaStream(); + const audioTrack1 = new window.MediaStreamTrack(PropertySymbol.illegalConstructor); + const audioTrack2 = new window.MediaStreamTrack(PropertySymbol.illegalConstructor); + const videoTrack = new window.MediaStreamTrack(PropertySymbol.illegalConstructor); audioTrack1[PropertySymbol.kind] = 'audio'; audioTrack2[PropertySymbol.kind] = 'audio'; videoTrack[PropertySymbol.kind] = 'video'; @@ -75,10 +82,10 @@ describe('MediaStream', () => { describe('getTrackById()', () => { it('Returns track by id.', () => { - const stream = new MediaStream(); - const track1 = new MediaStreamTrack(PropertySymbol.illegalConstructor); - const track2 = new MediaStreamTrack(PropertySymbol.illegalConstructor); - const track3 = new MediaStreamTrack(PropertySymbol.illegalConstructor); + const stream = new window.MediaStream(); + const track1 = new window.MediaStreamTrack(PropertySymbol.illegalConstructor); + const track2 = new window.MediaStreamTrack(PropertySymbol.illegalConstructor); + const track3 = new window.MediaStreamTrack(PropertySymbol.illegalConstructor); track1[PropertySymbol.kind] = 'audio'; track2[PropertySymbol.kind] = 'audio'; track3[PropertySymbol.kind] = 'video'; @@ -93,10 +100,10 @@ describe('MediaStream', () => { describe('getVideoTracks()', () => { it('Returns video tracks.', () => { - const stream = new MediaStream(); - const audioTrack = new MediaStreamTrack(PropertySymbol.illegalConstructor); - const videoTrack1 = new MediaStreamTrack(PropertySymbol.illegalConstructor); - const videoTrack2 = new MediaStreamTrack(PropertySymbol.illegalConstructor); + const stream = new window.MediaStream(); + const audioTrack = new window.MediaStreamTrack(PropertySymbol.illegalConstructor); + const videoTrack1 = new window.MediaStreamTrack(PropertySymbol.illegalConstructor); + const videoTrack2 = new window.MediaStreamTrack(PropertySymbol.illegalConstructor); audioTrack[PropertySymbol.kind] = 'audio'; videoTrack1[PropertySymbol.kind] = 'video'; videoTrack2[PropertySymbol.kind] = 'video'; @@ -109,8 +116,8 @@ describe('MediaStream', () => { describe('removeTrack()', () => { it('Removes a track.', () => { - const stream = new MediaStream(); - const track = new MediaStreamTrack(PropertySymbol.illegalConstructor); + const stream = new window.MediaStream(); + const track = new window.MediaStreamTrack(PropertySymbol.illegalConstructor); track[PropertySymbol.kind] = 'audio'; stream.addTrack(track); stream.removeTrack(track); diff --git a/packages/happy-dom/test/nodes/html-media-element/MediaStreamTrack.test.ts b/packages/happy-dom/test/nodes/html-media-element/MediaStreamTrack.test.ts index ce5270c0e..79a814740 100644 --- a/packages/happy-dom/test/nodes/html-media-element/MediaStreamTrack.test.ts +++ b/packages/happy-dom/test/nodes/html-media-element/MediaStreamTrack.test.ts @@ -1,26 +1,35 @@ -import { describe, it, expect } from 'vitest'; -import MediaStreamTrack from '../../../src/nodes/html-media-element/MediaStreamTrack.js'; +import { describe, it, expect, beforeEach } from 'vitest'; import * as PropertySymbol from '../../../src/PropertySymbol.js'; import EventTarget from '../../../src/event/EventTarget.js'; +import BrowserWindow from '../../../src/window/BrowserWindow.js'; +import Window from '../../../src/window/Window.js'; describe('MediaStreamTrack', () => { + let window: BrowserWindow; + + beforeEach(() => { + window = new Window(); + }); + describe('constructor()', () => { it('Should throw an error if the "illegalConstructor" symbol is not sent to the constructor', () => { - expect(() => new MediaStreamTrack()).toThrow(new TypeError('Illegal constructor')); + expect(() => new window.MediaStreamTrack()).toThrow(new TypeError('Illegal constructor')); }); it('Should not throw an error if the "illegalConstructor" symbol is provided', () => { - expect(() => new MediaStreamTrack(PropertySymbol.illegalConstructor)).not.toThrow(); + expect(() => new window.MediaStreamTrack(PropertySymbol.illegalConstructor)).not.toThrow(); }); it('Is an instance of EventTarget', () => { - expect(new MediaStreamTrack(PropertySymbol.illegalConstructor)).toBeInstanceOf(EventTarget); + expect(new window.MediaStreamTrack(PropertySymbol.illegalConstructor)).toBeInstanceOf( + EventTarget + ); }); }); describe('applyConstraints()', () => { it('Applies constraints.', () => { - const track = new MediaStreamTrack(PropertySymbol.illegalConstructor); + const track = new window.MediaStreamTrack(PropertySymbol.illegalConstructor); track[PropertySymbol.kind] = 'video'; const constraints = { width: { min: 640, ideal: 1280 }, @@ -41,7 +50,7 @@ describe('MediaStreamTrack', () => { describe('getConstrains()', () => { it('Returns constraints.', () => { - const track = new MediaStreamTrack(PropertySymbol.illegalConstructor); + const track = new window.MediaStreamTrack(PropertySymbol.illegalConstructor); track[PropertySymbol.kind] = 'video'; const constraints = { width: { min: 640, ideal: 1280 }, @@ -55,7 +64,7 @@ describe('MediaStreamTrack', () => { describe('getCapabilities()', () => { it('Returns capabilities.', () => { - const track = new MediaStreamTrack(PropertySymbol.illegalConstructor); + const track = new window.MediaStreamTrack(PropertySymbol.illegalConstructor); track[PropertySymbol.kind] = 'video'; expect(track.getCapabilities()).toEqual({ aspectRatio: { @@ -81,7 +90,7 @@ describe('MediaStreamTrack', () => { }); it('Is possible to edit object.', () => { - const track = new MediaStreamTrack(PropertySymbol.illegalConstructor); + const track = new window.MediaStreamTrack(PropertySymbol.illegalConstructor); track[PropertySymbol.kind] = 'video'; track[PropertySymbol.capabilities].width.max = 800; expect(track.getCapabilities().width.max).toBe(800); @@ -90,7 +99,7 @@ describe('MediaStreamTrack', () => { describe('getSettings()', () => { it('Returns settings.', () => { - const track = new MediaStreamTrack(PropertySymbol.illegalConstructor); + const track = new window.MediaStreamTrack(PropertySymbol.illegalConstructor); track[PropertySymbol.kind] = 'video'; expect(track.getSettings()).toEqual({ deviceId: '', @@ -100,7 +109,7 @@ describe('MediaStreamTrack', () => { }); it('Is possible to edit object.', () => { - const track = new MediaStreamTrack(PropertySymbol.illegalConstructor); + const track = new window.MediaStreamTrack(PropertySymbol.illegalConstructor); track[PropertySymbol.kind] = 'video'; track[PropertySymbol.settings].frameRate = 30; expect(track.getSettings().frameRate).toBe(30); @@ -109,7 +118,7 @@ describe('MediaStreamTrack', () => { describe('clone()', () => { it('Clones the track.', () => { - const track = new MediaStreamTrack(PropertySymbol.illegalConstructor); + const track = new window.MediaStreamTrack(PropertySymbol.illegalConstructor); track[PropertySymbol.kind] = 'video'; const clone = track.clone(); expect(clone).not.toBe(track); diff --git a/packages/happy-dom/test/nodes/html-media-element/RemotePlayback.test.ts b/packages/happy-dom/test/nodes/html-media-element/RemotePlayback.test.ts index 44c2ae9c2..33669b5c9 100644 --- a/packages/happy-dom/test/nodes/html-media-element/RemotePlayback.test.ts +++ b/packages/happy-dom/test/nodes/html-media-element/RemotePlayback.test.ts @@ -1,31 +1,38 @@ import { describe, it, expect, beforeEach } from 'vitest'; -import RemotePlayback from '../../../src/nodes/html-media-element/RemotePlayback.js'; +import BrowserWindow from '../../../src/window/BrowserWindow.js'; +import Window from '../../../src/window/Window.js'; describe('RemotePlayback', () => { + let window: BrowserWindow; + + beforeEach(() => { + window = new Window(); + }); + describe('get state()', () => { it('Should return "disconnected" by default', () => { - const remotePlayback = new RemotePlayback(); + const remotePlayback = new window.RemotePlayback(); expect(remotePlayback.state).toBe('disconnected'); }); }); describe('watchAvailability()', () => { it('Should return a Promise that resolves to undefined', async () => { - const remotePlayback = new RemotePlayback(); + const remotePlayback = new window.RemotePlayback(); await expect(remotePlayback.watchAvailability()).resolves.toBeUndefined(); }); }); describe('cancelWatchAvailability()', () => { it('Should not throw an error', () => { - const remotePlayback = new RemotePlayback(); + const remotePlayback = new window.RemotePlayback(); expect(() => remotePlayback.cancelWatchAvailability()).not.toThrow(); }); }); describe('prompt()', () => { it('Should not throw an error', () => { - const remotePlayback = new RemotePlayback(); + const remotePlayback = new window.RemotePlayback(); expect(() => remotePlayback.prompt()).not.toThrow(); }); }); diff --git a/packages/happy-dom/test/nodes/html-media-element/TextTrack.test.ts b/packages/happy-dom/test/nodes/html-media-element/TextTrack.test.ts index a56beb8cc..4fdda56e2 100644 --- a/packages/happy-dom/test/nodes/html-media-element/TextTrack.test.ts +++ b/packages/happy-dom/test/nodes/html-media-element/TextTrack.test.ts @@ -15,27 +15,27 @@ describe('TextTrack', () => { describe('constructor()', () => { it('Should throw an error if the "illegalConstructor" symbol is not sent to the constructor', () => { - expect(() => new TextTrack()).toThrow(new TypeError('Illegal constructor')); + expect(() => new window.TextTrack()).toThrow(new TypeError('Illegal constructor')); }); it('Should not throw an error if the "illegalConstructor" symbol is provided', () => { - expect(() => new TextTrack(PropertySymbol.illegalConstructor)).not.toThrow(); + expect(() => new window.TextTrack(PropertySymbol.illegalConstructor)).not.toThrow(); }); it('Is an instance of EventTarget', () => { - const textTrackList = new TextTrack(PropertySymbol.illegalConstructor); + const textTrackList = new window.TextTrack(PropertySymbol.illegalConstructor); expect(textTrackList).toBeInstanceOf(EventTarget); }); }); describe('get kind()', () => { it('Should return "subtitles" by default', () => { - const textTrack = new TextTrack(PropertySymbol.illegalConstructor); + const textTrack = new window.TextTrack(PropertySymbol.illegalConstructor); expect(textTrack.kind).toBe(TextTrackKindEnum.subtitles); }); it('Should return the value set', () => { - const textTrack = new TextTrack(PropertySymbol.illegalConstructor); + const textTrack = new window.TextTrack(PropertySymbol.illegalConstructor); textTrack[PropertySymbol.kind] = TextTrackKindEnum.captions; expect(textTrack.kind).toBe('captions'); }); @@ -43,12 +43,12 @@ describe('TextTrack', () => { describe('get label()', () => { it('Should return an empty string by default', () => { - const textTrack = new TextTrack(PropertySymbol.illegalConstructor); + const textTrack = new window.TextTrack(PropertySymbol.illegalConstructor); expect(textTrack.label).toBe(''); }); it('Should return the value set', () => { - const textTrack = new TextTrack(PropertySymbol.illegalConstructor); + const textTrack = new window.TextTrack(PropertySymbol.illegalConstructor); textTrack[PropertySymbol.label] = 'test'; expect(textTrack.label).toBe('test'); }); @@ -56,12 +56,12 @@ describe('TextTrack', () => { describe('get language()', () => { it('Should return an empty string by default', () => { - const textTrack = new TextTrack(PropertySymbol.illegalConstructor); + const textTrack = new window.TextTrack(PropertySymbol.illegalConstructor); expect(textTrack.language).toBe(''); }); it('Should return the value set', () => { - const textTrack = new TextTrack(PropertySymbol.illegalConstructor); + const textTrack = new window.TextTrack(PropertySymbol.illegalConstructor); textTrack[PropertySymbol.language] = 'test'; expect(textTrack.language).toBe('test'); }); @@ -69,12 +69,12 @@ describe('TextTrack', () => { describe('get id()', () => { it('Should return an empty string by default', () => { - const textTrack = new TextTrack(PropertySymbol.illegalConstructor); + const textTrack = new window.TextTrack(PropertySymbol.illegalConstructor); expect(textTrack.id).toBe(''); }); it('Should return the value set', () => { - const textTrack = new TextTrack(PropertySymbol.illegalConstructor); + const textTrack = new window.TextTrack(PropertySymbol.illegalConstructor); textTrack[PropertySymbol.id] = 'test'; expect(textTrack.id).toBe('test'); }); @@ -82,12 +82,12 @@ describe('TextTrack', () => { describe('get mode()', () => { it('Should return "disabled" by default', () => { - const textTrack = new TextTrack(PropertySymbol.illegalConstructor); + const textTrack = new window.TextTrack(PropertySymbol.illegalConstructor); expect(textTrack.mode).toBe('disabled'); }); it('Should return the value set', () => { - const textTrack = new TextTrack(PropertySymbol.illegalConstructor); + const textTrack = new window.TextTrack(PropertySymbol.illegalConstructor); textTrack.mode = 'showing'; expect(textTrack.mode).toBe('showing'); }); @@ -95,13 +95,13 @@ describe('TextTrack', () => { describe('set mode()', () => { it('Should set the mode', () => { - const textTrack = new TextTrack(PropertySymbol.illegalConstructor); + const textTrack = new window.TextTrack(PropertySymbol.illegalConstructor); textTrack.mode = 'showing'; expect(textTrack.mode).toBe('showing'); }); it('Should ignore invalid values', () => { - const textTrack = new TextTrack(PropertySymbol.illegalConstructor); + const textTrack = new window.TextTrack(PropertySymbol.illegalConstructor); textTrack.mode = <'showing'>'invalid'; expect(textTrack.mode).toBe('disabled'); textTrack.mode = 'showing'; @@ -113,12 +113,12 @@ describe('TextTrack', () => { describe('get cues()', () => { it('Should return null by default', () => { - const textTrack = new TextTrack(PropertySymbol.illegalConstructor); + const textTrack = new window.TextTrack(PropertySymbol.illegalConstructor); expect(textTrack.cues).toBe(null); }); it('Should return a TextTrackCueList when mode is "showing"', () => { - const textTrack = new TextTrack(PropertySymbol.illegalConstructor); + const textTrack = new window.TextTrack(PropertySymbol.illegalConstructor); textTrack.mode = 'showing'; expect(textTrack.cues).toBeInstanceOf(window.TextTrackCueList); }); @@ -126,12 +126,12 @@ describe('TextTrack', () => { describe('get activeCues()', () => { it('Should return null by default', () => { - const textTrack = new TextTrack(PropertySymbol.illegalConstructor); + const textTrack = new window.TextTrack(PropertySymbol.illegalConstructor); expect(textTrack.activeCues).toBe(null); }); it('Should return a TextTrackCueList when mode is "showing"', () => { - const textTrack = new TextTrack(PropertySymbol.illegalConstructor); + const textTrack = new window.TextTrack(PropertySymbol.illegalConstructor); textTrack.mode = 'showing'; expect(textTrack.activeCues).toBeInstanceOf(window.TextTrackCueList); }); @@ -139,7 +139,7 @@ describe('TextTrack', () => { describe('addCue()', () => { it('Should add a cue', () => { - const textTrack = new TextTrack(PropertySymbol.illegalConstructor); + const textTrack = new window.TextTrack(PropertySymbol.illegalConstructor); const cue = new window.VTTCue(0, 10, 'test'); textTrack.addCue(cue); @@ -157,7 +157,7 @@ describe('TextTrack', () => { describe('removeCue()', () => { it('Should remove a cue', () => { - const textTrack = new TextTrack(PropertySymbol.illegalConstructor); + const textTrack = new window.TextTrack(PropertySymbol.illegalConstructor); const cue = new window.VTTCue(0, 10, 'test'); textTrack.addCue(cue); diff --git a/packages/happy-dom/test/nodes/html-media-element/TextTrackCue.test.ts b/packages/happy-dom/test/nodes/html-media-element/TextTrackCue.test.ts index a9df30516..a719a0fd8 100644 --- a/packages/happy-dom/test/nodes/html-media-element/TextTrackCue.test.ts +++ b/packages/happy-dom/test/nodes/html-media-element/TextTrackCue.test.ts @@ -1,24 +1,31 @@ -import { describe, it, expect } from 'vitest'; -import TextTrackCue from '../../../src/nodes/html-media-element/TextTrackCue.js'; +import { describe, it, expect, beforeEach } from 'vitest'; import * as PropertySymbol from '../../../src/PropertySymbol.js'; +import BrowserWindow from '../../../src/window/BrowserWindow.js'; +import Window from '../../../src/window/Window.js'; describe('TextTrackCue', () => { + let window: BrowserWindow; + + beforeEach(() => { + window = new Window(); + }); + describe('constructor()', () => { it('Should throw an error if constructed without the "illegalConstructor" symbol', () => { // @ts-ignore - expect(() => new TextTrackCue()).toThrow(new TypeError('Illegal constructor')); + expect(() => new window.TextTrackCue()).toThrow(new TypeError('Illegal constructor')); }); it('Should not throw an error if constructed with the "illegalConstructor" symbol', () => { // @ts-ignore - expect(() => new TextTrackCue(PropertySymbol.illegalConstructor)).not.toThrow(); + expect(() => new window.TextTrackCue(PropertySymbol.illegalConstructor)).not.toThrow(); }); }); describe('get id()', () => { it('Should return an empty string by default', () => { // @ts-ignore - const textTrackCue = new TextTrackCue(PropertySymbol.illegalConstructor); + const textTrackCue = new window.TextTrackCue(PropertySymbol.illegalConstructor); expect(textTrackCue.id).toBe(''); }); }); @@ -26,7 +33,7 @@ describe('TextTrackCue', () => { describe('get startTime()', () => { it('Should return 0 by default', () => { // @ts-ignore - const textTrackCue = new TextTrackCue(PropertySymbol.illegalConstructor); + const textTrackCue = new window.TextTrackCue(PropertySymbol.illegalConstructor); expect(textTrackCue.startTime).toBe(0); }); }); @@ -34,7 +41,7 @@ describe('TextTrackCue', () => { describe('get endTime()', () => { it('Should return 0 by default', () => { // @ts-ignore - const textTrackCue = new TextTrackCue(PropertySymbol.illegalConstructor); + const textTrackCue = new window.TextTrackCue(PropertySymbol.illegalConstructor); expect(textTrackCue.endTime).toBe(0); }); }); @@ -42,7 +49,7 @@ describe('TextTrackCue', () => { describe('get pauseOnExit()', () => { it('Should return false by default', () => { // @ts-ignore - const textTrackCue = new TextTrackCue(PropertySymbol.illegalConstructor); + const textTrackCue = new window.TextTrackCue(PropertySymbol.illegalConstructor); expect(textTrackCue.pauseOnExit).toBe(false); }); }); @@ -50,13 +57,13 @@ describe('TextTrackCue', () => { describe('get track()', () => { it('Should return null by default', () => { // @ts-ignore - const textTrackCue = new TextTrackCue(PropertySymbol.illegalConstructor); + const textTrackCue = new window.TextTrackCue(PropertySymbol.illegalConstructor); expect(textTrackCue.track).toBe(null); }); it('Should return the value set', () => { // @ts-ignore - const textTrackCue = new TextTrackCue(PropertySymbol.illegalConstructor); + const textTrackCue = new window.TextTrackCue(PropertySymbol.illegalConstructor); const track = {}; textTrackCue[PropertySymbol.track] = track; expect(textTrackCue.track).toBe(track); diff --git a/packages/happy-dom/test/nodes/html-media-element/TextTrackList.test.ts b/packages/happy-dom/test/nodes/html-media-element/TextTrackList.test.ts index 58ec223b0..f0542cf5e 100644 --- a/packages/happy-dom/test/nodes/html-media-element/TextTrackList.test.ts +++ b/packages/happy-dom/test/nodes/html-media-element/TextTrackList.test.ts @@ -15,31 +15,31 @@ describe('TextTrackList', () => { describe('constructor()', () => { it('Should throw an error if the "illegalConstructor" symbol is not sent to the constructor', () => { - expect(() => new TextTrackList()).toThrow(new TypeError('Illegal constructor')); + expect(() => new window.TextTrackList()).toThrow(new TypeError('Illegal constructor')); }); it('Should not throw an error if the "illegalConstructor" symbol is provided', () => { - expect(() => new TextTrackList(PropertySymbol.illegalConstructor)).not.toThrow(); + expect(() => new window.TextTrackList(PropertySymbol.illegalConstructor)).not.toThrow(); }); it('Is an instance of EventTarget', () => { - const textTrackList = new TextTrackList(PropertySymbol.illegalConstructor); + const textTrackList = new window.TextTrackList(PropertySymbol.illegalConstructor); expect(textTrackList).toBeInstanceOf(EventTarget); }); }); describe('get length()', () => { it('Should return 0 by default', () => { - const textTrackList = new TextTrackList(PropertySymbol.illegalConstructor); + const textTrackList = new window.TextTrackList(PropertySymbol.illegalConstructor); expect(textTrackList.length).toBe(0); }); it('Should return the number of tracks', () => { const items = [ - new TextTrack(PropertySymbol.illegalConstructor), - new TextTrack(PropertySymbol.illegalConstructor) + new window.TextTrack(PropertySymbol.illegalConstructor), + new window.TextTrack(PropertySymbol.illegalConstructor) ]; - const textTrackList = new TextTrackList(PropertySymbol.illegalConstructor, items); + const textTrackList = new window.TextTrackList(PropertySymbol.illegalConstructor, items); expect(textTrackList.length).toBe(2); }); }); @@ -47,10 +47,10 @@ describe('TextTrackList', () => { describe('get [index]()', () => { it('Should return the item at the index', () => { const items = [ - new TextTrack(PropertySymbol.illegalConstructor), - new TextTrack(PropertySymbol.illegalConstructor) + new window.TextTrack(PropertySymbol.illegalConstructor), + new window.TextTrack(PropertySymbol.illegalConstructor) ]; - const textTrackList = new TextTrackList(PropertySymbol.illegalConstructor, items); + const textTrackList = new window.TextTrackList(PropertySymbol.illegalConstructor, items); expect(textTrackList[0]).toBe(items[0]); expect(textTrackList[1]).toBe(items[1]); }); @@ -58,21 +58,21 @@ describe('TextTrackList', () => { describe('get [Symbol.toStringTag]()', () => { it('Should return "TextTrackList"', () => { - const textTrackList = new TextTrackList(PropertySymbol.illegalConstructor); + const textTrackList = new window.TextTrackList(PropertySymbol.illegalConstructor); expect(textTrackList[Symbol.toStringTag]).toBe('TextTrackList'); }); }); describe('toLocaleString()', () => { it('Should return "[object TextTrackList]"', () => { - const textTrackList = new TextTrackList(PropertySymbol.illegalConstructor); + const textTrackList = new window.TextTrackList(PropertySymbol.illegalConstructor); expect(textTrackList.toLocaleString()).toBe('[object TextTrackList]'); }); }); describe('toString()', () => { it('Should return "[object TextTrackList]"', () => { - const textTrackList = new TextTrackList(PropertySymbol.illegalConstructor); + const textTrackList = new window.TextTrackList(PropertySymbol.illegalConstructor); expect(textTrackList.toString()).toBe('[object TextTrackList]'); }); }); @@ -80,10 +80,10 @@ describe('TextTrackList', () => { describe('[Symbol.iterator]()', () => { it('Should return an iterator', () => { const items = [ - new TextTrack(PropertySymbol.illegalConstructor), - new TextTrack(PropertySymbol.illegalConstructor) + new window.TextTrack(PropertySymbol.illegalConstructor), + new window.TextTrack(PropertySymbol.illegalConstructor) ]; - const textTrackList = new TextTrackList(PropertySymbol.illegalConstructor, items); + const textTrackList = new window.TextTrackList(PropertySymbol.illegalConstructor, items); const iteratedItems: TextTrack[] = []; for (const item of textTrackList) { @@ -96,17 +96,17 @@ describe('TextTrackList', () => { describe('getTrackById()', () => { it('Should return null if no track is found', () => { - const textTrackList = new TextTrackList(PropertySymbol.illegalConstructor); + const textTrackList = new window.TextTrackList(PropertySymbol.illegalConstructor); expect(textTrackList.getTrackById('test')).toBeNull(); }); it('Should return the track if found', () => { - const track1 = new TextTrack(PropertySymbol.illegalConstructor); - const track2 = new TextTrack(PropertySymbol.illegalConstructor); + const track1 = new window.TextTrack(PropertySymbol.illegalConstructor); + const track2 = new window.TextTrack(PropertySymbol.illegalConstructor); track1[PropertySymbol.id] = 'track1'; track2[PropertySymbol.id] = 'track2'; const items = [track1, track2]; - const textTrackList = new TextTrackList(PropertySymbol.illegalConstructor, items); + const textTrackList = new window.TextTrackList(PropertySymbol.illegalConstructor, items); expect(textTrackList.getTrackById('track1')).toBe(track1); expect(textTrackList.getTrackById('track2')).toBe(track2); }); 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 2c9f1ab3f..c74cc5970 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 @@ -9,6 +9,7 @@ import ErrorEvent from '../../../src/event/events/ErrorEvent.js'; import BrowserWindow from '../../../src/window/BrowserWindow.js'; import Fetch from '../../../src/fetch/Fetch.js'; import BrowserErrorCaptureEnum from '../../../src/browser/enums/BrowserErrorCaptureEnum.js'; +import EventTarget from '../../../src/event/EventTarget.js'; describe('HTMLScriptElement', () => { let window: Window; @@ -177,6 +178,8 @@ describe('HTMLScriptElement', () => { it('Loads external script asynchronously.', async () => { let fetchedURL: string | null = null; let loadEvent: Event | null = null; + let loadEventTarget: EventTarget | null = null; + let loadEventCurrentTarget: EventTarget | null = null; vi.spyOn(Fetch.prototype, 'send').mockImplementation(async function () { fetchedURL = this.request.url; @@ -192,13 +195,17 @@ describe('HTMLScriptElement', () => { script.async = true; script.addEventListener('load', (event) => { loadEvent = event; + loadEventTarget = event.target; + loadEventCurrentTarget = event.currentTarget; }); document.body.appendChild(script); await window.happyDOM?.waitUntilComplete(); - expect(((loadEvent)).target).toBe(script); + expect(((loadEvent)).target).toBe(null); + expect(loadEventTarget).toBe(script); + expect(loadEventCurrentTarget).toBe(script); expect(fetchedURL).toBe('https://localhost:8080/path/to/script.js'); expect(window['test']).toBe('test'); expect(window['currentScript']).toBe(script); @@ -237,6 +244,8 @@ describe('HTMLScriptElement', () => { let fetchedWindow: BrowserWindow | null = null; let fetchedURL: string | null = null; let loadEvent: Event | null = null; + let loadEventTarget: EventTarget | null = null; + let loadEventCurrentTarget: EventTarget | null = null; vi.spyOn(ResourceFetch.prototype, 'fetchSync').mockImplementation(function (url: string) { fetchedWindow = this.window; @@ -248,11 +257,15 @@ describe('HTMLScriptElement', () => { script.src = 'path/to/script.js'; script.addEventListener('load', (event) => { loadEvent = event; + loadEventTarget = event.target; + loadEventCurrentTarget = event.currentTarget; }); document.body.appendChild(script); - expect(((loadEvent)).target).toBe(script); + expect(((loadEvent)).target).toBe(null); + expect(loadEventTarget).toBe(script); + expect(loadEventCurrentTarget).toBe(script); expect(fetchedWindow).toBe(window); expect(fetchedURL).toBe('https://localhost:8080/base/path/to/script.js'); expect(window['test']).toBe('test'); diff --git a/packages/happy-dom/test/nodes/node/Node.test.ts b/packages/happy-dom/test/nodes/node/Node.test.ts index d8ab7e09e..0645eb570 100644 --- a/packages/happy-dom/test/nodes/node/Node.test.ts +++ b/packages/happy-dom/test/nodes/node/Node.test.ts @@ -12,6 +12,7 @@ import ErrorEvent from '../../../src/event/events/ErrorEvent.js'; import { beforeEach, describe, it, expect } from 'vitest'; import ShadowRoot from '../../../src/nodes/shadow-root/ShadowRoot.js'; import * as PropertySymbol from '../../../src/PropertySymbol.js'; +import EventTarget from '../../../src/event/EventTarget.js'; import NodeFactory from '../../../src/nodes/NodeFactory.js'; describe('Node', () => { @@ -86,10 +87,13 @@ describe('Node', () => { expect(() => new Node()).toThrow('Illegal constructor'); }); - it('Doesn\'t throw an exception if "ownerDocument" is defined as a property on the class', () => { - Node[PropertySymbol.ownerDocument] = document; - expect(() => new Node()).not.toThrow(); - Node[PropertySymbol.ownerDocument] = null; + it('Doesn\'t throw an exception if "window" is defined on the prototype that makes it possible to construct the Node with the "new" keyword', () => { + /** + * + */ + class ChildNode extends Node {} + ChildNode.prototype[PropertySymbol.window] = window; + expect(() => new ChildNode()).not.toThrow(); }); it("Doesn't throw an exception if NodeFactory is used", () => { @@ -455,13 +459,6 @@ describe('Node', () => { expect(div.localName).toBe(clone.localName); expect(div.namespaceURI).toBe(clone.namespaceURI); }); - - it("Doesn't remove ownerDocument of a custom element.", () => { - const customElement = document.createElement('custom-counter'); - const clone = customElement.cloneNode(true); - - expect(clone.constructor[PropertySymbol.ownerDocument]).toBe(document); - }); }); describe('appendChild()', () => { @@ -835,18 +832,28 @@ describe('Node', () => { const parent = document.createElement('div'); const event = new Event('click', { bubbles: false }); let childEvent: Event | null = null; + let childEventTarget: EventTarget | null = null; + let childEventCurrentTarget: EventTarget | null = null; let parentEvent: Event | null = null; parent.appendChild(child); - child.addEventListener('click', (event) => (childEvent = event)); - parent.addEventListener('click', (event) => (parentEvent = event)); + child.addEventListener('click', (event) => { + childEvent = event; + childEventTarget = event.target; + childEventCurrentTarget = event.currentTarget; + }); + parent.addEventListener('click', (event) => { + parentEvent = event; + }); expect(child.dispatchEvent(event)).toBe(true); expect(childEvent).toBe(event); - expect(((childEvent)).target).toBe(child); - expect(((childEvent)).currentTarget).toBe(child); + expect(((childEvent)).target).toBe(null); + expect(((childEvent)).currentTarget).toBe(null); + expect(childEventTarget).toBe(child); + expect(childEventCurrentTarget).toBe(child); expect(parentEvent).toBe(null); }); @@ -855,19 +862,35 @@ describe('Node', () => { const parent = document.createElement('div'); const event = new Event('click', { bubbles: true }); let childEvent: Event | null = null; + let childEventTarget: EventTarget | null = null; + let childEventCurrentTarget: EventTarget | null = null; let parentEvent: Event | null = null; + let parentEventTarget: EventTarget | null = null; + let parentEventCurrentTarget: EventTarget | null = null; parent.appendChild(child); - child.addEventListener('click', (event) => (childEvent = event)); - parent.addEventListener('click', (event) => (parentEvent = event)); + child.addEventListener('click', (event) => { + childEvent = event; + childEventTarget = event.target; + childEventCurrentTarget = event.currentTarget; + }); + parent.addEventListener('click', (event) => { + parentEvent = event; + parentEventTarget = event.target; + parentEventCurrentTarget = event.currentTarget; + }); expect(child.dispatchEvent(event)).toBe(true); expect(childEvent).toBe(event); expect(parentEvent).toBe(event); - expect(((parentEvent)).target).toBe(child); - expect(((parentEvent)).currentTarget).toBe(parent); + expect(((parentEvent)).target).toBe(null); + expect(((parentEvent)).currentTarget).toBe(null); + expect(childEventTarget).toBe(child); + expect(childEventCurrentTarget).toBe(child); + expect(parentEventTarget).toBe(child); + expect(parentEventCurrentTarget).toBe(parent); }); it('Does not bubble to parent if propagation is stopped.', () => { diff --git a/packages/happy-dom/test/window/BrowserWindow.test.ts b/packages/happy-dom/test/window/BrowserWindow.test.ts index f39835508..c8062d9cf 100644 --- a/packages/happy-dom/test/window/BrowserWindow.test.ts +++ b/packages/happy-dom/test/window/BrowserWindow.test.ts @@ -35,6 +35,12 @@ import Location from '../../src/location/Location.js'; import HTMLElementConfig from '../../src/config/HTMLElementConfig.js'; import '../types.d.js'; +import EventTarget from '../../src/event/EventTarget.js'; +import EventPhaseEnum from '../../src/event/EventPhaseEnum.js'; +import { PerformanceEntry, PerformanceObserver } from 'perf_hooks'; +import { URLSearchParams } from 'url'; +import Stream from 'stream'; +import { ReadableStream } from 'stream/web'; const PLATFORM = 'X11; ' + @@ -121,7 +127,7 @@ describe('BrowserWindow', () => { describe('get Headers()', () => { it('Returns Headers class.', () => { - expect(window.Headers).toBe(Headers); + expect(new window.Headers()).toBeInstanceOf(Headers); }); }); @@ -139,6 +145,18 @@ describe('BrowserWindow', () => { }); }); + describe('get PerformanceObserver()', () => { + it('Returns PerformanceObserver class.', () => { + expect(window.PerformanceObserver).toBe(PerformanceObserver); + expect(window.PerformanceEntry).toBe(PerformanceEntry); + expect(window.PerformanceObserverEntryList.name).toBe('PerformanceObserverEntryList'); + + expect(() => new window.PerformanceObserverEntryList()).toThrow( + new TypeError('Illegal constructor') + ); + }); + }); + describe('get {ElementClass}()', () => { for (const tagName of Object.keys(HTMLElementConfig)) { it(`Exposes the element class "${HTMLElementConfig[tagName].className}" for tag name "${tagName}"`, () => { @@ -155,6 +173,12 @@ describe('BrowserWindow', () => { }); }); + describe('get process()', () => { + it('Returns undefined.', () => { + expect(window['process']).toBeUndefined(); + }); + }); + describe('get crypto()', () => { it('Exposes "crypto" from the NodeJS crypto package.', () => { const array = new Uint32Array(5); @@ -202,7 +226,7 @@ describe('BrowserWindow', () => { length: 0 }, onLine: true, - permissions: new Permissions(), + permissions: new Permissions(window), clipboard: new Clipboard(window), platform: PLATFORM, plugins: { @@ -309,6 +333,30 @@ describe('BrowserWindow', () => { }); }); + describe('get URLSearchParams()', () => { + it('Returns the URLSearchParams class.', () => { + expect(window.URLSearchParams).toBe(URLSearchParams); + }); + }); + + describe('get WritableStream()', () => { + it('Returns the WritableStream class.', () => { + expect(window.WritableStream).toBe(Stream.Writable); + }); + }); + + describe('get ReadableStream()', () => { + it('Returns the ReadableStream class.', () => { + expect(window.ReadableStream).toBe(ReadableStream); + }); + }); + + describe('get TransformStream()', () => { + it('Returns the TransformStream class.', () => { + expect(window.TransformStream).toBe(Stream.Transform); + }); + }); + describe('eval()', () => { it('Respects direct eval.', () => { const result = window.eval(` @@ -986,6 +1034,35 @@ describe('BrowserWindow', () => { }, 20); }); }); + + it('Supports preventing timeout loops when the setting "preventTimerLoops" is set to "true".', async () => { + let loopCount = 0; + + browser.settings.timer.preventTimerLoops = false; + + const timeoutLoop = (): void => { + if (loopCount < 10) { + loopCount++; + window.setTimeout(timeoutLoop, loopCount < 3 ? 1 : 0); + } + }; + + timeoutLoop(); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + expect(loopCount).toBe(10); + + browser.settings.timer.preventTimerLoops = true; + + loopCount = 0; + + timeoutLoop(); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + expect(loopCount).toBe(3); + }); }); describe('queueMicrotask()', () => { @@ -1210,6 +1287,35 @@ describe('BrowserWindow', () => { }, 20); }); }); + + it('Supports preventing timeout loops when the setting "preventTimerLoops" is set to "true".', async () => { + let loopCount = 0; + + browser.settings.timer.preventTimerLoops = false; + + const timeoutLoop = (): void => { + if (loopCount < 10) { + loopCount++; + window.requestAnimationFrame(timeoutLoop); + } + }; + + timeoutLoop(); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + expect(loopCount).toBe(10); + + browser.settings.timer.preventTimerLoops = true; + + loopCount = 0; + + timeoutLoop(); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + expect(loopCount).toBe(3); + }); }); describe('cancelAnimationFrame()', () => { @@ -1339,14 +1445,22 @@ describe('BrowserWindow', () => { describe('addEventListener()', () => { it('Triggers "load" event if no resources needs to be loaded.', async () => { await new Promise((resolve) => { - let loadEvent: Event | null = null; - - window.addEventListener('load', (event) => { - loadEvent = event; + let event: Event | null = null; + let target: EventTarget | null = null; + let currentTarget: EventTarget | null = null; + + window.addEventListener('load', (e) => { + event = e; + target = e.target; + currentTarget = e.currentTarget; }); setTimeout(() => { - expect((loadEvent).target).toBe(document); + expect((event).target).toBe(null); + expect((event).currentTarget).toBe(null); + expect((event).eventPhase).toBe(EventPhaseEnum.none); + expect(target).toBe(document); + expect(currentTarget).toBe(document); resolve(null); }, 20); }); @@ -1363,6 +1477,8 @@ describe('BrowserWindow', () => { let resourceFetchJSWindow: BrowserWindow | null = null; let resourceFetchJSURL: string | null = null; let loadEvent: Event | null = null; + let loadEventTarget: EventTarget | null = null; + let loadEventCurrentTarget: EventTarget | null = null; vi.spyOn(ResourceFetch.prototype, 'fetch').mockImplementation(async function (url: string) { if (url.endsWith('.css')) { @@ -1378,6 +1494,8 @@ describe('BrowserWindow', () => { window.addEventListener('load', (event) => { loadEvent = event; + loadEventTarget = event.target; + loadEventCurrentTarget = event.currentTarget; }); const script = document.createElement('script'); @@ -1396,7 +1514,11 @@ describe('BrowserWindow', () => { expect(resourceFetchCSSURL).toBe(cssURL); expect(resourceFetchJSWindow === window).toBe(true); expect(resourceFetchJSURL).toBe(jsURL); - expect((loadEvent).target).toBe(document); + expect((loadEvent).target).toBe(null); + expect((loadEvent).currentTarget).toBe(null); + expect((loadEvent).eventPhase).toBe(EventPhaseEnum.none); + expect(loadEventTarget).toBe(document); + expect(loadEventCurrentTarget).toBe(document); expect(document.styleSheets.length).toBe(1); expect(document.styleSheets[0].cssRules[0].cssText).toBe(cssResponse); @@ -1412,6 +1534,7 @@ describe('BrowserWindow', () => { const errorEvents: ErrorEvent[] = []; window.addEventListener('error', (event) => { + expect(event.target).toBe(window); errorEvents.push(event); }); @@ -1425,9 +1548,9 @@ describe('BrowserWindow', () => { setTimeout(() => { expect(errorEvents.length).toBe(2); - expect(errorEvents[0].target).toBe(window); + expect(errorEvents[0].target).toBe(null); expect((errorEvents[0].error).message).toBe('Script error'); - expect(errorEvents[1].target).toBe(window); + expect(errorEvents[1].target).toBe(null); expect((errorEvents[1].error).message).toBe('Timeout error'); resolve(null); diff --git a/packages/integration-test/test/index.js b/packages/integration-test/test/index.js index 4c354bfd1..c3b834fa6 100644 --- a/packages/integration-test/test/index.js +++ b/packages/integration-test/test/index.js @@ -1,8 +1 @@ -await Promise.all([ - import('./tests/Fetch.test.js'), - import('./tests/XMLHttpRequest.test.js'), - import('./tests/WindowGlobals.test.js'), - import('./tests/BrowserFrameExceptionObserver.test.js'), - import('./tests/Browser.test.js'), - import('./tests/CommonJS.test.cjs') -]); +await Promise.all([import('./tests/BrowserExceptionObserver.test.js')]); diff --git a/packages/integration-test/test/tests/Browser.test.js b/packages/integration-test/test/tests/Browser.test.js index c96610115..3bb718658 100644 --- a/packages/integration-test/test/tests/Browser.test.js +++ b/packages/integration-test/test/tests/Browser.test.js @@ -5,9 +5,18 @@ describe('Browser', () => { it('Goes to a "github.com".', async () => { const browser = new Browser({ settings: { - errorCapture: BrowserErrorCaptureEnum.processLevel + errorCapture: BrowserErrorCaptureEnum.processLevel, + + // Github.com has a timer that is very long (hours) and a timer loop that never ends. + timer: { + maxTimeout: 500, + maxInterval: 100, + maxIntervalIterations: 1, + preventTimerLoops: true + } } }); + const page = browser.newPage(); await page.goto('https://github.com/capricorn86'); diff --git a/packages/integration-test/test/tests/BrowserFrameExceptionObserver.test.js b/packages/integration-test/test/tests/BrowserExceptionObserver.test.js similarity index 94% rename from packages/integration-test/test/tests/BrowserFrameExceptionObserver.test.js rename to packages/integration-test/test/tests/BrowserExceptionObserver.test.js index 5e08e4e99..cf377c63d 100644 --- a/packages/integration-test/test/tests/BrowserFrameExceptionObserver.test.js +++ b/packages/integration-test/test/tests/BrowserExceptionObserver.test.js @@ -1,7 +1,7 @@ import { describe, it, expect } from '../utilities/TestFunctions.js'; import { Browser, BrowserErrorCaptureEnum } from 'happy-dom'; -describe('BrowserFrameExceptionObserver', () => { +describe('BrowserExceptionObserver', () => { describe('observe()', () => { it('Observes unhandles fetch rejections.', async () => { const browser = new Browser({ @@ -31,7 +31,7 @@ describe('BrowserFrameExceptionObserver', () => { `); - await new Promise((resolve) => setTimeout(resolve, 2)); + await new Promise((resolve) => setTimeout(resolve, 10)); expect(errorEvent instanceof window.ErrorEvent).toBe(true); expect(errorEvent.error.message).toBe('Test error'); @@ -66,7 +66,7 @@ describe('BrowserFrameExceptionObserver', () => { `); - await new Promise((resolve) => setTimeout(resolve, 2)); + await new Promise((resolve) => setTimeout(resolve, 10)); const consoleOutput = page.virtualConsolePrinter.readAsString();