From 4264264ebc82b92354805ee93f2ad5a3748c419a Mon Sep 17 00:00:00 2001 From: David Ortner Date: Wed, 10 Jan 2024 02:00:25 +0100 Subject: [PATCH] #466@trivial: Fixes MutationObserver and adds support for BrowserFrame.waitForNavigation(). --- package-lock.json | 12 +- .../src/GlobalRegistrator.ts | 5 +- packages/happy-dom/README.md | 35 +++- packages/happy-dom/src/PropertySymbol.ts | 2 + .../async-task-manager/AsyncTaskManager.ts | 28 ++-- packages/happy-dom/src/browser/Browser.ts | 4 +- .../happy-dom/src/browser/BrowserContext.ts | 4 +- .../happy-dom/src/browser/BrowserFrame.ts | 22 ++- packages/happy-dom/src/browser/BrowserPage.ts | 15 +- .../src/browser/DefaultBrowserSettings.ts | 4 +- .../detached-browser/DetachedBrowser.ts | 4 +- .../DetachedBrowserContext.ts | 4 +- .../detached-browser/DetachedBrowserFrame.ts | 22 ++- .../detached-browser/DetachedBrowserPage.ts | 15 +- .../browser/enums/BrowserErrorCaptureEnum.ts | 10 ++ .../enums/BrowserErrorCapturingEnum.ts | 10 -- .../BrowserNavigationCrossOriginPolicyEnum.ts | 6 +- .../happy-dom/src/browser/types/IBrowser.ts | 4 +- .../src/browser/types/IBrowserContext.ts | 4 +- .../src/browser/types/IBrowserFrame.ts | 19 ++- .../src/browser/types/IBrowserPage.ts | 11 +- .../src/browser/types/IBrowserSettings.ts | 6 +- .../browser/types/IOptionalBrowserSettings.ts | 6 +- .../browser/utilities/BrowserFrameFactory.ts | 29 ++-- .../utilities/BrowserFrameNavigator.ts | 37 +++-- .../dom-implementation/DOMImplementation.ts | 6 +- packages/happy-dom/src/event/EventTarget.ts | 6 +- packages/happy-dom/src/fetch/Fetch.ts | 11 +- packages/happy-dom/src/index.ts | 12 +- .../src/mutation-observer/MutationListener.ts | 84 +++++++++- .../src/mutation-observer/MutationObserver.ts | 88 +++++++--- .../src/mutation-observer/MutationRecord.ts | 9 ++ .../src/nodes/character-data/CharacterData.ts | 12 +- .../happy-dom/src/nodes/document/Document.ts | 21 +-- .../document/DocumentReadyStateManager.ts | 2 +- .../document/NodeCreationOwnerDocument.ts | 14 ++ .../happy-dom/src/nodes/element/Element.ts | 10 +- .../src/nodes/element/ElementNamedNodeMap.ts | 37 +++-- .../html-script-element/HTMLScriptElement.ts | 4 +- .../HTMLScriptElementScriptLoader.ts | 4 +- packages/happy-dom/src/nodes/node/Node.ts | 14 +- .../happy-dom/src/nodes/node/NodeUtility.ts | 37 +++-- .../happy-dom/src/window/BrowserWindow.ts | 46 ++++-- .../happy-dom/src/window/DetachedWindowAPI.ts | 8 +- .../happy-dom/src/window/IBrowserWindow.ts | 6 + .../happy-dom/test/browser/Browser.test.ts | 4 +- .../test/browser/BrowserContext.test.ts | 4 +- .../test/browser/BrowserFrame.test.ts | 8 +- .../test/browser/BrowserPage.test.ts | 4 +- .../detached-browser/DetachedBrowser.test.ts | 4 +- .../DetachedBrowserContext.test.ts | 4 +- .../DetachedBrowserFrame.test.ts | 8 +- .../DetachedBrowserPage.test.ts | 4 +- packages/happy-dom/test/fetch/Fetch.test.ts | 4 +- packages/happy-dom/test/fetch/Request.test.ts | 24 +-- .../happy-dom/test/fetch/Response.test.ts | 28 ++-- .../happy-dom/test/fetch/SyncFetch.test.ts | 34 ++-- .../happy-dom/test/file/FileReader.test.ts | 2 +- .../MutationObserver.test.ts | 124 ++++++++++++--- .../test/nodes/element/Element.test.ts | 2 +- .../HTMLAnchorElement.test.ts | 6 +- .../html-link-element/HTMLLinkElement.test.ts | 8 +- .../HTMLScriptElement.test.ts | 16 +- .../test/window/BrowserWindow.test.ts | 2 +- .../test/window/DetachedWindowAPI.test.ts | 16 +- packages/happy-dom/test/window/Window.test.ts | 10 +- .../xml-http-request/XMLHttpRequest.test.ts | 4 +- .../test/tests/Browser.test.js | 7 +- .../BrowserFrameExceptionObserver.test.js | 6 +- packages/jest-environment/src/index.ts | 2 +- .../.prettierrc.cjs | 1 - .../uncaught-exception-observer/README.md | 2 +- .../uncaught-exception-observer/package.json | 78 +-------- .../src/UncaughtExceptionObserver.ts | 99 ------------ .../uncaught-exception-observer/src/index.ts | 3 - .../test/UncaughtExceptionObserver.test.ts | 150 ------------------ .../test/tsconfig.json | 12 -- .../uncaught-exception-observer/tsconfig.json | 37 ----- .../vitest.config.ts | 8 - 79 files changed, 726 insertions(+), 748 deletions(-) create mode 100644 packages/happy-dom/src/browser/enums/BrowserErrorCaptureEnum.ts delete mode 100644 packages/happy-dom/src/browser/enums/BrowserErrorCapturingEnum.ts create mode 100644 packages/happy-dom/src/nodes/document/NodeCreationOwnerDocument.ts delete mode 100644 packages/uncaught-exception-observer/.prettierrc.cjs delete mode 100644 packages/uncaught-exception-observer/src/UncaughtExceptionObserver.ts delete mode 100644 packages/uncaught-exception-observer/src/index.ts delete mode 100644 packages/uncaught-exception-observer/test/UncaughtExceptionObserver.test.ts delete mode 100644 packages/uncaught-exception-observer/test/tsconfig.json delete mode 100644 packages/uncaught-exception-observer/tsconfig.json delete mode 100644 packages/uncaught-exception-observer/vitest.config.ts diff --git a/package-lock.json b/package-lock.json index 7fbf496cc..abad5394c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12021,17 +12021,7 @@ "name": "@happy-dom/uncaught-exception-observer", "version": "0.0.0", "license": "MIT", - "devDependencies": { - "@types/node": "^16.11.7", - "@typescript-eslint/eslint-plugin": "^5.16.0", - "@typescript-eslint/parser": "^5.16.0", - "happy-dom": "^0.0.0", - "prettier": "^2.6.0", - "typescript": "^5.0.4" - }, - "peerDependencies": { - "happy-dom": ">= 2.25.2" - } + "devDependencies": {} } } } diff --git a/packages/global-registrator/src/GlobalRegistrator.ts b/packages/global-registrator/src/GlobalRegistrator.ts index db03c78c7..774165e63 100644 --- a/packages/global-registrator/src/GlobalRegistrator.ts +++ b/packages/global-registrator/src/GlobalRegistrator.ts @@ -25,7 +25,10 @@ export default class GlobalRegistrator { if (global[key] !== window[key] && !IGNORE_LIST.includes(key)) { this.registered[key] = global[key] !== window[key] && global[key] !== undefined ? global[key] : null; - global[key] = typeof window[key] === 'function' ? window[key].bind(global) : window[key]; + global[key] = + typeof window[key] === 'function' && !window[key].toString().startsWith('class ') + ? window[key].bind(global) + : window[key]; } } diff --git a/packages/happy-dom/README.md b/packages/happy-dom/README.md index f4d7ed260..c0aef71ca 100644 --- a/packages/happy-dom/README.md +++ b/packages/happy-dom/README.md @@ -51,14 +51,12 @@ npm install happy-dom A simple example of how you can use Happy DOM. +## Window + ```javascript import { Window } from 'happy-dom'; -const window = new Window({ - url: 'https://localhost:8080', - width: 1024, - height: 768 -}); +const window = new Window({ url: 'https://localhost:8080' }); const document = window.document; document.body.innerHTML = '
'; @@ -72,6 +70,33 @@ container.appendChild(button); console.log(document.body.innerHTML); ``` +## Browser + +```javascript +import { Browser, BrowserErrorCaptureEnum } from 'happy-dom'; + +const browser = new Browser({ settings: { errorCapture: BrowserErrorCaptureEnum.processLevel } }); +const page = browser.newPage(); + +// Navigates page +await page.goto('https://github.com/capricorn86'); + +// Waits for all operations on the page to complete (fetch, timers etc.) +await page.waitUntilComplete(); + +// Clicks on link +page.mainFrame.document.querySelector('a[href*="capricorn86/happy-dom"]').click(); + +// Waits for all operations on the page to complete (fetch, timers etc.) +await page.waitUntilComplete(); + +// Outputs "GitHub - capricorn86/happy-dom: Happy DOM..." +console.log(page.mainFrame.document.title); + +// Closes the browser +await browser.close(); +``` + # Documentation Read more about how to use Happy DOM in our [Wiki](https://github.com/capricorn86/happy-dom/wiki). diff --git a/packages/happy-dom/src/PropertySymbol.ts b/packages/happy-dom/src/PropertySymbol.ts index 72ab95ac1..355027e30 100644 --- a/packages/happy-dom/src/PropertySymbol.ts +++ b/packages/happy-dom/src/PropertySymbol.ts @@ -22,6 +22,7 @@ export const currentScript = Symbol('currentScript'); export const currentTarget = Symbol('currentTarget'); export const data = Symbol('data'); export const defaultView = Symbol('defaultView'); +export const destroy = Symbol('destroy'); export const dirtyness = Symbol('dirtyness'); export const end = Symbol('end'); export const entries = Symbol('entries'); @@ -77,3 +78,4 @@ export const value = Symbol('value'); export const width = Symbol('width'); export const window = Symbol('window'); export const windowResizeListener = Symbol('windowResizeListener'); +export const mutationObservers = Symbol('mutationObservers'); diff --git a/packages/happy-dom/src/async-task-manager/AsyncTaskManager.ts b/packages/happy-dom/src/async-task-manager/AsyncTaskManager.ts index 574a97117..73570190b 100644 --- a/packages/happy-dom/src/async-task-manager/AsyncTaskManager.ts +++ b/packages/happy-dom/src/async-task-manager/AsyncTaskManager.ts @@ -7,17 +7,17 @@ export default class AsyncTaskManager { private runningTaskCount = 0; private runningTimers: NodeJS.Timeout[] = []; private runningImmediates: NodeJS.Immediate[] = []; - private whenCompleteImmediate: NodeJS.Immediate | null = null; - private whenCompleteResolvers: Array<() => void> = []; + private waitUntilCompleteTimer: NodeJS.Immediate | null = null; + private waitUntilCompleteResolvers: Array<() => void> = []; /** * Returns a promise that is resolved when async tasks are complete. * * @returns Promise. */ - public whenComplete(): Promise { + public waitUntilComplete(): Promise { return new Promise((resolve) => { - this.whenCompleteResolvers.push(resolve); + this.waitUntilCompleteResolvers.push(resolve); this.endTask(this.startTask()); }); } @@ -106,12 +106,12 @@ export default class AsyncTaskManager { if (this.runningTasks[taskID]) { delete this.runningTasks[taskID]; this.runningTaskCount--; - if (this.whenCompleteImmediate) { - global.clearImmediate(this.whenCompleteImmediate); + if (this.waitUntilCompleteTimer) { + global.clearImmediate(this.waitUntilCompleteTimer); } if (!this.runningTaskCount && !this.runningTimers.length && !this.runningImmediates.length) { - this.whenCompleteImmediate = global.setImmediate(() => { - this.whenCompleteImmediate = null; + this.waitUntilCompleteTimer = global.setImmediate(() => { + this.waitUntilCompleteTimer = null; if ( !this.runningTaskCount && !this.runningTimers.length && @@ -147,8 +147,8 @@ export default class AsyncTaskManager { * Resolves when complete. */ private resolveWhenComplete(): void { - const resolvers = this.whenCompleteResolvers; - this.whenCompleteResolvers = []; + const resolvers = this.waitUntilCompleteResolvers; + this.waitUntilCompleteResolvers = []; for (const resolver of resolvers) { resolver(); } @@ -169,9 +169,9 @@ export default class AsyncTaskManager { this.runningImmediates = []; this.runningTimers = []; - if (this.whenCompleteImmediate) { - global.clearImmediate(this.whenCompleteImmediate); - this.whenCompleteImmediate = null; + if (this.waitUntilCompleteTimer) { + global.clearImmediate(this.waitUntilCompleteTimer); + this.waitUntilCompleteTimer = null; } for (const immediate of runningImmediates) { @@ -187,6 +187,6 @@ export default class AsyncTaskManager { } // We need to wait for microtasks to complete before resolving. - return this.whenComplete(); + return this.waitUntilComplete(); } } diff --git a/packages/happy-dom/src/browser/Browser.ts b/packages/happy-dom/src/browser/Browser.ts index 48886c7ea..8783ebf1f 100644 --- a/packages/happy-dom/src/browser/Browser.ts +++ b/packages/happy-dom/src/browser/Browser.ts @@ -54,11 +54,11 @@ export default class Browser implements IBrowser { * * @returns Promise. */ - public async whenComplete(): Promise { + public async waitUntilComplete(): Promise { if (this.contexts.length === 0) { throw new Error('No default context. The browser has been closed.'); } - await Promise.all(this.contexts.map((page) => page.whenComplete())); + await Promise.all(this.contexts.map((page) => page.waitUntilComplete())); } /** diff --git a/packages/happy-dom/src/browser/BrowserContext.ts b/packages/happy-dom/src/browser/BrowserContext.ts index a46cee49f..d9c527e81 100644 --- a/packages/happy-dom/src/browser/BrowserContext.ts +++ b/packages/happy-dom/src/browser/BrowserContext.ts @@ -58,8 +58,8 @@ export default class BrowserContext implements IBrowserContext { * * @returns Promise. */ - public async whenComplete(): Promise { - await Promise.all(this.pages.map((page) => page.whenComplete())); + public async waitUntilComplete(): Promise { + await Promise.all(this.pages.map((page) => page.waitUntilComplete())); } /** diff --git a/packages/happy-dom/src/browser/BrowserFrame.ts b/packages/happy-dom/src/browser/BrowserFrame.ts index 2b13da6aa..2882c9726 100644 --- a/packages/happy-dom/src/browser/BrowserFrame.ts +++ b/packages/happy-dom/src/browser/BrowserFrame.ts @@ -12,7 +12,7 @@ import BrowserFrameScriptEvaluator from './utilities/BrowserFrameScriptEvaluator import BrowserFrameNavigator from './utilities/BrowserFrameNavigator.js'; import IReloadOptions from './types/IReloadOptions.js'; import BrowserFrameExceptionObserver from './utilities/BrowserFrameExceptionObserver.js'; -import BrowserErrorCapturingEnum from './enums/BrowserErrorCapturingEnum.js'; +import BrowserErrorCaptureEnum from './enums/BrowserErrorCaptureEnum.js'; import IDocument from '../nodes/document/IDocument.js'; /** @@ -26,6 +26,7 @@ export default class BrowserFrame implements IBrowserFrame { public readonly window: BrowserWindow; public [PropertySymbol.asyncTaskManager] = new AsyncTaskManager(); public [PropertySymbol.exceptionObserver]: BrowserFrameExceptionObserver | null = null; + public [PropertySymbol.listeners]: { navigation: Array<() => void> } = { navigation: [] }; /** * Constructor. @@ -37,7 +38,7 @@ export default class BrowserFrame implements IBrowserFrame { this.window = new BrowserWindow(this); // Attach process level error capturing. - if (page.context.browser.settings.errorCapturing === BrowserErrorCapturingEnum.processLevel) { + if (page.context.browser.settings.errorCapture === BrowserErrorCaptureEnum.processLevel) { this[PropertySymbol.exceptionObserver] = new BrowserFrameExceptionObserver(); this[PropertySymbol.exceptionObserver].observe(this); } @@ -95,17 +96,22 @@ export default class BrowserFrame implements IBrowserFrame { } /** - * Returns a promise that is resolved when all async tasks are complete. - * - * @returns Promise. + * Returns a promise that is resolved when all resources has been loaded, fetch has completed, and all async tasks such as timers are complete. */ - public async whenComplete(): Promise { + public async waitUntilComplete(): Promise { await Promise.all([ - this[PropertySymbol.asyncTaskManager].whenComplete(), - ...this.childFrames.map((frame) => frame.whenComplete()) + this[PropertySymbol.asyncTaskManager].waitUntilComplete(), + ...this.childFrames.map((frame) => frame.waitUntilComplete()) ]); } + /** + * Returns a promise that is resolved when the frame has navigated and the response HTML has been written to the document. + */ + public waitForNavigation(): Promise { + return new Promise((resolve) => this[PropertySymbol.listeners].navigation.push(resolve)); + } + /** * Aborts all ongoing operations. */ diff --git a/packages/happy-dom/src/browser/BrowserPage.ts b/packages/happy-dom/src/browser/BrowserPage.ts index d0d2eab78..4f3f966be 100644 --- a/packages/happy-dom/src/browser/BrowserPage.ts +++ b/packages/happy-dom/src/browser/BrowserPage.ts @@ -79,12 +79,17 @@ export default class BrowserPage implements IBrowserPage { } /** - * Returns a promise that is resolved when all async tasks are complete. - * - * @returns Promise. + * Returns a promise that is resolved when all resources has been loaded, fetch has completed, and all async tasks such as timers are complete. + */ + public waitUntilComplete(): Promise { + return this.mainFrame.waitUntilComplete(); + } + + /** + * Returns a promise that is resolved when the page has navigated and the response HTML has been written to the document. */ - public async whenComplete(): Promise { - await this.mainFrame.whenComplete(); + public waitForNavigation(): Promise { + return this.mainFrame.waitForNavigation(); } /** diff --git a/packages/happy-dom/src/browser/DefaultBrowserSettings.ts b/packages/happy-dom/src/browser/DefaultBrowserSettings.ts index 530cb1523..808d3f347 100644 --- a/packages/happy-dom/src/browser/DefaultBrowserSettings.ts +++ b/packages/happy-dom/src/browser/DefaultBrowserSettings.ts @@ -1,5 +1,5 @@ import PackageVersion from '../version.js'; -import BrowserErrorCapturingEnum from './enums/BrowserErrorCapturingEnum.js'; +import BrowserErrorCaptureEnum from './enums/BrowserErrorCaptureEnum.js'; import BrowserNavigationCrossOriginPolicyEnum from './enums/BrowserNavigationCrossOriginPolicyEnum.js'; import IBrowserSettings from './types/IBrowserSettings.js'; @@ -10,7 +10,7 @@ export default { disableIframePageLoading: false, disableComputedStyleRendering: false, disableErrorCapturing: false, - errorCapturing: BrowserErrorCapturingEnum.tryAndCatch, + errorCapture: BrowserErrorCaptureEnum.tryAndCatch, enableFileSystemHttpRequests: 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 d616adcd8..77fcc17b1 100644 --- a/packages/happy-dom/src/browser/detached-browser/DetachedBrowser.ts +++ b/packages/happy-dom/src/browser/detached-browser/DetachedBrowser.ts @@ -71,8 +71,8 @@ export default class DetachedBrowser implements IBrowser { * * @returns Promise. */ - public async whenComplete(): Promise { - await Promise.all(this.contexts.map((page) => page.whenComplete())); + public async waitUntilComplete(): Promise { + await Promise.all(this.contexts.map((page) => page.waitUntilComplete())); } /** diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserContext.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserContext.ts index 42a4d56be..be8d7fda1 100644 --- a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserContext.ts +++ b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserContext.ts @@ -60,8 +60,8 @@ export default class DetachedBrowserContext implements IBrowserContext { * * @returns Promise. */ - public async whenComplete(): Promise { - await Promise.all(this.pages.map((page) => page.whenComplete())); + public async waitUntilComplete(): Promise { + await Promise.all(this.pages.map((page) => page.waitUntilComplete())); } /** diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts index e0569b56f..4abd4520f 100644 --- a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts +++ b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts @@ -11,7 +11,7 @@ import BrowserFrameScriptEvaluator from '../utilities/BrowserFrameScriptEvaluato import BrowserFrameNavigator from '../utilities/BrowserFrameNavigator.js'; import IBrowserWindow from '../../window/IBrowserWindow.js'; import IReloadOptions from '../types/IReloadOptions.js'; -import BrowserErrorCapturingEnum from '../enums/BrowserErrorCapturingEnum.js'; +import BrowserErrorCaptureEnum from '../enums/BrowserErrorCaptureEnum.js'; import BrowserFrameExceptionObserver from '../utilities/BrowserFrameExceptionObserver.js'; import IDocument from '../../nodes/document/IDocument.js'; @@ -27,6 +27,7 @@ export default class DetachedBrowserFrame implements IBrowserFrame { public window: IBrowserWindow; public [PropertySymbol.asyncTaskManager] = new AsyncTaskManager(); public [PropertySymbol.exceptionObserver]: BrowserFrameExceptionObserver | null = null; + public [PropertySymbol.listeners]: { navigation: Array<() => void> } = { navigation: [] }; /** * Constructor. @@ -41,7 +42,7 @@ export default class DetachedBrowserFrame implements IBrowserFrame { } // Attach process level error capturing. - if (page.context.browser.settings.errorCapturing === BrowserErrorCapturingEnum.processLevel) { + if (page.context.browser.settings.errorCapture === BrowserErrorCaptureEnum.processLevel) { this[PropertySymbol.exceptionObserver] = new BrowserFrameExceptionObserver(); this[PropertySymbol.exceptionObserver].observe(this); } @@ -111,17 +112,22 @@ export default class DetachedBrowserFrame implements IBrowserFrame { } /** - * Returns a promise that is resolved when all async tasks are complete. - * - * @returns Promise. + * Returns a promise that is resolved when all resources has been loaded, fetch has completed, and all async tasks such as timers are complete. */ - public async whenComplete(): Promise { + public async waitUntilComplete(): Promise { await Promise.all([ - this[PropertySymbol.asyncTaskManager].whenComplete(), - ...this.childFrames.map((frame) => frame.whenComplete()) + this[PropertySymbol.asyncTaskManager].waitUntilComplete(), + ...this.childFrames.map((frame) => frame.waitUntilComplete()) ]); } + /** + * Returns a promise that is resolved when the frame has navigated and the response HTML has been written to the document. + */ + public waitForNavigation(): Promise { + return new Promise((resolve) => this[PropertySymbol.listeners].navigation.push(resolve)); + } + /** * Aborts all ongoing operations. */ diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserPage.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserPage.ts index 77a4b01c5..0b041decc 100644 --- a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserPage.ts +++ b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserPage.ts @@ -91,12 +91,17 @@ export default class DetachedBrowserPage implements IBrowserPage { } /** - * Returns a promise that is resolved when all async tasks are complete. - * - * @returns Promise. + * Returns a promise that is resolved when all resources has been loaded, fetch has completed, and all async tasks such as timers are complete. + */ + public waitUntilComplete(): Promise { + return this.mainFrame.waitUntilComplete(); + } + + /** + * Returns a promise that is resolved when the page has navigated and the response HTML has been written to the document. */ - public async whenComplete(): Promise { - await this.mainFrame.whenComplete(); + public waitForNavigation(): Promise { + return this.mainFrame.waitForNavigation(); } /** diff --git a/packages/happy-dom/src/browser/enums/BrowserErrorCaptureEnum.ts b/packages/happy-dom/src/browser/enums/BrowserErrorCaptureEnum.ts new file mode 100644 index 000000000..f7c004f2a --- /dev/null +++ b/packages/happy-dom/src/browser/enums/BrowserErrorCaptureEnum.ts @@ -0,0 +1,10 @@ +enum BrowserErrorCaptureEnum { + /** Happy DOM use try and catch when evaluating code, but will not be able to catch all errors and Promise rejections. This will decrease performance as using try and catch makes the execution significally slower. This is the default setting. */ + tryAndCatch = 'tryAndCatch', + /** Happy DOM will add an event listener to the Node.js process to catch all errors and Promise rejections. This will not work in Jest and Vitest as it conflicts with their error listeners. */ + processLevel = 'processLevel', + /** Error capturing is disabled. Errors and Promise rejections will be thrown. */ + disabled = 'disabled' +} + +export default BrowserErrorCaptureEnum; diff --git a/packages/happy-dom/src/browser/enums/BrowserErrorCapturingEnum.ts b/packages/happy-dom/src/browser/enums/BrowserErrorCapturingEnum.ts deleted file mode 100644 index 398454593..000000000 --- a/packages/happy-dom/src/browser/enums/BrowserErrorCapturingEnum.ts +++ /dev/null @@ -1,10 +0,0 @@ -enum BrowserErrorCapturingEnum { - // Happy DOM will try to catch errors, but it will not be able to catch all errors and Promise rejections. This will decrease performance as using try and catch makes the just in time compiles significally slower. This is the default setting. - tryAndCatch = 'tryAndCatch', - // Happy DOM will add an event listener to the Node.js process to catch all errors and Promise rejections. - processLevel = 'processLevel', - // Error capturing is disabled. All errors and Promise rejections will be thrown. - disabled = 'disabled' -} - -export default BrowserErrorCapturingEnum; diff --git a/packages/happy-dom/src/browser/enums/BrowserNavigationCrossOriginPolicyEnum.ts b/packages/happy-dom/src/browser/enums/BrowserNavigationCrossOriginPolicyEnum.ts index 80ca82dc6..10bd41767 100644 --- a/packages/happy-dom/src/browser/enums/BrowserNavigationCrossOriginPolicyEnum.ts +++ b/packages/happy-dom/src/browser/enums/BrowserNavigationCrossOriginPolicyEnum.ts @@ -1,9 +1,9 @@ enum BrowserNavigationCrossOriginPolicyEnum { - // The browser can navigate to any origin. + /** The browser can navigate to any origin. */ anyOrigin = 'anyOrigin', - // The browser can only navigate to the same origin as the current page or its parent. + /** The browser can only navigate to the same origin as the current page or its parent. */ sameOrigin = 'sameOrigin', - // The browser can never navigate from a secure protocol (https) to an unsecure protocol (http), but it can always navigate to a secure (https). + /** The browser can never navigate from a secure protocol (https) to an unsecure protocol (http), but it can always navigate to a secure (https). */ strictOrigin = 'strictOrigin' } diff --git a/packages/happy-dom/src/browser/types/IBrowser.ts b/packages/happy-dom/src/browser/types/IBrowser.ts index 075b6669d..fa3fb884b 100644 --- a/packages/happy-dom/src/browser/types/IBrowser.ts +++ b/packages/happy-dom/src/browser/types/IBrowser.ts @@ -20,11 +20,11 @@ export default interface IBrowser { close(): void; /** - * Returns a promise that is resolved when all resources has been loaded, fetch has completed, and all tasks such as timers are complete. + * Returns a promise that is resolved when all resources has been loaded, fetch has completed, and all async tasks such as timers are complete. * * @returns Promise. */ - whenComplete(): Promise; + waitUntilComplete(): Promise; /** * Aborts all ongoing operations. diff --git a/packages/happy-dom/src/browser/types/IBrowserContext.ts b/packages/happy-dom/src/browser/types/IBrowserContext.ts index b6d7618d0..3d6fec4e1 100644 --- a/packages/happy-dom/src/browser/types/IBrowserContext.ts +++ b/packages/happy-dom/src/browser/types/IBrowserContext.ts @@ -21,11 +21,11 @@ export default interface IBrowserContext { close(): Promise; /** - * Returns a promise that is resolved when all resources has been loaded, fetch has completed, and all tasks such as timers are complete. + * Returns a promise that is resolved when all resources has been loaded, fetch has completed, and all async tasks such as timers are complete. * * @returns Promise. */ - whenComplete(): Promise; + waitUntilComplete(): Promise; /** * Aborts all ongoing operations. diff --git a/packages/happy-dom/src/browser/types/IBrowserFrame.ts b/packages/happy-dom/src/browser/types/IBrowserFrame.ts index 98507c41a..3d58cfe4d 100644 --- a/packages/happy-dom/src/browser/types/IBrowserFrame.ts +++ b/packages/happy-dom/src/browser/types/IBrowserFrame.ts @@ -14,22 +14,26 @@ import BrowserFrameExceptionObserver from '../utilities/BrowserFrameExceptionObs */ export default interface IBrowserFrame { readonly childFrames: IBrowserFrame[]; + readonly parentFrame: IBrowserFrame | null; + readonly opener: IBrowserFrame | null; + readonly page: IBrowserPage; readonly window: IBrowserWindow; readonly document: IDocument; content: string; url: string; - readonly parentFrame: IBrowserFrame | null; - readonly opener: IBrowserFrame | null; [PropertySymbol.asyncTaskManager]: AsyncTaskManager; [PropertySymbol.exceptionObserver]: BrowserFrameExceptionObserver | null; - readonly page: IBrowserPage; + [PropertySymbol.listeners]: { navigation: Array<() => void> }; /** - * Returns a promise that is resolved when all async tasks are complete. - * - * @returns Promise. + * Returns a promise that is resolved when all resources has been loaded, fetch has completed, and all async tasks such as timers are complete. + */ + waitUntilComplete(): Promise; + + /** + * Returns a promise that is resolved when the frame has navigated and the response HTML has been written to the document. */ - whenComplete(): Promise; + waitForNavigation(): Promise; /** * Aborts all ongoing operations. @@ -56,7 +60,6 @@ export default interface IBrowserFrame { * Reloads the current frame. * * @param [options] Options. - * @returns Response. */ reload(options: IReloadOptions): Promise; } diff --git a/packages/happy-dom/src/browser/types/IBrowserPage.ts b/packages/happy-dom/src/browser/types/IBrowserPage.ts index 8c5ae7c5f..262ce60a3 100644 --- a/packages/happy-dom/src/browser/types/IBrowserPage.ts +++ b/packages/happy-dom/src/browser/types/IBrowserPage.ts @@ -25,11 +25,14 @@ export default interface IBrowserPage { close(): Promise; /** - * Returns a promise that is resolved when all async tasks are complete. - * - * @returns Promise. + * Returns a promise that is resolved when all resources has been loaded, fetch has completed, and all async tasks such as timers are complete. + */ + waitUntilComplete(): Promise; + + /** + * Returns a promise that is resolved when the page has navigated and the response HTML has been written to the document. */ - whenComplete(): Promise; + waitForNavigation(): Promise; /** * Aborts all ongoing operations. diff --git a/packages/happy-dom/src/browser/types/IBrowserSettings.ts b/packages/happy-dom/src/browser/types/IBrowserSettings.ts index 8e02a1355..03b8337ed 100644 --- a/packages/happy-dom/src/browser/types/IBrowserSettings.ts +++ b/packages/happy-dom/src/browser/types/IBrowserSettings.ts @@ -1,4 +1,4 @@ -import BrowserErrorCapturingEnum from '../enums/BrowserErrorCapturingEnum.js'; +import BrowserErrorCaptureEnum from '../enums/BrowserErrorCaptureEnum.js'; import BrowserNavigationCrossOriginPolicyEnum from '../enums/BrowserNavigationCrossOriginPolicyEnum.js'; /** @@ -20,14 +20,14 @@ export default interface IBrowserSettings { /** * Disables error capturing. * - * @deprecated Use errorCapturing instead. + * @deprecated Use errorCapture instead. */ disableErrorCapturing: boolean; /** * Error capturing policy. */ - errorCapturing: BrowserErrorCapturingEnum; + errorCapture: BrowserErrorCaptureEnum; /** * @deprecated Not something that browsers support anymore as it is not secure. diff --git a/packages/happy-dom/src/browser/types/IOptionalBrowserSettings.ts b/packages/happy-dom/src/browser/types/IOptionalBrowserSettings.ts index 680951ab1..fed9ef5b4 100644 --- a/packages/happy-dom/src/browser/types/IOptionalBrowserSettings.ts +++ b/packages/happy-dom/src/browser/types/IOptionalBrowserSettings.ts @@ -1,4 +1,4 @@ -import BrowserErrorCapturingEnum from '../enums/BrowserErrorCapturingEnum.js'; +import BrowserErrorCaptureEnum from '../enums/BrowserErrorCaptureEnum.js'; import BrowserNavigationCrossOriginPolicyEnum from '../enums/BrowserNavigationCrossOriginPolicyEnum.js'; export default interface IOptionalBrowserSettings { @@ -17,14 +17,14 @@ export default interface IOptionalBrowserSettings { /** * Disables error capturing. * - * @deprecated Use errorCapturing instead. + * @deprecated Use errorCapture instead. */ disableErrorCapturing?: boolean; /** * Error capturing policy. */ - errorCapturing?: BrowserErrorCapturingEnum; + errorCapture?: BrowserErrorCaptureEnum; /** * @deprecated Not something that browsers support anymore as it is not secure. diff --git a/packages/happy-dom/src/browser/utilities/BrowserFrameFactory.ts b/packages/happy-dom/src/browser/utilities/BrowserFrameFactory.ts index b9e228510..f6008e4b2 100644 --- a/packages/happy-dom/src/browser/utilities/BrowserFrameFactory.ts +++ b/packages/happy-dom/src/browser/utilities/BrowserFrameFactory.ts @@ -1,7 +1,6 @@ import IBrowserFrame from '../types/IBrowserFrame.js'; import * as PropertySymbol from '../../PropertySymbol.js'; import IBrowserWindow from '../../window/IBrowserWindow.js'; -import WindowBrowserSettingsReader from '../../window/WindowBrowserSettingsReader.js'; import IBrowserPage from '../types/IBrowserPage.js'; /** * Browser frame factory. @@ -42,29 +41,25 @@ export default class BrowserFrameFactory { } } - (frame.window.closed) = true; + // We need to destroy the Window instance before triggering any async tasks as Window.close() is not async. + frame.window[PropertySymbol.destroy](); + (frame.page) = null; + (frame.window) = null; + (frame.opener) = null; if (!frame.childFrames.length) { - const window = frame.window; - WindowBrowserSettingsReader.removeSettings(frame.window); - (frame.page) = null; - (frame.window) = null; - (frame.opener) = null; - window.close(); - frame[PropertySymbol.exceptionObserver]?.disconnect(); - resolve(); - return; + return frame[PropertySymbol.asyncTaskManager] + .destroy() + .then(() => { + frame[PropertySymbol.exceptionObserver]?.disconnect(); + resolve(); + }) + .catch((error) => reject(error)); } Promise.all(frame.childFrames.slice().map((childFrame) => this.destroyFrame(childFrame))) .then(() => { return frame[PropertySymbol.asyncTaskManager].destroy().then(() => { - const window = frame.window; - WindowBrowserSettingsReader.removeSettings(frame.window); - (frame.page) = null; - (frame.window) = null; - (frame.opener) = null; - window.close(); frame[PropertySymbol.exceptionObserver]?.disconnect(); resolve(); }); diff --git a/packages/happy-dom/src/browser/utilities/BrowserFrameNavigator.ts b/packages/happy-dom/src/browser/utilities/BrowserFrameNavigator.ts index 6f6cbec14..82e8cd0ac 100644 --- a/packages/happy-dom/src/browser/utilities/BrowserFrameNavigator.ts +++ b/packages/happy-dom/src/browser/utilities/BrowserFrameNavigator.ts @@ -1,6 +1,5 @@ import IBrowserFrame from '../types/IBrowserFrame.js'; import * as PropertySymbol from '../../PropertySymbol.js'; -import WindowBrowserSettingsReader from '../../window/WindowBrowserSettingsReader.js'; import IGoToOptions from '../types/IGoToOptions.js'; import IResponse from '../../fetch/types/IResponse.js'; import DocumentReadyStateManager from '../../nodes/document/DocumentReadyStateManager.js'; @@ -12,7 +11,7 @@ import BrowserFrameFactory from './BrowserFrameFactory.js'; import BrowserFrameURL from './BrowserFrameURL.js'; import BrowserFrameValidator from './BrowserFrameValidator.js'; import AsyncTaskManager from '../../async-task-manager/AsyncTaskManager.js'; -import BrowserErrorCapturingEnum from '../enums/BrowserErrorCapturingEnum.js'; +import BrowserErrorCaptureEnum from '../enums/BrowserErrorCaptureEnum.js'; /** * Browser frame navigation utility. @@ -59,8 +58,7 @@ export default class BrowserFrameNavigator { if ( frame.page.context.browser.settings.disableErrorCapturing || - frame.page.context.browser.settings.errorCapturing !== - BrowserErrorCapturingEnum.tryAndCatch + frame.page.context.browser.settings.errorCapture !== BrowserErrorCaptureEnum.tryAndCatch ) { frame.window.eval(code); } else { @@ -94,10 +92,9 @@ export default class BrowserFrameNavigator { } (frame.childFrames) = []; - (frame.window.closed) = true; + frame.window[PropertySymbol.destroy](); frame[PropertySymbol.asyncTaskManager].destroy(); frame[PropertySymbol.asyncTaskManager] = new AsyncTaskManager(); - WindowBrowserSettingsReader.removeSettings(frame.window); (frame.window) = new windowClass(frame, { url: targetURL.href, width, height }); (frame.window.devicePixelRatio) = devicePixelRatio; @@ -124,6 +121,15 @@ export default class BrowserFrameNavigator { () => abortController.abort('Request timed out.'), options?.timeout ?? 30000 ); + const finalize = (): void => { + frame.window.clearTimeout(timeout); + readyStateManager.endTask(); + const listeners = frame[PropertySymbol.listeners].navigation; + frame[PropertySymbol.listeners].navigation = []; + for (const listener of listeners) { + listener(); + } + }; try { response = await frame.window.fetch(targetURL.href, { @@ -148,19 +154,26 @@ export default class BrowserFrameNavigator { responseText = await response.text(); } catch (error) { - frame.window.clearTimeout(timeout); - readyStateManager.endTask(); + finalize(); throw error; } - frame.window.clearTimeout(timeout); - frame.content = responseText; - readyStateManager.endTask(); - if (!response.ok) { frame.page.console.error(`GET ${targetURL.href} ${response.status} (${response.statusText})`); } + // 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)); + + await new Promise((resolve) => + frame.window.requestAnimationFrame(() => { + finalize(); + resolve(null); + }) + ); + return response; } } diff --git a/packages/happy-dom/src/dom-implementation/DOMImplementation.ts b/packages/happy-dom/src/dom-implementation/DOMImplementation.ts index 6537d470e..18b8282d2 100644 --- a/packages/happy-dom/src/dom-implementation/DOMImplementation.ts +++ b/packages/happy-dom/src/dom-implementation/DOMImplementation.ts @@ -1,6 +1,7 @@ import DocumentType from '../nodes/document-type/DocumentType.js'; import * as PropertySymbol from '../PropertySymbol.js'; import IDocument from '../nodes/document/IDocument.js'; +import NodeCreationOwnerDocument from '../nodes/document/NodeCreationOwnerDocument.js'; /** * The DOMImplementation interface represents an object providing methods which are not dependent on any particular document. Such an object is returned by the. @@ -45,10 +46,9 @@ export default class DOMImplementation { publicId: string, systemId: string ): DocumentType { - this.#document[PropertySymbol.defaultView].DocumentType[PropertySymbol.ownerDocument] = - this.#document; + NodeCreationOwnerDocument.ownerDocument = this.#document; const documentType = new this.#document[PropertySymbol.defaultView].DocumentType(); - this.#document[PropertySymbol.defaultView].DocumentType[PropertySymbol.ownerDocument] = null; + NodeCreationOwnerDocument.ownerDocument = null; documentType.name = qualifiedName; documentType.publicId = publicId; documentType.systemId = systemId; diff --git a/packages/happy-dom/src/event/EventTarget.ts b/packages/happy-dom/src/event/EventTarget.ts index d33f95b3c..0db9e4913 100644 --- a/packages/happy-dom/src/event/EventTarget.ts +++ b/packages/happy-dom/src/event/EventTarget.ts @@ -9,7 +9,7 @@ import IDocument from '../nodes/document/IDocument.js'; import IBrowserWindow from '../window/IBrowserWindow.js'; import WindowErrorUtility from '../window/WindowErrorUtility.js'; import WindowBrowserSettingsReader from '../window/WindowBrowserSettingsReader.js'; -import BrowserErrorCapturingEnum from '../browser/enums/BrowserErrorCapturingEnum.js'; +import BrowserErrorCaptureEnum from '../browser/enums/BrowserErrorCaptureEnum.js'; /** * Handles events. @@ -169,7 +169,7 @@ export default abstract class EventTarget implements IEventTarget { window && (this !== window || event.type !== 'error') && !browserSettings?.disableErrorCapturing && - browserSettings?.errorCapturing === BrowserErrorCapturingEnum.tryAndCatch + browserSettings?.errorCapture === BrowserErrorCaptureEnum.tryAndCatch ) { WindowErrorUtility.captureError(window, this[onEventName].bind(this, event)); } else { @@ -203,7 +203,7 @@ export default abstract class EventTarget implements IEventTarget { window && (this !== window || event.type !== 'error') && !browserSettings?.disableErrorCapturing && - browserSettings?.errorCapturing === BrowserErrorCapturingEnum.tryAndCatch + browserSettings?.errorCapture === BrowserErrorCaptureEnum.tryAndCatch ) { if ((listener).handleEvent) { WindowErrorUtility.captureError( diff --git a/packages/happy-dom/src/fetch/Fetch.ts b/packages/happy-dom/src/fetch/Fetch.ts index 44abdd72c..77b587760 100644 --- a/packages/happy-dom/src/fetch/Fetch.ts +++ b/packages/happy-dom/src/fetch/Fetch.ts @@ -350,7 +350,12 @@ export default class Fetch { this.resolve = (response: IResponse | Promise): void => { // We can end up here when closing down the browser frame and there is an ongoing request. // Therefore we need to check if browserFrame.page.context is still available. - if (!this.disableCache && response instanceof Response && this.#browserFrame.page.context) { + if ( + !this.disableCache && + response instanceof Response && + this.#browserFrame.page && + this.#browserFrame.page.context + ) { response[PropertySymbol.cachedResponse] = this.#browserFrame.page.context.responseCache.add(this.request, { ...response, @@ -484,7 +489,7 @@ export default class Fetch { return; } - this.response.body.emit('error', error); + this.response.body.destroy(error); } /** @@ -786,7 +791,7 @@ export default class Fetch { return; } - this.response.body.emit('error', error); + this.response.body.destroy(error); if (this.reject) { this.reject(error); diff --git a/packages/happy-dom/src/index.ts b/packages/happy-dom/src/index.ts index e89f5c16c..05d69d5cd 100644 --- a/packages/happy-dom/src/index.ts +++ b/packages/happy-dom/src/index.ts @@ -7,7 +7,7 @@ import DetachedBrowser from './browser/detached-browser/DetachedBrowser.js'; import DetachedBrowserContext from './browser/detached-browser/DetachedBrowserContext.js'; import DetachedBrowserFrame from './browser/detached-browser/DetachedBrowserFrame.js'; import DetachedBrowserPage from './browser/detached-browser/DetachedBrowserPage.js'; -import BrowserErrorCapturingEnum from './browser/enums/BrowserErrorCapturingEnum.js'; +import BrowserErrorCaptureEnum from './browser/enums/BrowserErrorCaptureEnum.js'; import BrowserNavigationCrossOriginPolicyEnum from './browser/enums/BrowserNavigationCrossOriginPolicyEnum.js'; import Clipboard from './clipboard/Clipboard.js'; import ClipboardItem from './clipboard/ClipboardItem.js'; @@ -168,11 +168,19 @@ import type ISVGSVGElement from './nodes/svg-element/ISVGSVGElement.js'; import type IText from './nodes/text/IText.js'; import type IBrowserWindow from './window/IBrowserWindow.js'; import type IWindow from './window/IWindow.js'; +import type IBrowser from './browser/types/IBrowser.js'; +import type IBrowserContext from './browser/types/IBrowserContext.js'; +import type IBrowserFrame from './browser/types/IBrowserFrame.js'; +import type IBrowserPage from './browser/types/IBrowserPage.js'; export type { IAnimationEventInit, IAttr, IBrowserWindow, + IBrowser, + IBrowserContext, + IBrowserFrame, + IBrowserPage, IClipboardEventInit, IComment, ICustomEventInit, @@ -233,7 +241,7 @@ export { Blob, Browser, BrowserContext, - BrowserErrorCapturingEnum, + BrowserErrorCaptureEnum, BrowserFrame, BrowserNavigationCrossOriginPolicyEnum, BrowserPage, diff --git a/packages/happy-dom/src/mutation-observer/MutationListener.ts b/packages/happy-dom/src/mutation-observer/MutationListener.ts index b27678e32..2de8d46fd 100644 --- a/packages/happy-dom/src/mutation-observer/MutationListener.ts +++ b/packages/happy-dom/src/mutation-observer/MutationListener.ts @@ -1,12 +1,88 @@ import IMutationObserverInit from './IMutationObserverInit.js'; import MutationObserver from './MutationObserver.js'; import MutationRecord from './MutationRecord.js'; +import INode from '../nodes/node/INode.js'; +import IBrowserWindow from '../window/IBrowserWindow.js'; /** - * MutationObserverListener is a model for what to listen for on a Node. + * Mutation Observer Listener. */ export default class MutationListener { - public options: IMutationObserverInit = null; - public observer: MutationObserver = null; - public callback: (record: MutationRecord[], observer: MutationObserver) => void = null; + public readonly target: INode; + public options: IMutationObserverInit; + #window: IBrowserWindow; + #observer: MutationObserver; + #callback: (record: MutationRecord[], observer: MutationObserver) => void; + #records: MutationRecord[] = []; + #immediate: NodeJS.Immediate | null = null; + + /** + * Constructor. + * + * @param init Options. + * @param init.window Window. + * @param init.options Options. + * @param init.target Target. + * @param init.observer Observer. + * @param init.callback Callback. + */ + constructor(init: { + window: IBrowserWindow; + options: IMutationObserverInit; + target: INode; + observer: MutationObserver; + callback: (record: MutationRecord[], observer: MutationObserver) => void; + }) { + this.options = init.options; + this.target = init.target; + this.#window = init.window; + this.#observer = init.observer; + this.#callback = init.callback; + } + + /** + * Reports mutations. + * + * @param record Record. + */ + public report(record: MutationRecord): void { + this.#records.push(record); + if (this.#immediate) { + this.#window.cancelAnimationFrame(this.#immediate); + } + this.#immediate = this.#window.requestAnimationFrame(() => { + const records = this.#records; + if (records.length > 0) { + this.#records = []; + this.#callback(records, this.#observer); + } + }); + } + + /** + * Destroys the listener. + */ + public takeRecords(): MutationRecord[] { + if (this.#immediate) { + this.#window.cancelAnimationFrame(this.#immediate); + } + const records = this.#records; + this.#records = []; + return records; + } + + /** + * Destroys the listener. + */ + public destroy(): void { + if (this.#immediate) { + this.#window.cancelAnimationFrame(this.#immediate); + } + (this.options) = null; + (this.target) = null; + (this.#observer) = null; + (this.#callback) = null; + (this.#immediate) = null; + (this.#records) = null; + } } diff --git a/packages/happy-dom/src/mutation-observer/MutationObserver.ts b/packages/happy-dom/src/mutation-observer/MutationObserver.ts index 2eee6dd33..95e306e71 100644 --- a/packages/happy-dom/src/mutation-observer/MutationObserver.ts +++ b/packages/happy-dom/src/mutation-observer/MutationObserver.ts @@ -3,8 +3,9 @@ import * as PropertySymbol from '../PropertySymbol.js'; import INode from '../nodes/node/INode.js'; import Node from '../nodes/node/Node.js'; import IMutationObserverInit from './IMutationObserverInit.js'; -import MutationObserverListener from './MutationListener.js'; +import MutationListener from './MutationListener.js'; import MutationRecord from './MutationRecord.js'; +import IBrowserWindow from '../window/IBrowserWindow.js'; /** * The MutationObserver interface provides the ability to watch for changes being made to the DOM tree. @@ -12,9 +13,9 @@ import MutationRecord from './MutationRecord.js'; * @see https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver */ export default class MutationObserver { - private callback: (records: MutationRecord[], observer: MutationObserver) => void; - private target: INode = null; - private listener: MutationObserverListener = null; + #callback: (records: MutationRecord[], observer: MutationObserver) => void; + #listeners: MutationListener[] = []; + #window: IBrowserWindow | null = null; /** * Constructor. @@ -22,7 +23,7 @@ export default class MutationObserver { * @param callback Callback. */ constructor(callback: (records: MutationRecord[], observer: MutationObserver) => void) { - this.callback = callback; + this.#callback = callback; } /** @@ -34,39 +35,90 @@ export default class MutationObserver { public observe(target: INode, options: IMutationObserverInit): void { if (!target) { throw new DOMException( - 'Failed to observer. The first parameter "target" should be of type "Node".' + `Failed to execute 'observe' on 'MutationObserver': The first parameter "target" should be of type "Node".` ); } + if (!options || (!options.childList && !options.attributes && !options.characterData)) { + throw new DOMException( + `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.ownerDocument + ? target.ownerDocument[PropertySymbol.defaultView] + : target[PropertySymbol.defaultView]; + } + + // Makes sure that attribute names are lower case. + // TODO: Is this correct? options = Object.assign({}, options, { attributeFilter: options.attributeFilter ? options.attributeFilter.map((name) => name.toLowerCase()) : null }); - this.target = target; - this.listener = new MutationObserverListener(); - this.listener.options = options; - this.listener.callback = this.callback.bind(this); - this.listener.observer = this; + /** + * @see https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/observe#reusing_mutationobservers + */ + for (const listener of this.#listeners) { + if (listener.target === target) { + listener.options = options; + return; + } + } + + const listener = new MutationListener({ + window: this.#window, + options, + callback: this.#callback.bind(this), + observer: this, + target + }); + + this.#listeners.push(listener); - (target)[PropertySymbol.observe](this.listener); + // Stores all observers on the window object, so that they can be disconnected when the window is closed. + this.#window[PropertySymbol.mutationObservers].push(this); + + // Starts observing target node. + (target)[PropertySymbol.observe](listener); } /** * Disconnects. */ public disconnect(): void { - if (this.target) { - (this.target)[PropertySymbol.unobserve](this.listener); - this.target = null; + if (this.#listeners.length === 0) { + return; } + + const mutationObservers = this.#window[PropertySymbol.mutationObservers]; + const index = mutationObservers.indexOf(this); + + if (index !== -1) { + mutationObservers.splice(index, 1); + } + + for (const listener of this.#listeners) { + (listener.target)[PropertySymbol.unobserve](listener); + listener.destroy(); + } + + this.#listeners = []; } /** - * Takes records. + * Returns a list of all matching DOM changes that have been detected but not yet processed by the observer's callback function, leaving the mutation queue empty. + * + * @returns Records. */ - public takeRecords(): [] { - return []; + public takeRecords(): MutationRecord[] { + let records = []; + for (const listener of this.#listeners) { + records = records.concat(listener.takeRecords()); + } + return records; } } diff --git a/packages/happy-dom/src/mutation-observer/MutationRecord.ts b/packages/happy-dom/src/mutation-observer/MutationRecord.ts index 2d1b456d5..b5eb97983 100644 --- a/packages/happy-dom/src/mutation-observer/MutationRecord.ts +++ b/packages/happy-dom/src/mutation-observer/MutationRecord.ts @@ -15,4 +15,13 @@ export default class MutationRecord { public attributeName: string = null; public attributeNamespace: string = null; public oldValue: string = null; + + /** + * Constructor. + * + * @param init Options to initialize the mutation record. + */ + constructor(init?: Partial) { + Object.assign(this, init); + } } diff --git a/packages/happy-dom/src/nodes/character-data/CharacterData.ts b/packages/happy-dom/src/nodes/character-data/CharacterData.ts index a80aaeba0..3df313019 100644 --- a/packages/happy-dom/src/nodes/character-data/CharacterData.ts +++ b/packages/happy-dom/src/nodes/character-data/CharacterData.ts @@ -65,11 +65,13 @@ export default abstract class CharacterData extends Node implements ICharacterDa if (this[PropertySymbol.observers].length > 0) { for (const observer of this[PropertySymbol.observers]) { if (observer.options.characterData) { - const record = new MutationRecord(); - record.target = this; - record.type = MutationTypeEnum.characterData; - record.oldValue = observer.options.characterDataOldValue ? oldValue : null; - observer.callback([record], observer.observer); + observer.report( + new MutationRecord({ + target: this, + type: MutationTypeEnum.characterData, + oldValue: observer.options.characterDataOldValue ? oldValue : null + }) + ); } } } diff --git a/packages/happy-dom/src/nodes/document/Document.ts b/packages/happy-dom/src/nodes/document/Document.ts index e60431829..448148598 100644 --- a/packages/happy-dom/src/nodes/document/Document.ts +++ b/packages/happy-dom/src/nodes/document/Document.ts @@ -45,6 +45,7 @@ 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 NodeCreationOwnerDocument from './NodeCreationOwnerDocument.js'; const PROCESSING_INSTRUCTION_TARGET_REGEXP = /^[a-z][a-z0-9-]+$/; @@ -834,9 +835,9 @@ export default class Document extends Node implements IDocument { this[PropertySymbol.defaultView][ElementTag[tagName]] || HTMLUnknownElement; - elementClass[PropertySymbol.ownerDocument] = this; + NodeCreationOwnerDocument.ownerDocument = this; const element = new elementClass(); - elementClass[PropertySymbol.ownerDocument] = null; + NodeCreationOwnerDocument.ownerDocument = null; element.tagName = tagName; (element.namespaceURI) = namespaceURI; @@ -856,9 +857,9 @@ export default class Document extends Node implements IDocument { * @returns Text node. */ public createTextNode(data?: string): IText { - this[PropertySymbol.defaultView].Text[PropertySymbol.ownerDocument] = this; + NodeCreationOwnerDocument.ownerDocument = this; const node = new this[PropertySymbol.defaultView].Text(data); - this[PropertySymbol.defaultView].Text[PropertySymbol.ownerDocument] = null; + NodeCreationOwnerDocument.ownerDocument = null; return node; } @@ -869,9 +870,9 @@ export default class Document extends Node implements IDocument { * @returns Text node. */ public createComment(data?: string): IComment { - this[PropertySymbol.defaultView].Comment[PropertySymbol.ownerDocument] = this; + NodeCreationOwnerDocument.ownerDocument = this; const node = new this[PropertySymbol.defaultView].Comment(data); - this[PropertySymbol.defaultView].Comment[PropertySymbol.ownerDocument] = null; + NodeCreationOwnerDocument.ownerDocument = null; return node; } @@ -942,9 +943,9 @@ export default class Document extends Node implements IDocument { * @returns Element. */ public createAttributeNS(namespaceURI: string, qualifiedName: string): IAttr { - this[PropertySymbol.defaultView].Attr[PropertySymbol.ownerDocument] = this; + NodeCreationOwnerDocument.ownerDocument = this; const attribute = new this[PropertySymbol.defaultView].Attr(); - this[PropertySymbol.defaultView].Attr[PropertySymbol.ownerDocument] = null; + NodeCreationOwnerDocument.ownerDocument = null; attribute.namespaceURI = namespaceURI; attribute.name = qualifiedName; return attribute; @@ -1030,9 +1031,9 @@ export default class Document extends Node implements IDocument { `Failed to execute 'createProcessingInstruction' on 'Document': The data provided ('?>') contains '?>'` ); } - this[PropertySymbol.defaultView].ProcessingInstruction[PropertySymbol.ownerDocument] = this; + NodeCreationOwnerDocument.ownerDocument = this; const processingInstruction = new this[PropertySymbol.defaultView].ProcessingInstruction(data); - this[PropertySymbol.defaultView].ProcessingInstruction[PropertySymbol.ownerDocument] = null; + NodeCreationOwnerDocument.ownerDocument = null; processingInstruction.target = target; return processingInstruction; } diff --git a/packages/happy-dom/src/nodes/document/DocumentReadyStateManager.ts b/packages/happy-dom/src/nodes/document/DocumentReadyStateManager.ts index 0ba7269a1..61121a3a1 100644 --- a/packages/happy-dom/src/nodes/document/DocumentReadyStateManager.ts +++ b/packages/happy-dom/src/nodes/document/DocumentReadyStateManager.ts @@ -24,7 +24,7 @@ export default class DocumentReadyStateManager { * * @returns Promise. */ - public whenComplete(): Promise { + public waitUntilComplete(): Promise { return new Promise((resolve) => { if (this.isComplete) { resolve(); diff --git a/packages/happy-dom/src/nodes/document/NodeCreationOwnerDocument.ts b/packages/happy-dom/src/nodes/document/NodeCreationOwnerDocument.ts new file mode 100644 index 000000000..3524d8fe1 --- /dev/null +++ b/packages/happy-dom/src/nodes/document/NodeCreationOwnerDocument.ts @@ -0,0 +1,14 @@ +import IDocument from './IDocument.js'; + +/** + * When creating a new node, the ownerDocument property is set to the document object associated with the node's. + * The ownerDocument property has to be available in the constructor of the node. + * + * This is used for setting current ownerDocument state when creating a new node. + * + * Another method for achieving this wich is also supported in Node, is to set a static property on the node class. + * This may be necessary for sub-classes wich are bound to a document, but can cause problems in some cases when Node.js sets this.constructor to Reflect.comnstruct(), which is not the original class. + */ +export default <{ ownerDocument: IDocument | null }>{ + ownerDocument: null +}; diff --git a/packages/happy-dom/src/nodes/element/Element.ts b/packages/happy-dom/src/nodes/element/Element.ts index c3eccbc52..b1b829412 100644 --- a/packages/happy-dom/src/nodes/element/Element.ts +++ b/packages/happy-dom/src/nodes/element/Element.ts @@ -31,7 +31,8 @@ import DocumentFragment from '../document-fragment/DocumentFragment.js'; import ElementNamedNodeMap from './ElementNamedNodeMap.js'; import WindowErrorUtility from '../../window/WindowErrorUtility.js'; import WindowBrowserSettingsReader from '../../window/WindowBrowserSettingsReader.js'; -import BrowserErrorCapturingEnum from '../../browser/enums/BrowserErrorCapturingEnum.js'; +import BrowserErrorCaptureEnum from '../../browser/enums/BrowserErrorCaptureEnum.js'; +import NodeCreationOwnerDocument from '../document/NodeCreationOwnerDocument.js'; /** * Element. @@ -691,12 +692,11 @@ export default class Element extends Node implements IElement { throw new DOMException('Shadow root has already been attached.'); } - this.ownerDocument[PropertySymbol.defaultView].ShadowRoot[PropertySymbol.ownerDocument] = - this.ownerDocument; + NodeCreationOwnerDocument.ownerDocument = this.ownerDocument; (this[PropertySymbol.shadowRoot]) = new this.ownerDocument[ PropertySymbol.defaultView ].ShadowRoot(); - this.ownerDocument[PropertySymbol.defaultView].ShadowRoot[PropertySymbol.ownerDocument] = null; + NodeCreationOwnerDocument.ownerDocument = null; (this[PropertySymbol.shadowRoot].host) = this; (this[PropertySymbol.shadowRoot].mode) = init.mode; (this[PropertySymbol.shadowRoot])[PropertySymbol.connectToNode](this); @@ -949,7 +949,7 @@ export default class Element extends Node implements IElement { if ( browserSettings.disableErrorCapturing || - browserSettings.errorCapturing !== BrowserErrorCapturingEnum.tryAndCatch + browserSettings.errorCapture !== BrowserErrorCaptureEnum.tryAndCatch ) { this.ownerDocument[PropertySymbol.defaultView].eval(code); } else { diff --git a/packages/happy-dom/src/nodes/element/ElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/element/ElementNamedNodeMap.ts index fe6d8caa7..e9d763d7f 100644 --- a/packages/happy-dom/src/nodes/element/ElementNamedNodeMap.ts +++ b/packages/happy-dom/src/nodes/element/ElementNamedNodeMap.ts @@ -7,6 +7,7 @@ import IAttr from '../attr/IAttr.js'; import Element from './Element.js'; import HTMLCollection from './HTMLCollection.js'; import IElement from './IElement.js'; +import MutationListener from '../../mutation-observer/MutationListener.js'; /** * Named Node Map. @@ -95,18 +96,22 @@ export default class ElementNamedNodeMap extends NamedNodeMap { // MutationObserver if (this[PropertySymbol.ownerElement][PropertySymbol.observers].length > 0) { - for (const observer of this[PropertySymbol.ownerElement][PropertySymbol.observers]) { + for (const observer of ( + this[PropertySymbol.ownerElement][PropertySymbol.observers] + )) { if ( observer.options.attributes && (!observer.options.attributeFilter || observer.options.attributeFilter.includes(item.name)) ) { - const record = new MutationRecord(); - record.target = this[PropertySymbol.ownerElement]; - record.type = MutationTypeEnum.attributes; - record.attributeName = item.name; - record.oldValue = observer.options.attributeOldValue ? oldValue : null; - observer.callback([record], observer.observer); + observer.report( + new MutationRecord({ + target: this[PropertySymbol.ownerElement], + type: MutationTypeEnum.attributes, + attributeName: item.name, + oldValue: observer.options.attributeOldValue ? oldValue : null + }) + ); } } } @@ -167,18 +172,22 @@ export default class ElementNamedNodeMap extends NamedNodeMap { // MutationObserver if (this[PropertySymbol.ownerElement][PropertySymbol.observers].length > 0) { - for (const observer of this[PropertySymbol.ownerElement][PropertySymbol.observers]) { + for (const observer of ( + this[PropertySymbol.ownerElement][PropertySymbol.observers] + )) { if ( observer.options.attributes && (!observer.options.attributeFilter || observer.options.attributeFilter.includes(removedItem.name)) ) { - const record = new MutationRecord(); - record.target = this[PropertySymbol.ownerElement]; - record.type = MutationTypeEnum.attributes; - record.attributeName = removedItem.name; - record.oldValue = observer.options.attributeOldValue ? removedItem.value : null; - observer.callback([record], observer.observer); + observer.report( + new MutationRecord({ + target: this[PropertySymbol.ownerElement], + type: MutationTypeEnum.attributes, + attributeName: removedItem.name, + oldValue: observer.options.attributeOldValue ? removedItem.value : null + }) + ); } } } 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 517081694..8aef02c4c 100644 --- a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts +++ b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts @@ -10,7 +10,7 @@ import WindowErrorUtility from '../../window/WindowErrorUtility.js'; import WindowBrowserSettingsReader from '../../window/WindowBrowserSettingsReader.js'; import HTMLScriptElementScriptLoader from './HTMLScriptElementScriptLoader.js'; import IBrowserFrame from '../../browser/types/IBrowserFrame.js'; -import BrowserErrorCapturingEnum from '../../browser/enums/BrowserErrorCapturingEnum.js'; +import BrowserErrorCaptureEnum from '../../browser/enums/BrowserErrorCaptureEnum.js'; /** * HTML Script Element. @@ -225,7 +225,7 @@ export default class HTMLScriptElement extends HTMLElement implements IHTMLScrip if ( browserSettings.disableErrorCapturing || - browserSettings.errorCapturing !== BrowserErrorCapturingEnum.tryAndCatch + browserSettings.errorCapture !== BrowserErrorCaptureEnum.tryAndCatch ) { this.ownerDocument[PropertySymbol.defaultView].eval(code); } else { diff --git a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementScriptLoader.ts b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementScriptLoader.ts index 84c5b8a16..7dfa4fe7b 100644 --- a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementScriptLoader.ts +++ b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementScriptLoader.ts @@ -7,7 +7,7 @@ import WindowErrorUtility from '../../window/WindowErrorUtility.js'; import DocumentReadyStateManager from '../document/DocumentReadyStateManager.js'; import IHTMLScriptElement from './IHTMLScriptElement.js'; import IBrowserFrame from '../../browser/types/IBrowserFrame.js'; -import BrowserErrorCapturingEnum from '../../browser/enums/BrowserErrorCapturingEnum.js'; +import BrowserErrorCaptureEnum from '../../browser/enums/BrowserErrorCaptureEnum.js'; /** * Helper class for getting the URL relative to a Location object. @@ -111,7 +111,7 @@ export default class HTMLScriptElementScriptLoader { if ( browserSettings.disableErrorCapturing || - browserSettings.errorCapturing !== BrowserErrorCapturingEnum.tryAndCatch + browserSettings.errorCapture !== BrowserErrorCaptureEnum.tryAndCatch ) { element.ownerDocument[PropertySymbol.defaultView].eval(code); } else { diff --git a/packages/happy-dom/src/nodes/node/Node.ts b/packages/happy-dom/src/nodes/node/Node.ts index 156261fcd..978968b41 100644 --- a/packages/happy-dom/src/nodes/node/Node.ts +++ b/packages/happy-dom/src/nodes/node/Node.ts @@ -11,6 +11,7 @@ import NodeUtility from './NodeUtility.js'; import IAttr from '../attr/IAttr.js'; import NodeList from './NodeList.js'; import INodeList from './INodeList.js'; +import NodeCreationOwnerDocument from '../document/NodeCreationOwnerDocument.js'; /** * Node. @@ -70,8 +71,13 @@ export default class Node extends EventTarget implements INode { */ constructor() { super(); - if ((this.constructor)[PropertySymbol.ownerDocument]) { - this.ownerDocument = (this.constructor)[PropertySymbol.ownerDocument]; + if ( + NodeCreationOwnerDocument.ownerDocument || + (this.constructor)[PropertySymbol.ownerDocument] + ) { + this.ownerDocument = + NodeCreationOwnerDocument.ownerDocument || + (this.constructor)[PropertySymbol.ownerDocument]; } } @@ -273,9 +279,9 @@ export default class Node extends EventTarget implements INode { * @returns Cloned node. */ public cloneNode(deep = false): INode { - (this.constructor)[PropertySymbol.ownerDocument] = this.ownerDocument; + NodeCreationOwnerDocument.ownerDocument = this.ownerDocument; const clone = new (this.constructor)(); - (this.constructor)[PropertySymbol.ownerDocument] = null; + NodeCreationOwnerDocument.ownerDocument = null; // Document has childNodes directly when it is created if (clone[PropertySymbol.childNodes].length) { diff --git a/packages/happy-dom/src/nodes/node/NodeUtility.ts b/packages/happy-dom/src/nodes/node/NodeUtility.ts index e69c3a3bb..33f954156 100644 --- a/packages/happy-dom/src/nodes/node/NodeUtility.ts +++ b/packages/happy-dom/src/nodes/node/NodeUtility.ts @@ -72,17 +72,18 @@ export default class NodeUtility { // MutationObserver if ((ancestorNode)[PropertySymbol.observers].length > 0) { - const record = new MutationRecord(); - record.target = ancestorNode; - record.type = MutationTypeEnum.childList; - record.addedNodes = [node]; + const record = new MutationRecord({ + target: ancestorNode, + type: MutationTypeEnum.childList, + addedNodes: [node] + }); for (const observer of (ancestorNode)[PropertySymbol.observers]) { if (observer.options.subtree) { (node)[PropertySymbol.observe](observer); } if (observer.options.childList) { - observer.callback([record], observer.observer); + observer.report(record); } } } @@ -114,15 +115,18 @@ export default class NodeUtility { // MutationObserver if ((ancestorNode)[PropertySymbol.observers].length > 0) { - const record = new MutationRecord(); - record.target = ancestorNode; - record.type = MutationTypeEnum.childList; - record.removedNodes = [node]; + const record = new MutationRecord({ + target: ancestorNode, + type: MutationTypeEnum.childList, + removedNodes: [node] + }); for (const observer of (ancestorNode)[PropertySymbol.observers]) { - (node)[PropertySymbol.unobserve](observer); + if (observer.options.subtree) { + (node)[PropertySymbol.unobserve](observer); + } if (observer.options.childList) { - observer.callback([record], observer.observer); + observer.report(record); } } } @@ -199,17 +203,18 @@ export default class NodeUtility { // MutationObserver if ((ancestorNode)[PropertySymbol.observers].length > 0) { - const record = new MutationRecord(); - record.target = ancestorNode; - record.type = MutationTypeEnum.childList; - record.addedNodes = [newNode]; + const record = new MutationRecord({ + target: ancestorNode, + type: MutationTypeEnum.childList, + addedNodes: [newNode] + }); for (const observer of (ancestorNode)[PropertySymbol.observers]) { if (observer.options.subtree) { (newNode)[PropertySymbol.observe](observer); } if (observer.options.childList) { - observer.callback([record], observer.observer); + observer.report(record); } } } diff --git a/packages/happy-dom/src/window/BrowserWindow.ts b/packages/happy-dom/src/window/BrowserWindow.ts index f4961e2b2..cb4a75982 100644 --- a/packages/happy-dom/src/window/BrowserWindow.ts +++ b/packages/happy-dom/src/window/BrowserWindow.ts @@ -140,7 +140,7 @@ import IResponseBody from '../fetch/types/IResponseBody.js'; import IResponseInit from '../fetch/types/IResponseInit.js'; import IRequestInfo from '../fetch/types/IRequestInfo.js'; import IBrowserWindow from './IBrowserWindow.js'; -import BrowserErrorCapturingEnum from '../browser/enums/BrowserErrorCapturingEnum.js'; +import BrowserErrorCaptureEnum from '../browser/enums/BrowserErrorCaptureEnum.js'; import AudioImplementation from '../nodes/html-audio-element/Audio.js'; import ImageImplementation from '../nodes/html-image-element/Image.js'; import DocumentFragmentImplementation from '../nodes/document-fragment/DocumentFragment.js'; @@ -493,6 +493,7 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow // Used for tracking capture event listeners to improve performance when they are not used. // See EventTarget class. public [PropertySymbol.captureEventListenerCount]: { [eventType: string]: number } = {}; + public readonly [PropertySymbol.mutationObservers]: MutationObserver[] = []; public readonly [PropertySymbol.readyStateManager] = new DocumentReadyStateManager(this); // Private properties @@ -558,7 +559,8 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow key !== 'constructor' && key[0] !== '_' && key[0] === key[0].toLowerCase() && - typeof this[key] === 'function' + typeof this[key] === 'function' && + !this[key].toString().startsWith('class ') ) { this[key] = this[key].bind(this); } @@ -674,7 +676,7 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow this.DocumentFragment[PropertySymbol.ownerDocument] = this.document; // Ready state manager - this[PropertySymbol.readyStateManager].whenComplete().then(() => { + this[PropertySymbol.readyStateManager].waitUntilComplete().then(() => { (this.document.readyState) = DocumentReadyStateEnum.complete; this.document.dispatchEvent(new Event('readystatechange')); this.document.dispatchEvent(new Event('load', { bubbles: true })); @@ -829,9 +831,8 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow * Closes the window. */ public close(): void { - this.Audio[PropertySymbol.ownerDocument] = null; - this.Image[PropertySymbol.ownerDocument] = null; - this.DocumentFragment[PropertySymbol.ownerDocument] = null; + // When using a Window instance directly, the Window instance is the main frame and we will close the page and destroy the browser. + // When using the Browser API we should only close the page when the Window instance is connected to the main frame (we should not close child frames such as iframes). if (this.#browserFrame.page?.mainFrame === this.#browserFrame) { this.#browserFrame.page.close(); } @@ -860,7 +861,7 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow const useTryCatch = !settings || !settings.disableErrorCapturing || - settings.errorCapturing === BrowserErrorCapturingEnum.tryAndCatch; + settings.errorCapture === BrowserErrorCaptureEnum.tryAndCatch; const id = this.#setTimeout(() => { if (useTryCatch) { WindowErrorUtility.captureError(this, () => callback(...args)); @@ -896,7 +897,7 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow const useTryCatch = !settings || !settings.disableErrorCapturing || - settings.errorCapturing === BrowserErrorCapturingEnum.tryAndCatch; + settings.errorCapture === BrowserErrorCaptureEnum.tryAndCatch; const id = this.#setInterval(() => { if (useTryCatch) { WindowErrorUtility.captureError( @@ -933,7 +934,7 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow const useTryCatch = !settings || !settings.disableErrorCapturing || - settings.errorCapturing === BrowserErrorCapturingEnum.tryAndCatch; + settings.errorCapture === BrowserErrorCaptureEnum.tryAndCatch; const id = global.setImmediate(() => { if (useTryCatch) { WindowErrorUtility.captureError(this, () => callback(this.performance.now())); @@ -970,7 +971,7 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow const useTryCatch = !settings || !settings.disableErrorCapturing || - settings.errorCapturing === BrowserErrorCapturingEnum.tryAndCatch; + settings.errorCapture === BrowserErrorCaptureEnum.tryAndCatch; this.#queueMicrotask(() => { if (!isAborted) { if (useTryCatch) { @@ -1077,4 +1078,29 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow VMGlobalPropertyScript.runInContext(this); } } + + /** + * Destroys the window. + */ + public [PropertySymbol.destroy](): void { + (this.closed) = true; + this.Audio[PropertySymbol.ownerDocument] = null; + this.Image[PropertySymbol.ownerDocument] = null; + this.DocumentFragment[PropertySymbol.ownerDocument] = null; + for (const mutationObserver of this[PropertySymbol.mutationObservers]) { + mutationObserver.disconnect(); + } + + // Disconnects nodes from the document, so that they can be garbage collected. + for (const node of this.document[PropertySymbol.childNodes].slice()) { + this.document.removeChild(node); + } + + this.document[PropertySymbol.activeElement] = null; + this.document[PropertySymbol.nextActiveElement] = null; + this.document[PropertySymbol.currentScript] = null; + this.document[PropertySymbol.selection] = null; + + WindowBrowserSettingsReader.removeSettings(this); + } } diff --git a/packages/happy-dom/src/window/DetachedWindowAPI.ts b/packages/happy-dom/src/window/DetachedWindowAPI.ts index 3fa867eb5..11c009a97 100644 --- a/packages/happy-dom/src/window/DetachedWindowAPI.ts +++ b/packages/happy-dom/src/window/DetachedWindowAPI.ts @@ -41,18 +41,18 @@ export default class DetachedWindowAPI { * * @returns Promise. */ - public whenComplete(): Promise { - return this.#browserFrame.whenComplete(); + public waitUntilComplete(): Promise { + return this.#browserFrame.waitUntilComplete(); } /** * Waits for all async tasks to complete. * - * @deprecated Use whenComplete() instead. + * @deprecated Use waitUntilComplete() instead. * @returns Promise. */ public whenAsyncComplete(): Promise { - return this.whenComplete(); + return this.waitUntilComplete(); } /** diff --git a/packages/happy-dom/src/window/IBrowserWindow.ts b/packages/happy-dom/src/window/IBrowserWindow.ts index 8118fdfae..84e062dce 100644 --- a/packages/happy-dom/src/window/IBrowserWindow.ts +++ b/packages/happy-dom/src/window/IBrowserWindow.ts @@ -1,4 +1,5 @@ import CustomElementRegistry from '../custom-element/CustomElementRegistry.js'; +import * as PropertySymbol from '../PropertySymbol.js'; import Document from '../nodes/document/Document.js'; import IDocument from '../nodes/document/IDocument.js'; import HTMLDocument from '../nodes/html-document/HTMLDocument.js'; @@ -568,4 +569,9 @@ export default interface IBrowserWindow extends IEventTarget, INodeJSGlobal { * @param listener Listener. */ postMessage(message: unknown, targetOrigin?: string, transfer?: unknown[]): void; + + /** + * Destroys the window. + */ + [PropertySymbol.destroy](): void; } diff --git a/packages/happy-dom/test/browser/Browser.test.ts b/packages/happy-dom/test/browser/Browser.test.ts index 9e6e6a6e7..de4ad40a4 100644 --- a/packages/happy-dom/test/browser/Browser.test.ts +++ b/packages/happy-dom/test/browser/Browser.test.ts @@ -97,7 +97,7 @@ describe('Browser', () => { }); }); - describe('whenComplete()', () => { + describe('waitUntilComplete()', () => { it('Returns a promise that is resolved when all resources has been loaded, fetch has completed, and all async tasks such as timers are complete.', async () => { const browser = new Browser(); const page1 = browser.newPage(); @@ -106,7 +106,7 @@ describe('Browser', () => { page1.evaluate('setTimeout(() => { globalThis.test = 1; }, 10);'); page2.evaluate('setTimeout(() => { globalThis.test = 2; }, 10);'); page3.evaluate('setTimeout(() => { globalThis.test = 3; }, 10);'); - await browser.whenComplete(); + await browser.waitUntilComplete(); expect(page1.mainFrame.window['test']).toBe(1); expect(page2.mainFrame.window['test']).toBe(2); expect(page3.mainFrame.window['test']).toBe(3); diff --git a/packages/happy-dom/test/browser/BrowserContext.test.ts b/packages/happy-dom/test/browser/BrowserContext.test.ts index 5db952d7d..c5d49df1f 100644 --- a/packages/happy-dom/test/browser/BrowserContext.test.ts +++ b/packages/happy-dom/test/browser/BrowserContext.test.ts @@ -48,14 +48,14 @@ describe('BrowserContext', () => { }); }); - describe('whenComplete()', () => { + describe('waitUntilComplete()', () => { it('Waits for all pages to complete.', async () => { const browser = new Browser(); const page1 = browser.newPage(); const page2 = browser.newPage(); page1.evaluate('setTimeout(() => { globalThis.test = 1; }, 10);'); page2.evaluate('setTimeout(() => { globalThis.test = 2; }, 10);'); - await browser.defaultContext.whenComplete(); + await browser.defaultContext.waitUntilComplete(); expect(page1.mainFrame.window['test']).toBe(1); expect(page2.mainFrame.window['test']).toBe(2); }); diff --git a/packages/happy-dom/test/browser/BrowserFrame.test.ts b/packages/happy-dom/test/browser/BrowserFrame.test.ts index c9a2317a0..97f6574f7 100644 --- a/packages/happy-dom/test/browser/BrowserFrame.test.ts +++ b/packages/happy-dom/test/browser/BrowserFrame.test.ts @@ -10,7 +10,7 @@ import DOMException from '../../src/exception/DOMException'; import DOMExceptionNameEnum from '../../src/exception/DOMExceptionNameEnum'; import BrowserNavigationCrossOriginPolicyEnum from '../../src/browser/enums/BrowserNavigationCrossOriginPolicyEnum'; import BrowserFrameFactory from '../../src/browser/utilities/BrowserFrameFactory'; -import BrowserErrorCapturingEnum from '../../src/browser/enums/BrowserErrorCapturingEnum'; +import BrowserErrorCaptureEnum from '../../src/browser/enums/BrowserErrorCaptureEnum'; describe('BrowserFrame', () => { afterEach(() => { @@ -79,7 +79,7 @@ describe('BrowserFrame', () => { it('Removes listeners and child nodes before setting the document HTML content.', () => { const browser = new Browser({ - settings: { errorCapturing: BrowserErrorCapturingEnum.disabled } + settings: { errorCapture: BrowserErrorCaptureEnum.disabled } }); const page = browser.defaultContext.newPage(); page.mainFrame.content = '
test
'; @@ -118,7 +118,7 @@ describe('BrowserFrame', () => { }); }); - describe('whenComplete()', () => { + describe('waitUntilComplete()', () => { it('Waits for all pages to complete.', async () => { const browser = new Browser(); const page = browser.newPage(); @@ -127,7 +127,7 @@ describe('BrowserFrame', () => { page.mainFrame.evaluate('setTimeout(() => { globalThis.test = 1; }, 10);'); frame1.evaluate('setTimeout(() => { globalThis.test = 2; }, 10);'); frame2.evaluate('setTimeout(() => { globalThis.test = 3; }, 10);'); - await page.whenComplete(); + await page.waitUntilComplete(); expect(page.mainFrame.window['test']).toBe(1); expect(frame1.window['test']).toBe(2); expect(frame2.window['test']).toBe(3); diff --git a/packages/happy-dom/test/browser/BrowserPage.test.ts b/packages/happy-dom/test/browser/BrowserPage.test.ts index 1a6efe3e3..d99036981 100644 --- a/packages/happy-dom/test/browser/BrowserPage.test.ts +++ b/packages/happy-dom/test/browser/BrowserPage.test.ts @@ -123,7 +123,7 @@ describe('BrowserPage', () => { }); }); - describe('whenComplete()', () => { + describe('waitUntilComplete()', () => { it('Waits for all pages to complete.', async () => { const browser = new Browser(); const page = browser.newPage(); @@ -131,7 +131,7 @@ describe('BrowserPage', () => { const frame2 = BrowserFrameFactory.newChildFrame(page.mainFrame); frame1.evaluate('setTimeout(() => { globalThis.test = 1; }, 10);'); frame2.evaluate('setTimeout(() => { globalThis.test = 2; }, 10);'); - await page.whenComplete(); + await page.waitUntilComplete(); expect(frame1.window['test']).toBe(1); expect(frame2.window['test']).toBe(2); }); diff --git a/packages/happy-dom/test/browser/detached-browser/DetachedBrowser.test.ts b/packages/happy-dom/test/browser/detached-browser/DetachedBrowser.test.ts index d1e35b293..79812f9eb 100644 --- a/packages/happy-dom/test/browser/detached-browser/DetachedBrowser.test.ts +++ b/packages/happy-dom/test/browser/detached-browser/DetachedBrowser.test.ts @@ -95,14 +95,14 @@ describe('DetachedBrowser', () => { }); }); - describe('whenComplete()', () => { + describe('waitUntilComplete()', () => { it('Returns a promise that is resolved when all resources has been loaded, fetch has completed, and all async tasks such as timers are complete.', async () => { const browser = new DetachedBrowser(BrowserWindow); const page1 = browser.newPage(); const page2 = browser.newPage(); page1.evaluate('setTimeout(() => { globalThis.test = 1; }, 10);'); page2.evaluate('setTimeout(() => { globalThis.test = 2; }, 10);'); - await browser.whenComplete(); + await browser.waitUntilComplete(); expect(page1.mainFrame.window['test']).toBe(1); expect(page2.mainFrame.window['test']).toBe(2); }); diff --git a/packages/happy-dom/test/browser/detached-browser/DetachedBrowserContext.test.ts b/packages/happy-dom/test/browser/detached-browser/DetachedBrowserContext.test.ts index 6da6b21aa..afa1ad12b 100644 --- a/packages/happy-dom/test/browser/detached-browser/DetachedBrowserContext.test.ts +++ b/packages/happy-dom/test/browser/detached-browser/DetachedBrowserContext.test.ts @@ -55,14 +55,14 @@ describe('DetachedBrowserContext', () => { }); }); - describe('whenComplete()', () => { + describe('waitUntilComplete()', () => { it('Waits for all pages to complete.', async () => { const browser = new DetachedBrowser(BrowserWindow); const page1 = browser.newPage(); const page2 = browser.newPage(); page1.evaluate('setTimeout(() => { globalThis.test = 1; }, 10);'); page2.evaluate('setTimeout(() => { globalThis.test = 2; }, 10);'); - await browser.defaultContext.whenComplete(); + await browser.defaultContext.waitUntilComplete(); expect(page1.mainFrame.window['test']).toBe(1); expect(page2.mainFrame.window['test']).toBe(2); }); diff --git a/packages/happy-dom/test/browser/detached-browser/DetachedBrowserFrame.test.ts b/packages/happy-dom/test/browser/detached-browser/DetachedBrowserFrame.test.ts index 211905397..43b304a4a 100644 --- a/packages/happy-dom/test/browser/detached-browser/DetachedBrowserFrame.test.ts +++ b/packages/happy-dom/test/browser/detached-browser/DetachedBrowserFrame.test.ts @@ -11,7 +11,7 @@ import DOMException from '../../../src/exception/DOMException'; import DOMExceptionNameEnum from '../../../src/exception/DOMExceptionNameEnum'; import BrowserNavigationCrossOriginPolicyEnum from '../../../src/browser/enums/BrowserNavigationCrossOriginPolicyEnum'; import BrowserFrameFactory from '../../../src/browser/utilities/BrowserFrameFactory'; -import BrowserErrorCapturingEnum from '../../../src/browser/enums/BrowserErrorCapturingEnum'; +import BrowserErrorCaptureEnum from '../../../src/browser/enums/BrowserErrorCaptureEnum'; describe('DetachedBrowserFrame', () => { afterEach(() => { @@ -85,7 +85,7 @@ describe('DetachedBrowserFrame', () => { it('Removes listeners and child nodes before setting the document HTML content.', () => { const browser = new DetachedBrowser(BrowserWindow, { - settings: { errorCapturing: BrowserErrorCapturingEnum.disabled } + settings: { errorCapture: BrowserErrorCaptureEnum.disabled } }); browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.defaultContext.pages[0]; @@ -127,7 +127,7 @@ describe('DetachedBrowserFrame', () => { }); }); - describe('whenComplete()', () => { + describe('waitUntilComplete()', () => { it('Waits for all pages to complete.', async () => { const browser = new DetachedBrowser(BrowserWindow); browser.defaultContext.pages[0].mainFrame.window = new Window(); @@ -137,7 +137,7 @@ describe('DetachedBrowserFrame', () => { page.mainFrame.evaluate('setTimeout(() => { globalThis.test = 1; }, 10);'); frame1.evaluate('setTimeout(() => { globalThis.test = 2; }, 10);'); frame2.evaluate('setTimeout(() => { globalThis.test = 3; }, 10);'); - await page.whenComplete(); + await page.waitUntilComplete(); expect(page.mainFrame.window['test']).toBe(1); expect(frame1.window['test']).toBe(2); expect(frame2.window['test']).toBe(3); diff --git a/packages/happy-dom/test/browser/detached-browser/DetachedBrowserPage.test.ts b/packages/happy-dom/test/browser/detached-browser/DetachedBrowserPage.test.ts index 6c9ada717..43077d8b3 100644 --- a/packages/happy-dom/test/browser/detached-browser/DetachedBrowserPage.test.ts +++ b/packages/happy-dom/test/browser/detached-browser/DetachedBrowserPage.test.ts @@ -132,7 +132,7 @@ describe('DetachedBrowserPage', () => { }); }); - describe('whenComplete()', () => { + describe('waitUntilComplete()', () => { it('Waits for all pages to complete.', async () => { const browser = new DetachedBrowser(BrowserWindow); browser.defaultContext.pages[0].mainFrame.window = new Window(); @@ -141,7 +141,7 @@ describe('DetachedBrowserPage', () => { const frame2 = BrowserFrameFactory.newChildFrame(page.mainFrame); frame1.evaluate('setTimeout(() => { globalThis.test = 1; }, 10);'); frame2.evaluate('setTimeout(() => { globalThis.test = 2; }, 10);'); - await page.whenComplete(); + await page.waitUntilComplete(); expect(frame1.window['test']).toBe(1); expect(frame2.window['test']).toBe(2); }); diff --git a/packages/happy-dom/test/fetch/Fetch.test.ts b/packages/happy-dom/test/fetch/Fetch.test.ts index 82a854d89..dd241f50f 100644 --- a/packages/happy-dom/test/fetch/Fetch.test.ts +++ b/packages/happy-dom/test/fetch/Fetch.test.ts @@ -3432,7 +3432,7 @@ describe('Fetch', () => { expect(response.status).toBe(200); }); - it('Supports window.happyDOM?.whenComplete().', async () => { + it('Supports window.happyDOM?.waitUntilComplete().', async () => { await new Promise((resolve) => { const window = new Window({ url: 'https://localhost:8080/' }); const chunks = ['chunk1', 'chunk2', 'chunk3']; @@ -3488,7 +3488,7 @@ describe('Fetch', () => { } }); - window.happyDOM?.whenComplete().then(() => (isAsyncComplete = true)); + window.happyDOM?.waitUntilComplete().then(() => (isAsyncComplete = true)); window.fetch('https://localhost:8080/test/', { method: 'POST', diff --git a/packages/happy-dom/test/fetch/Request.test.ts b/packages/happy-dom/test/fetch/Request.test.ts index 5c7dcc77c..566af02da 100644 --- a/packages/happy-dom/test/fetch/Request.test.ts +++ b/packages/happy-dom/test/fetch/Request.test.ts @@ -492,7 +492,7 @@ describe('Request', () => { expect(Buffer.from(arrayBuffer).toString()).toBe('Hello World'); }); - it('Supports window.happyDOM?.whenComplete().', async () => { + it('Supports window.happyDOM?.waitUntilComplete().', async () => { await new Promise((resolve) => { const request = new window.Request(TEST_URL, { method: 'POST', body: 'Hello World' }); let isAsyncComplete = false; @@ -502,7 +502,7 @@ describe('Request', () => { new Promise((resolve) => setTimeout(() => resolve(Buffer.from('Hello World')), 10)) ); - window.happyDOM?.whenComplete().then(() => (isAsyncComplete = true)); + window.happyDOM?.waitUntilComplete().then(() => (isAsyncComplete = true)); request.arrayBuffer(); setTimeout(() => { @@ -533,7 +533,7 @@ describe('Request', () => { expect(text).toBe('Hello World'); }); - it('Supports window.happyDOM?.whenComplete().', async () => { + it('Supports window.happyDOM?.waitUntilComplete().', async () => { await new Promise((resolve) => { const request = new window.Request(TEST_URL, { method: 'POST', body: 'Hello World' }); let isAsyncComplete = false; @@ -543,7 +543,7 @@ describe('Request', () => { new Promise((resolve) => setTimeout(() => resolve(Buffer.from('Hello World')), 10)) ); - window.happyDOM?.whenComplete().then(() => (isAsyncComplete = true)); + window.happyDOM?.waitUntilComplete().then(() => (isAsyncComplete = true)); request.blob(); setTimeout(() => { @@ -567,7 +567,7 @@ describe('Request', () => { expect(buffer.toString()).toBe('Hello World'); }); - it('Supports window.happyDOM?.whenComplete().', async () => { + it('Supports window.happyDOM?.waitUntilComplete().', async () => { await new Promise((resolve) => { const request = new window.Request(TEST_URL, { method: 'POST', body: 'Hello World' }); let isAsyncComplete = false; @@ -577,7 +577,7 @@ describe('Request', () => { new Promise((resolve) => setTimeout(() => resolve(Buffer.from('Hello World')), 10)) ); - window.happyDOM?.whenComplete().then(() => (isAsyncComplete = true)); + window.happyDOM?.waitUntilComplete().then(() => (isAsyncComplete = true)); request.buffer(); setTimeout(() => { @@ -600,7 +600,7 @@ describe('Request', () => { expect(text).toBe('Hello World'); }); - it('Supports window.happyDOM?.whenComplete().', async () => { + it('Supports window.happyDOM?.waitUntilComplete().', async () => { await new Promise((resolve) => { const request = new window.Request(TEST_URL, { method: 'POST', body: 'Hello World' }); let isAsyncComplete = false; @@ -610,7 +610,7 @@ describe('Request', () => { new Promise((resolve) => setTimeout(() => resolve(Buffer.from('Hello World')), 10)) ); - window.happyDOM?.whenComplete().then(() => (isAsyncComplete = true)); + window.happyDOM?.waitUntilComplete().then(() => (isAsyncComplete = true)); request.text(); setTimeout(() => { @@ -636,7 +636,7 @@ describe('Request', () => { expect(json).toEqual({ key1: 'value1' }); }); - it('Supports window.happyDOM?.whenComplete().', async () => { + it('Supports window.happyDOM?.waitUntilComplete().', async () => { await new Promise((resolve) => { const request = new window.Request(TEST_URL, { method: 'POST', @@ -651,7 +651,7 @@ describe('Request', () => { ) ); - window.happyDOM?.whenComplete().then(() => (isAsyncComplete = true)); + window.happyDOM?.waitUntilComplete().then(() => (isAsyncComplete = true)); request.json(); setTimeout(() => { @@ -676,7 +676,7 @@ describe('Request', () => { expect(requestFormData).toEqual(formData); }); - it('Supports window.happyDOM?.whenComplete().', async () => { + it('Supports window.happyDOM?.waitUntilComplete().', async () => { await new Promise((resolve) => { const formData = new FormData(); formData.append('some', 'test'); @@ -687,7 +687,7 @@ describe('Request', () => { (): Promise => new Promise((resolve) => setTimeout(() => resolve(formData), 10)) ); - window.happyDOM?.whenComplete().then(() => (isAsyncComplete = true)); + window.happyDOM?.waitUntilComplete().then(() => (isAsyncComplete = true)); request.formData(); setTimeout(() => { diff --git a/packages/happy-dom/test/fetch/Response.test.ts b/packages/happy-dom/test/fetch/Response.test.ts index 7a23173f2..6f1d35eab 100644 --- a/packages/happy-dom/test/fetch/Response.test.ts +++ b/packages/happy-dom/test/fetch/Response.test.ts @@ -113,7 +113,7 @@ describe('Response', () => { expect(Buffer.from(arrayBuffer).toString()).toBe('Hello World'); }); - it('Supports window.happyDOM?.whenComplete().', async () => { + it('Supports window.happyDOM?.waitUntilComplete().', async () => { await new Promise((resolve) => { async function* generate(): AsyncGenerator { yield 'Hello World'; @@ -127,7 +127,7 @@ describe('Response', () => { new Promise((resolve) => setTimeout(() => resolve(Buffer.from('Hello World')), 10)) ); - window.happyDOM?.whenComplete().then(() => (isAsyncComplete = true)); + window.happyDOM?.waitUntilComplete().then(() => (isAsyncComplete = true)); response.arrayBuffer(); setTimeout(() => { @@ -156,7 +156,7 @@ describe('Response', () => { expect(text).toBe('Hello World'); }); - it('Supports window.happyDOM?.whenComplete().', async () => { + it('Supports window.happyDOM?.waitUntilComplete().', async () => { await new Promise((resolve) => { async function* generate(): AsyncGenerator { yield 'Hello World'; @@ -172,7 +172,7 @@ describe('Response', () => { new Promise((resolve) => setTimeout(() => resolve(Buffer.from('Hello World')), 10)) ); - window.happyDOM?.whenComplete().then(() => (isAsyncComplete = true)); + window.happyDOM?.waitUntilComplete().then(() => (isAsyncComplete = true)); response.blob(); setTimeout(() => { @@ -196,7 +196,7 @@ describe('Response', () => { expect(buffer.toString()).toBe('Hello World'); }); - it('Supports window.happyDOM?.whenComplete().', async () => { + it('Supports window.happyDOM?.waitUntilComplete().', async () => { await new Promise((resolve) => { async function* generate(): AsyncGenerator { yield 'Hello World'; @@ -210,7 +210,7 @@ describe('Response', () => { new Promise((resolve) => setTimeout(() => resolve(Buffer.from('Hello World')), 5)) ); - window.happyDOM?.whenComplete().then(() => { + window.happyDOM?.waitUntilComplete().then(() => { isAsyncComplete = true; }); response.buffer(); @@ -235,7 +235,7 @@ describe('Response', () => { expect(text).toBe('Hello World'); }); - it('Supports window.happyDOM?.whenComplete().', async () => { + it('Supports window.happyDOM?.waitUntilComplete().', async () => { await new Promise((resolve) => { async function* generate(): AsyncGenerator { yield 'Hello World'; @@ -249,7 +249,7 @@ describe('Response', () => { new Promise((resolve) => setTimeout(() => resolve(Buffer.from('Hello World')), 10)) ); - window.happyDOM?.whenComplete().then(() => (isAsyncComplete = true)); + window.happyDOM?.waitUntilComplete().then(() => (isAsyncComplete = true)); response.text(); setTimeout(() => { @@ -272,7 +272,7 @@ describe('Response', () => { expect(json).toEqual({ key1: 'value1' }); }); - it('Supports window.happyDOM?.whenComplete().', async () => { + it('Supports window.happyDOM?.waitUntilComplete().', async () => { await new Promise((resolve) => { async function* generate(): AsyncGenerator { yield '{ "key1": "value1" }'; @@ -288,7 +288,7 @@ describe('Response', () => { ) ); - window.happyDOM?.whenComplete().then(() => (isAsyncComplete = true)); + window.happyDOM?.waitUntilComplete().then(() => (isAsyncComplete = true)); response.json(); setTimeout(() => { @@ -387,7 +387,7 @@ describe('Response', () => { expect(await file2.arrayBuffer()).toEqual(imageBuffer.buffer); }); - it('Supports window.happyDOM?.whenComplete() for "application/x-www-form-urlencoded" content.', async () => { + it('Supports window.happyDOM?.waitUntilComplete() for "application/x-www-form-urlencoded" content.', async () => { await new Promise((resolve) => { async function* generate(): AsyncGenerator { yield 'key=value'; @@ -405,7 +405,7 @@ describe('Response', () => { new Promise((resolve) => setTimeout(() => resolve(Buffer.from('')), 10)) ); - window.happyDOM?.whenComplete().then(() => (isAsyncComplete = true)); + window.happyDOM?.waitUntilComplete().then(() => (isAsyncComplete = true)); response.formData(); setTimeout(() => { @@ -419,7 +419,7 @@ describe('Response', () => { }); }); - it('Supports window.happyDOM?.whenComplete() for multipart content.', async () => { + it('Supports window.happyDOM?.waitUntilComplete() for multipart content.', async () => { await new Promise((resolve) => { const response = new window.Response(new FormData()); let isAsyncComplete = false; @@ -431,7 +431,7 @@ describe('Response', () => { ) ); - window.happyDOM?.whenComplete().then(() => (isAsyncComplete = true)); + window.happyDOM?.waitUntilComplete().then(() => (isAsyncComplete = true)); response.formData(); setTimeout(() => { diff --git a/packages/happy-dom/test/fetch/SyncFetch.test.ts b/packages/happy-dom/test/fetch/SyncFetch.test.ts index 7e39ae029..7388434f8 100644 --- a/packages/happy-dom/test/fetch/SyncFetch.test.ts +++ b/packages/happy-dom/test/fetch/SyncFetch.test.ts @@ -2195,7 +2195,7 @@ describe('SyncFetch', () => { expect(requestCount).toBe(1); }); - it('Revalidates cache with a "If-Modified-Since" request for a GET response with "Cache-Control" set to "max-age=0.001".', async () => { + it('Revalidates cache with a "If-Modified-Since" request for a GET response with "Cache-Control" set to "max-age=0.002".', async () => { browserFrame.url = 'https://localhost:8080/'; const url = 'https://localhost:8080/some/path'; @@ -2216,7 +2216,7 @@ describe('SyncFetch', () => { 'last-modified', 'Mon, 11 Dec 2023 02:00:00 GMT', 'cache-control', - 'max-age=0.001' + 'max-age=0.002' ], data: '' } @@ -2233,7 +2233,7 @@ describe('SyncFetch', () => { 'content-length', String(responseText.length), 'cache-control', - 'max-age=0.001', + 'max-age=0.002', 'last-modified', 'Mon, 11 Dec 2023 01:00:00 GMT' ], @@ -2279,7 +2279,7 @@ describe('SyncFetch', () => { expect(headers1).toEqual({ 'content-type': 'text/html', 'content-length': String(responseText.length), - 'cache-control': `max-age=0.001`, + 'cache-control': `max-age=0.002`, 'last-modified': 'Mon, 11 Dec 2023 01:00:00 GMT' }); @@ -2292,7 +2292,7 @@ describe('SyncFetch', () => { expect(headers2).toEqual({ 'content-type': 'text/html', 'content-length': String(responseText.length), - 'Cache-Control': 'max-age=0.001', + 'Cache-Control': 'max-age=0.002', 'Last-Modified': 'Mon, 11 Dec 2023 02:00:00 GMT' }); @@ -2329,7 +2329,7 @@ describe('SyncFetch', () => { ]); }); - it('Updates cache after a failed revalidation with a "If-Modified-Since" request for a GET response with "Cache-Control" set to "max-age=0.001".', async () => { + it('Updates cache after a failed revalidation with a "If-Modified-Since" request for a GET response with "Cache-Control" set to "max-age=0.002".', async () => { browserFrame.url = 'https://localhost:8080/'; const url = 'https://localhost:8080/some/path'; @@ -2353,7 +2353,7 @@ describe('SyncFetch', () => { 'content-length', String(responseText2.length), 'cache-control', - 'max-age=0.001', + 'max-age=0.002', 'last-modified', 'Mon, 11 Dec 2023 02:00:00 GMT' ], @@ -2372,7 +2372,7 @@ describe('SyncFetch', () => { 'content-length', String(responseText1.length), 'cache-control', - 'max-age=0.001', + 'max-age=0.002', 'last-modified', 'Mon, 11 Dec 2023 01:00:00 GMT' ], @@ -2426,7 +2426,7 @@ describe('SyncFetch', () => { expect(headers1).toEqual({ 'content-type': 'text/html', 'content-length': String(responseText1.length), - 'cache-control': `max-age=0.001`, + 'cache-control': `max-age=0.002`, 'last-modified': 'Mon, 11 Dec 2023 01:00:00 GMT' }); @@ -2439,7 +2439,7 @@ describe('SyncFetch', () => { expect(headers2).toEqual({ 'content-type': 'text/html', 'content-length': String(responseText2.length), - 'cache-control': 'max-age=0.001', + 'cache-control': 'max-age=0.002', 'last-modified': 'Mon, 11 Dec 2023 02:00:00 GMT' }); @@ -2519,7 +2519,7 @@ describe('SyncFetch', () => { 'content-length', String(responseText.length), 'cache-control', - 'max-age=0.001', + 'max-age=0.002', 'last-modified', 'Mon, 11 Dec 2023 01:00:00 GMT', 'etag', @@ -2575,7 +2575,7 @@ describe('SyncFetch', () => { expect(headers1).toEqual({ 'content-type': 'text/html', 'content-length': String(responseText.length), - 'cache-control': `max-age=0.001`, + 'cache-control': `max-age=0.002`, 'last-modified': 'Mon, 11 Dec 2023 01:00:00 GMT', etag: etag1 }); @@ -2589,7 +2589,7 @@ describe('SyncFetch', () => { expect(headers2).toEqual({ 'content-type': 'text/html', 'content-length': String(responseText.length), - 'cache-control': `max-age=0.001`, + 'cache-control': `max-age=0.002`, 'Last-Modified': 'Mon, 11 Dec 2023 02:00:00 GMT', ETag: etag2 }); @@ -2653,7 +2653,7 @@ describe('SyncFetch', () => { 'content-length', String(responseText2.length), 'cache-control', - 'max-age=0.001', + 'max-age=0.002', 'last-modified', 'Mon, 11 Dec 2023 02:00:00 GMT', 'etag', @@ -2674,7 +2674,7 @@ describe('SyncFetch', () => { 'content-length', String(responseText1.length), 'cache-control', - 'max-age=0.001', + 'max-age=0.002', 'last-modified', 'Mon, 11 Dec 2023 01:00:00 GMT', 'etag', @@ -2722,7 +2722,7 @@ describe('SyncFetch', () => { expect(headers1).toEqual({ 'content-type': 'text/html', 'content-length': String(responseText1.length), - 'cache-control': `max-age=0.001`, + 'cache-control': `max-age=0.002`, 'last-modified': 'Mon, 11 Dec 2023 01:00:00 GMT', etag: etag1 }); @@ -2736,7 +2736,7 @@ describe('SyncFetch', () => { expect(headers2).toEqual({ 'content-type': 'text/html', 'content-length': String(responseText2.length), - 'cache-control': `max-age=0.001`, + 'cache-control': `max-age=0.002`, 'last-modified': 'Mon, 11 Dec 2023 02:00:00 GMT', etag: etag2 }); diff --git a/packages/happy-dom/test/file/FileReader.test.ts b/packages/happy-dom/test/file/FileReader.test.ts index e97bba1c5..dbb8455e4 100644 --- a/packages/happy-dom/test/file/FileReader.test.ts +++ b/packages/happy-dom/test/file/FileReader.test.ts @@ -22,7 +22,7 @@ describe('FileReader', () => { result = fileReader.result; }); fileReader.readAsDataURL(blob); - await window.happyDOM?.whenComplete(); + await window.happyDOM?.waitUntilComplete(); expect(result).toBe('data:text/plain;charset=utf-8;base64,VEVTVA=='); }); }); diff --git a/packages/happy-dom/test/mutation-observer/MutationObserver.test.ts b/packages/happy-dom/test/mutation-observer/MutationObserver.test.ts index 3366b3004..8bd48f1ab 100644 --- a/packages/happy-dom/test/mutation-observer/MutationObserver.test.ts +++ b/packages/happy-dom/test/mutation-observer/MutationObserver.test.ts @@ -14,7 +14,7 @@ describe('MutationObserver', () => { }); describe('observe()', () => { - it('Observes attributes.', () => { + it('Observes attributes.', async () => { let records: MutationRecord[] = []; const div = document.createElement('div'); const observer = new MutationObserver((mutationRecords) => { @@ -22,6 +22,9 @@ describe('MutationObserver', () => { }); observer.observe(div, { attributes: true }); div.setAttribute('attr', 'value'); + + await new Promise((resolve) => setTimeout(resolve, 1)); + expect(records).toEqual([ { addedNodes: [], @@ -37,7 +40,7 @@ describe('MutationObserver', () => { ]); }); - it('Observes attributes and old attribute values.', () => { + it('Observes attributes and old attribute values.', async () => { let records: MutationRecord[] = []; const div = document.createElement('div'); const observer = new MutationObserver((mutationRecords) => { @@ -46,6 +49,9 @@ describe('MutationObserver', () => { div.setAttribute('attr', 'old'); observer.observe(div, { attributeOldValue: true, attributes: true }); div.setAttribute('attr', 'new'); + + await new Promise((resolve) => setTimeout(resolve, 1)); + expect(records).toEqual([ { addedNodes: [], @@ -61,7 +67,7 @@ describe('MutationObserver', () => { ]); }); - it('Only observes a list of filtered attributes if defined.', () => { + it('Only observes a list of filtered attributes if defined.', async () => { const records: MutationRecord[][] = []; const div = document.createElement('div'); const observer = new MutationObserver((mutationRecords) => { @@ -76,6 +82,9 @@ describe('MutationObserver', () => { }); div.setAttribute('attr1', 'new'); div.setAttribute('attr2', 'new'); + + await new Promise((resolve) => setTimeout(resolve, 1)); + expect(records).toEqual([ [ { @@ -93,7 +102,7 @@ describe('MutationObserver', () => { ]); }); - it('Observers character data changes on text node.', () => { + it('Observers character data changes on text node.', async () => { const records: MutationRecord[][] = []; const text = document.createTextNode('old'); const observer = new MutationObserver((mutationRecords) => { @@ -101,6 +110,9 @@ describe('MutationObserver', () => { }); observer.observe(text, { characterData: true, characterDataOldValue: true }); text.textContent = 'new'; + + await new Promise((resolve) => setTimeout(resolve, 1)); + expect(records).toEqual([ [ { @@ -118,7 +130,7 @@ describe('MutationObserver', () => { ]); }); - it('Observers character data changes to child text nodes.', () => { + it('Observers character data changes to child text nodes.', async () => { const records: MutationRecord[][] = []; const div = document.createElement('div'); const text = document.createTextNode('old'); @@ -128,6 +140,9 @@ describe('MutationObserver', () => { div.appendChild(text); observer.observe(div, { characterData: true, subtree: true, characterDataOldValue: true }); text.textContent = 'new'; + + await new Promise((resolve) => setTimeout(resolve, 1)); + expect(records).toEqual([ [ { @@ -145,7 +160,7 @@ describe('MutationObserver', () => { ]); }); - it('Observers added and removed nodes.', () => { + it('Observers added and removed nodes.', async () => { const records: MutationRecord[][] = []; const div = document.createElement('div'); const span = document.createElement('span'); @@ -160,6 +175,8 @@ describe('MutationObserver', () => { span.appendChild(article); span.removeChild(article); + await new Promise((resolve) => setTimeout(resolve, 1)); + expect(records).toEqual([ [ { @@ -172,9 +189,7 @@ describe('MutationObserver', () => { removedNodes: [], target: div, type: 'childList' - } - ], - [ + }, { addedNodes: [span], attributeName: null, @@ -185,9 +200,7 @@ describe('MutationObserver', () => { removedNodes: [], target: div, type: 'childList' - } - ], - [ + }, { addedNodes: [article], attributeName: null, @@ -198,9 +211,7 @@ describe('MutationObserver', () => { removedNodes: [], target: span, type: 'childList' - } - ], - [ + }, { addedNodes: [], attributeName: null, @@ -216,26 +227,67 @@ describe('MutationObserver', () => { ]); }); - it('Calls callback with the observer as second parameter.', () => { + it('Can observe document node.', async () => { + let records: MutationRecord[] = []; const div = document.createElement('div'); - const observer = new MutationObserver((mutationRecords, observer) => { - expect(observer).toBeInstanceOf(MutationObserver); + const observer = new MutationObserver((mutationRecords) => { + records = mutationRecords; }); - observer.observe(div, { attributes: true }); + document.appendChild(div); + observer.observe(document, { attributes: true, subtree: true }); div.setAttribute('attr', 'value'); + + await new Promise((resolve) => setTimeout(resolve, 1)); + + expect(records).toEqual([ + { + addedNodes: [], + attributeName: 'attr', + attributeNamespace: null, + nextSibling: null, + oldValue: null, + previousSibling: null, + removedNodes: [], + target: div, + type: 'attributes' + } + ]); }); }); describe('disconnect()', () => { - it('Disconnects the observer.', () => { + it('Disconnects the observer.', async () => { let records: MutationRecord[] = []; const div = document.createElement('div'); const observer = new MutationObserver((mutationRecords) => { records = mutationRecords; }); + observer.observe(div, { attributes: true }); + observer.disconnect(); + div.setAttribute('attr', 'value'); + + await new Promise((resolve) => setTimeout(resolve, 1)); + + expect(records).toEqual([]); + }); + + it('Disconnects the observer when closing window.', async () => { + let records: MutationRecord[] = []; + const div = document.createElement('div'); + const observer = new MutationObserver((mutationRecords) => { + records = mutationRecords; + }); + observer.observe(div, { attributes: true }); + + window.close(); + + div.setAttribute('attr', 'value'); + + await new Promise((resolve) => setTimeout(resolve, 1)); + expect(records).toEqual([]); }); @@ -244,4 +296,36 @@ describe('MutationObserver', () => { expect(() => observer.disconnect()).not.toThrow(); }); }); + + describe('takeRecords()', () => { + it('Returns all records and empties the record queue.', async () => { + let records: MutationRecord[] = []; + const div = document.createElement('div'); + const observer = new MutationObserver((mutationRecords) => { + records = mutationRecords; + }); + + observer.observe(div, { attributes: true }); + + div.setAttribute('attr', 'value'); + + expect(observer.takeRecords()).toEqual([ + { + addedNodes: [], + attributeName: 'attr', + attributeNamespace: null, + nextSibling: null, + oldValue: null, + previousSibling: null, + removedNodes: [], + target: div, + type: 'attributes' + } + ]); + + await new Promise((resolve) => setTimeout(resolve, 1)); + + expect(records).toEqual([]); + }); + }); }); diff --git a/packages/happy-dom/test/nodes/element/Element.test.ts b/packages/happy-dom/test/nodes/element/Element.test.ts index 83b1bc377..f0cc9e3ca 100644 --- a/packages/happy-dom/test/nodes/element/Element.test.ts +++ b/packages/happy-dom/test/nodes/element/Element.test.ts @@ -1510,7 +1510,7 @@ describe('Element', () => { element[functionName]({ left: 50, top: 60, behavior: 'smooth' }); expect(element.scrollLeft).toBe(0); expect(element.scrollTop).toBe(0); - await window.happyDOM?.whenComplete(); + await window.happyDOM?.waitUntilComplete(); expect(element.scrollLeft).toBe(50); expect(element.scrollTop).toBe(60); }); diff --git a/packages/happy-dom/test/nodes/html-anchor-element/HTMLAnchorElement.test.ts b/packages/happy-dom/test/nodes/html-anchor-element/HTMLAnchorElement.test.ts index 8895759d5..784e190a6 100644 --- a/packages/happy-dom/test/nodes/html-anchor-element/HTMLAnchorElement.test.ts +++ b/packages/happy-dom/test/nodes/html-anchor-element/HTMLAnchorElement.test.ts @@ -428,7 +428,7 @@ describe('HTMLAnchorElement', () => { expect(newWindow === window).toBe(false); expect(newWindow.location.href).toBe('https://www.example.com/'); - await browser.whenComplete(); + await browser.waitUntilComplete(); expect(newWindow.document.body.innerHTML).toBe('Test'); @@ -459,7 +459,7 @@ describe('HTMLAnchorElement', () => { expect(newWindow === window).toBe(false); expect(newWindow.location.href).toBe('https://www.example.com/'); - await browser.whenComplete(); + await browser.waitUntilComplete(); expect(newWindow.document.body.innerHTML).toBe('Test'); @@ -488,7 +488,7 @@ describe('HTMLAnchorElement', () => { expect(newWindow === window).toBe(false); expect(newWindow.location.href).toBe('https://www.example.com/'); - await browser.whenComplete(); + await browser.waitUntilComplete(); expect(newWindow.document.body.innerHTML).toBe('Test'); }); 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 52fc6093f..fef661d65 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 @@ -100,7 +100,7 @@ describe('HTMLLinkElement', () => { element.rel = 'stylesheet'; element.href = 'https://localhost:8080/test/path/file.css'; - await window.happyDOM?.whenComplete(); + await window.happyDOM?.waitUntilComplete(); expect(loadedWindow).toBe(window); expect(loadedURL).toBe('https://localhost:8080/test/path/file.css'); @@ -127,7 +127,7 @@ describe('HTMLLinkElement', () => { element.rel = 'stylesheet'; element.href = 'https://localhost:8080/test/path/file.css'; - await window.happyDOM?.whenComplete(); + await window.happyDOM?.waitUntilComplete(); expect(((errorEvent)).error).toEqual(thrownError); expect(((errorEvent)).message).toEqual('error'); @@ -175,7 +175,7 @@ describe('HTMLLinkElement', () => { document.body.appendChild(element); - await window.happyDOM?.whenComplete(); + await window.happyDOM?.waitUntilComplete(); expect(loadedWindow).toBe(window); expect(loadedURL).toBe('https://localhost:8080/test/path/file.css'); @@ -201,7 +201,7 @@ describe('HTMLLinkElement', () => { document.body.appendChild(element); - await window.happyDOM?.whenComplete(); + await window.happyDOM?.waitUntilComplete(); expect(((errorEvent)).error).toEqual(thrownError); expect(((errorEvent)).message).toEqual('error'); 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 44e3258b0..f640619b1 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,7 +9,7 @@ import ErrorEvent from '../../../src/event/events/ErrorEvent.js'; import IWindow from '../../../src/window/IWindow.js'; import IBrowserWindow from '../../../src/window/IBrowserWindow.js'; import Fetch from '../../../src/fetch/Fetch.js'; -import BrowserErrorCapturingEnum from '../../../src/browser/enums/BrowserErrorCapturingEnum.js'; +import BrowserErrorCaptureEnum from '../../../src/browser/enums/BrowserErrorCaptureEnum.js'; describe('HTMLScriptElement', () => { let window: IWindow; @@ -99,7 +99,7 @@ describe('HTMLScriptElement', () => { element.async = true; element.src = 'https://localhost:8080/path/to/script.js'; - await window.happyDOM?.whenComplete(); + await window.happyDOM?.waitUntilComplete(); expect(window['test']).toBe('test'); }); @@ -119,7 +119,7 @@ describe('HTMLScriptElement', () => { element.async = true; element.src = 'https://localhost:8080/path/to/script.js'; - await window.happyDOM?.whenComplete(); + await window.happyDOM?.waitUntilComplete(); expect(window['test']).toBe(undefined); }); @@ -190,7 +190,7 @@ describe('HTMLScriptElement', () => { document.body.appendChild(script); - await window.happyDOM?.whenComplete(); + await window.happyDOM?.waitUntilComplete(); expect(((loadEvent)).target).toBe(script); expect(fetchedURL).toBe('https://localhost:8080/path/to/script.js'); @@ -219,7 +219,7 @@ describe('HTMLScriptElement', () => { document.body.appendChild(script); - await window.happyDOM?.whenComplete(); + await window.happyDOM?.waitUntilComplete(); expect(((errorEvent)).message).toBe( 'Failed to perform request to "https://localhost:8080/path/to/script.js". Status 404 Not Found.' @@ -451,7 +451,7 @@ describe('HTMLScriptElement', () => { document.body.appendChild(script); - await window.happyDOM?.whenComplete(); + await window.happyDOM?.waitUntilComplete(); expect(((errorEvent)).error?.message).toBe( 'Invalid regular expression: missing /' @@ -522,9 +522,9 @@ describe('HTMLScriptElement', () => { }).toThrow(new TypeError('Invalid regular expression: missing /')); }); - it('Throws an exception when appending an element that contains invalid Javascript and the Happy DOM setting "errorCapturing" is set to "disabled".', () => { + it('Throws an exception when appending an element that contains invalid Javascript and the Happy DOM setting "errorCapture" is set to "disabled".', () => { window = new Window({ - settings: { errorCapturing: BrowserErrorCapturingEnum.disabled } + settings: { errorCapture: BrowserErrorCaptureEnum.disabled } }); document = window.document; diff --git a/packages/happy-dom/test/window/BrowserWindow.test.ts b/packages/happy-dom/test/window/BrowserWindow.test.ts index 8efac6ba9..56c612a89 100644 --- a/packages/happy-dom/test/window/BrowserWindow.test.ts +++ b/packages/happy-dom/test/window/BrowserWindow.test.ts @@ -940,7 +940,7 @@ describe('BrowserWindow', () => { expect(window.pageYOffset).toBe(0); expect(window.scrollX).toBe(0); expect(window.scrollY).toBe(0); - await browserFrame.whenComplete(); + await browserFrame.waitUntilComplete(); expect(window.document.documentElement.scrollLeft).toBe(50); expect(window.document.documentElement.scrollTop).toBe(60); expect(window.pageXOffset).toBe(50); diff --git a/packages/happy-dom/test/window/DetachedWindowAPI.test.ts b/packages/happy-dom/test/window/DetachedWindowAPI.test.ts index bae7bbe0c..e8f28d5cb 100644 --- a/packages/happy-dom/test/window/DetachedWindowAPI.test.ts +++ b/packages/happy-dom/test/window/DetachedWindowAPI.test.ts @@ -47,7 +47,7 @@ describe('DetachedWindowAPI', () => { }); }); - describe('whenComplete()', () => { + describe('waitUntilComplete()', () => { it('Resolves the Promise when all async tasks has been completed.', async () => { const responseText = '{ "test": "test" }'; mockModule('https', { @@ -79,7 +79,7 @@ describe('DetachedWindowAPI', () => { window.location.href = 'https://localhost:8080'; let isFirstWhenAsyncCompleteCalled = false; - window.happyDOM?.whenComplete().then(() => { + window.happyDOM?.waitUntilComplete().then(() => { isFirstWhenAsyncCompleteCalled = true; }); let tasksDone = 0; @@ -109,20 +109,20 @@ describe('DetachedWindowAPI', () => { tasksDone++; }); }); - await window.happyDOM?.whenComplete(); + await window.happyDOM?.waitUntilComplete(); expect(tasksDone).toBe(6); expect(isFirstWhenAsyncCompleteCalled).toBe(true); }); }); describe('whenAsyncComplete()', () => { - it('Calls whenComplete().', async () => { + it('Calls waitUntilComplete().', async () => { let isCalled = false; - vi.spyOn(window.happyDOM, 'whenComplete').mockImplementation(() => { + vi.spyOn(window.happyDOM, 'waitUntilComplete').mockImplementation(() => { isCalled = true; return Promise.resolve(); }); - await window.happyDOM?.whenComplete(); + await window.happyDOM?.waitUntilComplete(); expect(isCalled).toBe(true); }); }); @@ -132,7 +132,7 @@ describe('DetachedWindowAPI', () => { await new Promise((resolve) => { window.location.href = 'https://localhost:8080'; let isFirstWhenAsyncCompleteCalled = false; - window.happyDOM?.whenComplete().then(() => { + window.happyDOM?.waitUntilComplete().then(() => { isFirstWhenAsyncCompleteCalled = true; }); let tasksDone = 0; @@ -178,7 +178,7 @@ describe('DetachedWindowAPI', () => { .catch(() => {}); let isSecondWhenAsyncCompleteCalled = false; - window.happyDOM?.whenComplete().then(() => { + window.happyDOM?.waitUntilComplete().then(() => { isSecondWhenAsyncCompleteCalled = true; }); diff --git a/packages/happy-dom/test/window/Window.test.ts b/packages/happy-dom/test/window/Window.test.ts index dbc299aef..fcac18924 100644 --- a/packages/happy-dom/test/window/Window.test.ts +++ b/packages/happy-dom/test/window/Window.test.ts @@ -12,7 +12,7 @@ import PackageVersion from '../../src/version.js'; import IHTMLIFrameElement from '../../src/nodes/html-iframe-element/IHTMLIFrameElement.js'; import DetachedWindowAPI from '../../src/window/DetachedWindowAPI.js'; import '../types.d.js'; -import BrowserErrorCapturingEnum from '../../src/browser/enums/BrowserErrorCapturingEnum.js'; +import BrowserErrorCaptureEnum from '../../src/browser/enums/BrowserErrorCaptureEnum.js'; import * as PropertySymbol from '../../src/PropertySymbol.js'; const GET_NAVIGATOR_PLATFORM = (): string => { @@ -177,8 +177,8 @@ describe('Window', () => { expect(windowWithOptions.happyDOM?.settings.disableCSSFileLoading).toBe(false); expect(windowWithOptions.happyDOM?.settings.disableIframePageLoading).toBe(false); expect(windowWithOptions.happyDOM?.settings.disableErrorCapturing).toBe(false); - expect(windowWithOptions.happyDOM?.settings.errorCapturing).toBe( - BrowserErrorCapturingEnum.tryAndCatch + expect(windowWithOptions.happyDOM?.settings.errorCapture).toBe( + BrowserErrorCaptureEnum.tryAndCatch ); expect(windowWithOptions.happyDOM?.settings.enableFileSystemHttpRequests).toBe(false); expect(windowWithOptions.happyDOM?.settings.navigator.userAgent).toBe('test'); @@ -199,8 +199,8 @@ describe('Window', () => { expect(windowWithoutOptions.happyDOM?.settings.disableCSSFileLoading).toBe(false); expect(windowWithoutOptions.happyDOM?.settings.disableIframePageLoading).toBe(false); expect(windowWithoutOptions.happyDOM?.settings.disableErrorCapturing).toBe(false); - expect(windowWithoutOptions.happyDOM?.settings.errorCapturing).toBe( - BrowserErrorCapturingEnum.tryAndCatch + expect(windowWithoutOptions.happyDOM?.settings.errorCapture).toBe( + BrowserErrorCaptureEnum.tryAndCatch ); expect(windowWithoutOptions.happyDOM?.settings.enableFileSystemHttpRequests).toBe(false); expect(windowWithoutOptions.happyDOM?.settings.navigator.userAgent).toBe( diff --git a/packages/happy-dom/test/xml-http-request/XMLHttpRequest.test.ts b/packages/happy-dom/test/xml-http-request/XMLHttpRequest.test.ts index f2d1dcf12..d539758d5 100644 --- a/packages/happy-dom/test/xml-http-request/XMLHttpRequest.test.ts +++ b/packages/happy-dom/test/xml-http-request/XMLHttpRequest.test.ts @@ -1055,7 +1055,7 @@ describe('XMLHttpRequest', () => { request.open('GET', REQUEST_URL, true); request.send(); - await window.happyDOM?.whenComplete(); + await window.happyDOM?.waitUntilComplete(); expect(request.responseText).toBe(responseText); }); @@ -1126,7 +1126,7 @@ describe('XMLHttpRequest', () => { request.open('GET', REQUEST_URL, true); request.send(); - await window.happyDOM?.whenComplete(); + await window.happyDOM?.waitUntilComplete(); expect(request.readyState).toBe(XMLHttpRequestReadyStateEnum.done); expect(request.responseText).toBe(responseText); diff --git a/packages/integration-test/test/tests/Browser.test.js b/packages/integration-test/test/tests/Browser.test.js index 531ccc2af..ba6aae4e7 100644 --- a/packages/integration-test/test/tests/Browser.test.js +++ b/packages/integration-test/test/tests/Browser.test.js @@ -1,18 +1,17 @@ import { describe, it, expect } from '../utilities/TestFunctions.js'; -import { Browser, BrowserErrorCapturingEnum } from 'happy-dom'; +import { Browser, BrowserErrorCaptureEnum } from 'happy-dom'; describe('Browser', () => { it('Goes to a real page.', async () => { const browser = new Browser({ - settings: { errorCapturing: BrowserErrorCapturingEnum.processLevel } + settings: { errorCapture: BrowserErrorCaptureEnum.processLevel } }); const page = browser.newPage(); await page.goto('https://github.com/capricorn86'); - await page.whenComplete(); page.mainFrame.document.querySelector('a[href="/capricorn86/happy-dom"]').click(); - await page.whenComplete(); + await page.waitForNavigation(); expect(page.mainFrame.url).toBe('https://github.com/capricorn86/happy-dom'); expect( diff --git a/packages/integration-test/test/tests/BrowserFrameExceptionObserver.test.js b/packages/integration-test/test/tests/BrowserFrameExceptionObserver.test.js index f274acdd1..5e08e4e99 100644 --- a/packages/integration-test/test/tests/BrowserFrameExceptionObserver.test.js +++ b/packages/integration-test/test/tests/BrowserFrameExceptionObserver.test.js @@ -1,11 +1,11 @@ import { describe, it, expect } from '../utilities/TestFunctions.js'; -import { Browser, BrowserErrorCapturingEnum } from 'happy-dom'; +import { Browser, BrowserErrorCaptureEnum } from 'happy-dom'; describe('BrowserFrameExceptionObserver', () => { describe('observe()', () => { it('Observes unhandles fetch rejections.', async () => { const browser = new Browser({ - settings: { errorCapturing: BrowserErrorCapturingEnum.processLevel } + settings: { errorCapture: BrowserErrorCaptureEnum.processLevel } }); const page = browser.newPage(); const window = page.mainFrame.window; @@ -42,7 +42,7 @@ describe('BrowserFrameExceptionObserver', () => { it('Observes uncaught exceptions.', async () => { const browser = new Browser({ - settings: { errorCapturing: BrowserErrorCapturingEnum.processLevel } + settings: { errorCapture: BrowserErrorCaptureEnum.processLevel } }); const page = browser.newPage(); const window = page.mainFrame.window; diff --git a/packages/jest-environment/src/index.ts b/packages/jest-environment/src/index.ts index 5bce2c074..f4a213d50 100644 --- a/packages/jest-environment/src/index.ts +++ b/packages/jest-environment/src/index.ts @@ -90,7 +90,7 @@ export default class HappyDOMEnvironment implements JestEnvironment { global: (this.window) }); - // Jest is using the setTimeout function from Happy DOM internally for detecting when a test times out, but this causes window.happyDOM?.whenComplete() and window.happyDOM?.abort() to not work as expected. + // Jest is using the setTimeout function from Happy DOM internally for detecting when a test times out, but this causes window.happyDOM?.waitUntilComplete() and window.happyDOM?.abort() to not work as expected. // Hopefully Jest can fix this in the future as this fix is not very pretty. const happyDOMSetTimeout = this.global.setTimeout; (<(...args: unknown[]) => number>this.global.setTimeout) = (...args: unknown[]): number => { diff --git a/packages/uncaught-exception-observer/.prettierrc.cjs b/packages/uncaught-exception-observer/.prettierrc.cjs deleted file mode 100644 index 7b9cd9691..000000000 --- a/packages/uncaught-exception-observer/.prettierrc.cjs +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('../happy-dom/.prettierrc.cjs'); diff --git a/packages/uncaught-exception-observer/README.md b/packages/uncaught-exception-observer/README.md index 0c4c87747..47760d78c 100644 --- a/packages/uncaught-exception-observer/README.md +++ b/packages/uncaught-exception-observer/README.md @@ -1,4 +1,4 @@ -:warning: **This package is deprecated. Happy DOM now supports built in by setting "errorCapturing" to "processLevel".** :warning: +:warning: **This package is deprecated. Happy DOM now supports built in by setting "errorCapture" to "processLevel".** :warning: ![Happy DOM Logo](https://github.com/capricorn86/happy-dom/raw/master/docs/happy-dom-logo.jpg) diff --git a/packages/uncaught-exception-observer/package.json b/packages/uncaught-exception-observer/package.json index fab419450..2876cb785 100644 --- a/packages/uncaught-exception-observer/package.json +++ b/packages/uncaught-exception-observer/package.json @@ -6,81 +6,11 @@ "repository": "https://github.com/capricorn86/happy-dom", "author": "David Ortner", "description": "A utility for observing uncaught exceptions thrown in Happy DOM and dispatch them as events on the Happy DOM window.", - "main": "lib/index.js", - "type": "module", - "exports": { - ".": { - "import": "./lib/index.js", - "require": "./cjs/index.cjs", - "default": "./lib/index.js" - }, - "./lib/*.js": { - "import": "./lib/*.js", - "require": "./cjs/*.cjs", - "default": "./lib/*.js" - }, - "./lib/*.ts": { - "import": "./lib/*.ts", - "require": "./cjs/*.ts", - "default": "./lib/*.ts" - }, - "./lib/*.map": { - "import": "./lib/*.map", - "require": "./cjs/*.map", - "default": "./lib/*.map" - }, - "./cjs/*.cjs": { - "import": "./cjs/*.cjs", - "require": "./cjs/*.cjs", - "default": "./cjs/*.cjs" - }, - "./cjs/*.ts": { - "import": "./cjs/*.ts", - "require": "./cjs/*.ts", - "default": "./cjs/*.ts" - }, - "./cjs/*.map": { - "import": "./cjs/*.map", - "require": "./cjs/*.map", - "default": "./cjs/*.map" - }, - "./src/*.ts": "./src/*.ts", - "./package.json": "./package.json", - "./.eslintrc": "./.eslintrc.js" - }, - "keywords": [ - "jsdom", - "happy", - "dom", - "webcomponents", - "web", - "component", - "custom", - "elements", - "uncaught", - "error", - "exception", - "observer" - ], + "keywords": [], "publishConfig": { "access": "public" }, - "scripts": { - "compile": "tsc && tsc --moduleResolution Node --module CommonJS --outDir cjs && npm run change-cjs-file-extension", - "change-cjs-file-extension": "node ../happy-dom/bin/change-file-extension.cjs --dir=./cjs --fromExt=.js --toExt=.cjs", - "watch": "npm run compile && tsc -w --preserveWatchOutput", - "test": "tsc --project ./test && node ./tmp/UncaughtExceptionObserver.test.js", - "test:debug": "tsc --project ./test && node --inspect-brk ./tmp/UncaughtExceptionObserver.test.js" - }, - "peerDependencies": { - "happy-dom": ">= 2.25.2" - }, - "devDependencies": { - "@typescript-eslint/eslint-plugin": "^5.16.0", - "@typescript-eslint/parser": "^5.16.0", - "@types/node": "^16.11.7", - "prettier": "^2.6.0", - "typescript": "^5.0.4", - "happy-dom": "^0.0.0" - } + "scripts": {}, + "peerDependencies": {}, + "devDependencies": {} } diff --git a/packages/uncaught-exception-observer/src/UncaughtExceptionObserver.ts b/packages/uncaught-exception-observer/src/UncaughtExceptionObserver.ts deleted file mode 100644 index 2e4933054..000000000 --- a/packages/uncaught-exception-observer/src/UncaughtExceptionObserver.ts +++ /dev/null @@ -1,99 +0,0 @@ -import IWindow from 'happy-dom/lib/window/IWindow.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 UncaughtExceptionObserver { - private static listenerCount = 0; - private window: IWindow | 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 window - */ - public observe(window: IWindow): void { - if (this.window) { - throw new Error('Already observing.'); - } - - this.window = window; - - (this.constructor).listenerCount++; - - this.uncaughtExceptionListener = ( - error: unknown, - origin: 'uncaughtException' | 'unhandledRejection' - ) => { - if (origin === 'unhandledRejection') { - return; - } - - if ( - (error instanceof this.window.Error || error instanceof this.window.DOMException) && - error.stack?.includes('/happy-dom/') - ) { - this.window.console.error(error); - this.window.dispatchEvent( - new this.window.ErrorEvent('error', { error, message: error.message }) - ); - } 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 ( - (error instanceof this.window.Error || error instanceof this.window.DOMException) && - error.stack?.includes('/happy-dom/') - ) { - this.window.console.error(error); - this.window.dispatchEvent( - new this.window.ErrorEvent('error', { error, message: error.message }) - ); - } 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.window) { - return; - } - - (this.constructor).listenerCount--; - - process.off('uncaughtException', this.uncaughtExceptionListener); - process.off('unhandledRejection', this.uncaughtRejectionListener); - - this.uncaughtExceptionListener = null; - this.uncaughtRejectionListener = null; - this.window = null; - } -} diff --git a/packages/uncaught-exception-observer/src/index.ts b/packages/uncaught-exception-observer/src/index.ts deleted file mode 100644 index 1f39e1192..000000000 --- a/packages/uncaught-exception-observer/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import UncaughtExceptionObserver from './UncaughtExceptionObserver.js'; - -export { UncaughtExceptionObserver }; diff --git a/packages/uncaught-exception-observer/test/UncaughtExceptionObserver.test.ts b/packages/uncaught-exception-observer/test/UncaughtExceptionObserver.test.ts deleted file mode 100644 index f68a7bbdb..000000000 --- a/packages/uncaught-exception-observer/test/UncaughtExceptionObserver.test.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { Window, ErrorEvent, IResponse } from 'happy-dom'; -import UncaughtExceptionObserver from '../lib/UncaughtExceptionObserver.js'; - -async function itObservesUnhandledFetchRejections(): Promise { - const window = new Window(); - const document = window.document; - const observer = new UncaughtExceptionObserver(); - let errorEvent: ErrorEvent | null = null; - - observer.observe(window); - - window.addEventListener('error', (event) => (errorEvent = event)); - - window.fetch = () => { - return new Promise((resolve) => setTimeout(() => resolve({}), 0)); - }; - - document.write(` - - `); - - await new Promise((resolve) => setTimeout(resolve, 2)); - - observer.disconnect(); - - if (!(errorEvent instanceof window.ErrorEvent)) { - throw new Error('Error event not dispatched.'); - } - - if (errorEvent.error.message !== 'Test error') { - throw new Error('Error message not correct.'); - } - - if (errorEvent.message !== 'Test error') { - throw new Error('Error message not correct.'); - } -} - -async function itObservesUnhandledJavaScriptFetchRejections(): Promise { - const window = new Window({ - settings: { - disableErrorCapturing: true - } - }); - const document = window.document; - const observer = new UncaughtExceptionObserver(); - let errorEvent: ErrorEvent | null = null; - - observer.observe(window); - - window.addEventListener('error', (event) => (errorEvent = event)); - - document.write(` - - `); - - await new Promise((resolve) => setTimeout(resolve, 10)); - - observer.disconnect(); - - if (!(errorEvent instanceof window.ErrorEvent)) { - throw new Error('Error event not dispatched.'); - } - - if ( - errorEvent.error.message !== - 'Fetch to "https://localhost:3000/404.js" failed. Error: connect ECONNREFUSED 127.0.0.1:3000' - ) { - throw new Error('Error message not correct.'); - } - - if ( - errorEvent.message !== - 'Fetch to "https://localhost:3000/404.js" failed. Error: connect ECONNREFUSED 127.0.0.1:3000' - ) { - throw new Error('Error message not correct.'); - } -} - -async function itObservesUncaughtExceptions(): Promise { - const window = new Window(); - const document = window.document; - const observer = new UncaughtExceptionObserver(); - let errorEvent: ErrorEvent | null = null; - - observer.observe(window); - - window.addEventListener('error', (event) => (errorEvent = event)); - - window['customSetTimeout'] = setTimeout.bind(globalThis); - - document.write(` - - `); - - await new Promise((resolve) => setTimeout(resolve, 2)); - - observer.disconnect(); - - const consoleOutput = window.happyDOM?.virtualConsolePrinter.readAsString(); - - if (consoleOutput.startsWith('Error: Test error\nat Timeout.eval')) { - throw new Error(`Console output not correct.`); - } - - if (!(errorEvent instanceof window.ErrorEvent)) { - throw new Error('Error event not dispatched.'); - } - - if (errorEvent.error.message !== 'Test error') { - throw new Error('Error message not correct.'); - } - - if (errorEvent.message !== 'Test error') { - throw new Error('Error message not correct.'); - } -} - -async function main(): Promise { - try { - await itObservesUnhandledFetchRejections(); - await itObservesUnhandledJavaScriptFetchRejections(); - await itObservesUncaughtExceptions(); - } catch (error) { - // eslint-disable-next-line no-console - console.error(error); - process.exit(1); - } -} - -main(); diff --git a/packages/uncaught-exception-observer/test/tsconfig.json b/packages/uncaught-exception-observer/test/tsconfig.json deleted file mode 100644 index a42397ae1..000000000 --- a/packages/uncaught-exception-observer/test/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "../tsconfig.json", - "compilerOptions": { - "outDir": "../tmp", - "rootDir": "../test" - }, - "include": [ - "@types/node", - ".", - "../lib" - ] -} \ No newline at end of file diff --git a/packages/uncaught-exception-observer/tsconfig.json b/packages/uncaught-exception-observer/tsconfig.json deleted file mode 100644 index 25f063331..000000000 --- a/packages/uncaught-exception-observer/tsconfig.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "compilerOptions": { - "outDir": "lib", - "rootDir": "src", - "target": "ES2020", - "declaration": true, - "declarationMap": true, - "module": "Node16", - "moduleResolution": "Node16", - "esModuleInterop": true, - "experimentalDecorators": true, - "allowSyntheticDefaultImports": true, - "resolveJsonModule": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "removeComments": false, - "preserveConstEnums": true, - "sourceMap": true, - "skipLibCheck": true, - "baseUrl": ".", - "composite": false, - "incremental": false, - "lib": [ - "es2020" - ], - "types": [ - "node" - ] - }, - "include": [ - "@types/node", - "src" - ], - "exclude": [ - "@types/dom" - ] -} diff --git a/packages/uncaught-exception-observer/vitest.config.ts b/packages/uncaught-exception-observer/vitest.config.ts deleted file mode 100644 index 9ace40c13..000000000 --- a/packages/uncaught-exception-observer/vitest.config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { defineConfig } from 'vitest/config'; - -export default defineConfig({ - test: { - environment: 'node', - include: ['./test/**/*.test.ts'] - } -});