diff --git a/packages/happy-dom/src/async-task-manager/AsyncTaskManager.ts b/packages/happy-dom/src/async-task-manager/AsyncTaskManager.ts index e14997256..faa54253a 100644 --- a/packages/happy-dom/src/async-task-manager/AsyncTaskManager.ts +++ b/packages/happy-dom/src/async-task-manager/AsyncTaskManager.ts @@ -49,6 +49,10 @@ export default class AsyncTaskManager { * @param timerID Timer ID. */ public startTimer(timerID: NodeJS.Timeout): void { + if (this.waitUntilCompleteTimer) { + TIMER.clearTimeout(this.waitUntilCompleteTimer); + this.waitUntilCompleteTimer = null; + } this.runningTimers.push(timerID); } @@ -58,12 +62,16 @@ export default class AsyncTaskManager { * @param timerID Timer ID. */ public endTimer(timerID: NodeJS.Timeout): void { + if (this.waitUntilCompleteTimer) { + TIMER.clearTimeout(this.waitUntilCompleteTimer); + this.waitUntilCompleteTimer = null; + } const index = this.runningTimers.indexOf(timerID); if (index !== -1) { this.runningTimers.splice(index, 1); - if (!this.runningTaskCount && !this.runningTimers.length && !this.runningImmediates.length) { - this.resolveWhenComplete(); - } + } + if (!this.runningTaskCount && !this.runningTimers.length && !this.runningImmediates.length) { + this.resolveWhenComplete(); } } @@ -73,6 +81,10 @@ export default class AsyncTaskManager { * @param immediateID Immediate ID. */ public startImmediate(immediateID: NodeJS.Immediate): void { + if (this.waitUntilCompleteTimer) { + TIMER.clearTimeout(this.waitUntilCompleteTimer); + this.waitUntilCompleteTimer = null; + } this.runningImmediates.push(immediateID); } @@ -82,12 +94,16 @@ export default class AsyncTaskManager { * @param immediateID Immediate ID. */ public endImmediate(immediateID: NodeJS.Immediate): void { + if (this.waitUntilCompleteTimer) { + TIMER.clearTimeout(this.waitUntilCompleteTimer); + this.waitUntilCompleteTimer = null; + } const index = this.runningImmediates.indexOf(immediateID); if (index !== -1) { this.runningImmediates.splice(index, 1); - if (!this.runningTaskCount && !this.runningTimers.length && !this.runningImmediates.length) { - this.resolveWhenComplete(); - } + } + if (!this.runningTaskCount && !this.runningTimers.length && !this.runningImmediates.length) { + this.resolveWhenComplete(); } } @@ -98,6 +114,10 @@ export default class AsyncTaskManager { * @returns Task ID. */ public startTask(abortHandler?: () => void): number { + if (this.waitUntilCompleteTimer) { + TIMER.clearTimeout(this.waitUntilCompleteTimer); + this.waitUntilCompleteTimer = null; + } const taskID = this.newTaskID(); this.runningTasks[taskID] = abortHandler ? abortHandler : () => {}; this.runningTaskCount++; @@ -110,27 +130,16 @@ export default class AsyncTaskManager { * @param taskID Task ID. */ public endTask(taskID: number): void { + if (this.waitUntilCompleteTimer) { + TIMER.clearTimeout(this.waitUntilCompleteTimer); + this.waitUntilCompleteTimer = null; + } if (this.runningTasks[taskID]) { delete this.runningTasks[taskID]; this.runningTaskCount--; - if (this.waitUntilCompleteTimer) { - TIMER.clearTimeout(this.waitUntilCompleteTimer); - } - if (!this.runningTaskCount && !this.runningTimers.length && !this.runningImmediates.length) { - // In some cases, microtasks are used by transformed code and waitUntilComplete() is then resolved too early. - // To cater for this we use setTimeout() which has the lowest priority and will be executed last. - // "10ms" is an arbitrary value, but it seem to be enough when performing many manual tests. - this.waitUntilCompleteTimer = TIMER.setTimeout(() => { - this.waitUntilCompleteTimer = null; - if ( - !this.runningTaskCount && - !this.runningTimers.length && - !this.runningImmediates.length - ) { - this.resolveWhenComplete(); - } - }, 10); - } + } + if (!this.runningTaskCount && !this.runningTimers.length && !this.runningImmediates.length) { + this.resolveWhenComplete(); } } @@ -157,11 +166,28 @@ export default class AsyncTaskManager { * Resolves when complete. */ private resolveWhenComplete(): void { - const resolvers = this.waitUntilCompleteResolvers; - this.waitUntilCompleteResolvers = []; - for (const resolver of resolvers) { - resolver(); + if (this.runningTaskCount || this.runningTimers.length || this.runningImmediates.length) { + return; + } + + if (this.waitUntilCompleteTimer) { + TIMER.clearTimeout(this.waitUntilCompleteTimer); + this.waitUntilCompleteTimer = null; } + + // In some cases, microtasks are used by transformed code and waitUntilComplete() is then resolved too early. + // To cater for this we use setTimeout() which has the lowest priority and will be executed last. + // "10ms" is an arbitrary value, but it seem to be enough when performing many manual tests. + this.waitUntilCompleteTimer = TIMER.setTimeout(() => { + this.waitUntilCompleteTimer = null; + if (!this.runningTaskCount && !this.runningTimers.length && !this.runningImmediates.length) { + const resolvers = this.waitUntilCompleteResolvers; + this.waitUntilCompleteResolvers = []; + for (const resolver of resolvers) { + resolver(); + } + } + }, 10); } /** diff --git a/packages/happy-dom/src/browser/utilities/BrowserFrameFactory.ts b/packages/happy-dom/src/browser/utilities/BrowserFrameFactory.ts index 2fb4bd18e..7400b8ee9 100644 --- a/packages/happy-dom/src/browser/utilities/BrowserFrameFactory.ts +++ b/packages/happy-dom/src/browser/utilities/BrowserFrameFactory.ts @@ -42,6 +42,12 @@ export default class BrowserFrameFactory { } if (!frame.childFrames.length) { + if (frame.window && frame.window[PropertySymbol.mutationObservers]) { + for (const mutationObserver of frame.window[PropertySymbol.mutationObservers]) { + mutationObserver.disconnect(); + } + frame.window[PropertySymbol.mutationObservers] = []; + } return frame[PropertySymbol.asyncTaskManager] .destroy() .then(() => { @@ -60,6 +66,12 @@ export default class BrowserFrameFactory { Promise.all(frame.childFrames.slice().map((childFrame) => this.destroyFrame(childFrame))) .then(() => { + if (frame.window && frame.window[PropertySymbol.mutationObservers]) { + for (const mutationObserver of frame.window[PropertySymbol.mutationObservers]) { + mutationObserver.disconnect(); + } + frame.window[PropertySymbol.mutationObservers] = []; + } return frame[PropertySymbol.asyncTaskManager].destroy().then(() => { frame[PropertySymbol.exceptionObserver]?.disconnect(); if (frame.window) { diff --git a/packages/happy-dom/src/window/BrowserWindow.ts b/packages/happy-dom/src/window/BrowserWindow.ts index 8dc923d63..50bdb0032 100644 --- a/packages/happy-dom/src/window/BrowserWindow.ts +++ b/packages/happy-dom/src/window/BrowserWindow.ts @@ -494,7 +494,7 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal // Used for tracking capture event listeners to improve performance when they are not used. // See EventTarget class. public [PropertySymbol.captureEventListenerCount]: { [eventType: string]: number } = {}; - public readonly [PropertySymbol.mutationObservers]: MutationObserver[] = []; + public [PropertySymbol.mutationObservers]: MutationObserver[] = []; public readonly [PropertySymbol.readyStateManager] = new DocumentReadyStateManager(this); public [PropertySymbol.asyncTaskManager]: AsyncTaskManager | null = null; public [PropertySymbol.location]: Location; @@ -1319,6 +1319,8 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal mutationObserver.disconnect(); } + this[PropertySymbol.mutationObservers] = []; + // Disconnects nodes from the document, so that they can be garbage collected. for (const node of this.document[PropertySymbol.childNodes].slice()) { // Makes sure that something won't be triggered by the disconnect. diff --git a/packages/happy-dom/test/nodes/document/Document.test.ts b/packages/happy-dom/test/nodes/document/Document.test.ts index 97c67d4cc..0a726b051 100644 --- a/packages/happy-dom/test/nodes/document/Document.test.ts +++ b/packages/happy-dom/test/nodes/document/Document.test.ts @@ -1255,7 +1255,7 @@ describe('Document', () => { expect((readyChangeEvent).target).toBe(document); expect(document.readyState).toBe(DocumentReadyStateEnum.complete); resolve(null); - }, 1); + }, 20); }); });