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);
});
});