diff --git a/packages/happy-dom/src/nodes/html-element/HTMLElement.ts b/packages/happy-dom/src/nodes/html-element/HTMLElement.ts index 6022e22a9..feaa085a2 100644 --- a/packages/happy-dom/src/nodes/html-element/HTMLElement.ts +++ b/packages/happy-dom/src/nodes/html-element/HTMLElement.ts @@ -599,7 +599,23 @@ export default class HTMLElement extends Element { } if (newElement[PropertySymbol.isConnected] && newElement.connectedCallback) { - newElement.connectedCallback(); + const result = >newElement.connectedCallback(); + /** + * It is common to import dependencies in the connectedCallback() method of web components. + * As Happy DOM doesn't have support for dynamic imports yet, this is a temporary solution to wait for imports in connectedCallback(). + * + * @see https://github.com/capricorn86/happy-dom/issues/1442 + */ + if (result instanceof Promise) { + const asyncTaskManager = + this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow][ + PropertySymbol.asyncTaskManager + ]; + const taskID = asyncTaskManager.startTask(); + result + .then(() => asyncTaskManager.endTask(taskID)) + .catch(() => asyncTaskManager.endTask(taskID)); + } } this[PropertySymbol.connectToNode](null); diff --git a/packages/happy-dom/src/nodes/node/Node.ts b/packages/happy-dom/src/nodes/node/Node.ts index 98094c4b9..2321df6e1 100644 --- a/packages/happy-dom/src/nodes/node/Node.ts +++ b/packages/happy-dom/src/nodes/node/Node.ts @@ -551,7 +551,23 @@ export default class Node extends EventTarget { } if (isConnected && this.connectedCallback) { - this.connectedCallback(); + const result = >this.connectedCallback(); + /** + * It is common to import dependencies in the connectedCallback() method of web components. + * As Happy DOM doesn't have support for dynamic imports yet, this is a temporary solution to wait for imports in connectedCallback(). + * + * @see https://github.com/capricorn86/happy-dom/issues/1442 + */ + if (result instanceof Promise) { + const asyncTaskManager = + this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow][ + PropertySymbol.asyncTaskManager + ]; + const taskID = asyncTaskManager.startTask(); + result + .then(() => asyncTaskManager.endTask(taskID)) + .catch(() => asyncTaskManager.endTask(taskID)); + } } else if (!isConnected && this.disconnectedCallback) { this.disconnectedCallback(); } diff --git a/packages/happy-dom/src/window/BrowserWindow.ts b/packages/happy-dom/src/window/BrowserWindow.ts index 6213c76d9..8dc923d63 100644 --- a/packages/happy-dom/src/window/BrowserWindow.ts +++ b/packages/happy-dom/src/window/BrowserWindow.ts @@ -153,6 +153,7 @@ import RangeImplementation from '../range/Range.js'; import INodeJSGlobal from './INodeJSGlobal.js'; import CrossOriginBrowserWindow from './CrossOriginBrowserWindow.js'; import Response from '../fetch/Response.js'; +import AsyncTaskManager from '../async-task-manager/AsyncTaskManager.js'; const TIMER = { setTimeout: globalThis.setTimeout.bind(globalThis), @@ -495,6 +496,7 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal public [PropertySymbol.captureEventListenerCount]: { [eventType: string]: number } = {}; public readonly [PropertySymbol.mutationObservers]: MutationObserver[] = []; public readonly [PropertySymbol.readyStateManager] = new DocumentReadyStateManager(this); + public [PropertySymbol.asyncTaskManager]: AsyncTaskManager | null = null; public [PropertySymbol.location]: Location; public [PropertySymbol.history]: History; public [PropertySymbol.navigator]: Navigator; @@ -520,6 +522,8 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal constructor(browserFrame: IBrowserFrame, options?: { url?: string }) { super(); + const asyncTaskManager = browserFrame[PropertySymbol.asyncTaskManager]; + this.#browserFrame = browserFrame; this.customElements = new CustomElementRegistry(this); @@ -529,13 +533,13 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal this[PropertySymbol.sessionStorage] = StorageFactory.createStorage(); this[PropertySymbol.localStorage] = StorageFactory.createStorage(); this[PropertySymbol.location] = new Location(this.#browserFrame, options?.url ?? 'about:blank'); + this[PropertySymbol.asyncTaskManager] = asyncTaskManager; this.console = browserFrame.page.console; WindowBrowserSettingsReader.setSettings(this, this.#browserFrame.page.context.browser.settings); const window = this; - const asyncTaskManager = this.#browserFrame[PropertySymbol.asyncTaskManager]; this[PropertySymbol.setupVMContext](); @@ -1302,6 +1306,7 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal } (this.closed) = true; + this[PropertySymbol.asyncTaskManager] = null; this.Audio[PropertySymbol.ownerDocument] = null; this.Image[PropertySymbol.ownerDocument] = null; this.DocumentFragment[PropertySymbol.ownerDocument] = null; diff --git a/packages/happy-dom/test/window/DetachedWindowAPI.test.ts b/packages/happy-dom/test/window/DetachedWindowAPI.test.ts index e2e67de8a..c4737abde 100644 --- a/packages/happy-dom/test/window/DetachedWindowAPI.test.ts +++ b/packages/happy-dom/test/window/DetachedWindowAPI.test.ts @@ -87,7 +87,13 @@ describe('DetachedWindowAPI', () => { }); window.clearInterval(intervalID); window.setTimeout(() => { - tasksDone++; + window.setTimeout(() => { + window.setTimeout(() => { + window.setTimeout(() => { + tasksDone++; + }); + }); + }); }); window.setTimeout(() => { tasksDone++; @@ -100,7 +106,19 @@ describe('DetachedWindowAPI', () => { }); window.fetch('/url/1/').then((response) => { response.json().then(() => { - tasksDone++; + window.fetch('/url/1/').then((response) => { + response.json().then(() => { + window.fetch('/url/1/').then((response) => { + response.json().then(() => { + window.fetch('/url/1/').then((response) => { + response.json().then(() => { + tasksDone++; + }); + }); + }); + }); + }); + }); }); }); window.fetch('/url/2/').then((response) => { @@ -108,8 +126,36 @@ describe('DetachedWindowAPI', () => { tasksDone++; }); }); + + /** + * It is common to import dependencies in the connectedCallback() method of web components. + * As Happy DOM doesn't have support for dynamic imports yet, this is a temporary solution to wait for imports in connectedCallback(). + * + * @see https://github.com/capricorn86/happy-dom/issues/1442 + */ + class CustomElement extends window.HTMLElement { + /** */ + public async connectedCallback(): Promise { + await new Promise((resolve) => setTimeout(resolve, 200)); + tasksDone++; + } + } + /** */ + class CustomElement2 extends window.HTMLElement { + /** */ + public async connectedCallback(): Promise { + await new Promise((resolve) => setTimeout(resolve, 100)); + tasksDone++; + } + } + + window.customElements.define('custom-element', CustomElement); + window.document.body.appendChild(new CustomElement()); + window.document.body.appendChild(window.document.createElement('custom-element-2')); + window.customElements.define('custom-element-2', CustomElement2); + await window.happyDOM?.waitUntilComplete(); - expect(tasksDone).toBe(6); + expect(tasksDone).toBe(8); expect(isFirstWhenAsyncCompleteCalled).toBe(true); }); });