Skip to content

Commit 818d13f

Browse files
committed
fix: [#1442] It is common do make dynamic imports in connectedCallback() of a web component. As Happy DOM doesn't have support for dynamic imports using waitUntilComplete(), a temporary fix has been added to hook into promises returned by connectedCallback().
1 parent f0d6091 commit 818d13f

File tree

4 files changed

+89
-6
lines changed

4 files changed

+89
-6
lines changed

packages/happy-dom/src/nodes/html-element/HTMLElement.ts

+17-1
Original file line numberDiff line numberDiff line change
@@ -599,7 +599,23 @@ export default class HTMLElement extends Element {
599599
}
600600

601601
if (newElement[PropertySymbol.isConnected] && newElement.connectedCallback) {
602-
newElement.connectedCallback();
602+
const result = <void | Promise<void>>newElement.connectedCallback();
603+
/**
604+
* It is common to import dependencies in the connectedCallback() method of web components.
605+
* As Happy DOM doesn't have support for dynamic imports yet, this is a temporary solution to wait for imports in connectedCallback().
606+
*
607+
* @see https://github.com/capricorn86/happy-dom/issues/1442
608+
*/
609+
if (result instanceof Promise) {
610+
const asyncTaskManager =
611+
this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow][
612+
PropertySymbol.asyncTaskManager
613+
];
614+
const taskID = asyncTaskManager.startTask();
615+
result
616+
.then(() => asyncTaskManager.endTask(taskID))
617+
.catch(() => asyncTaskManager.endTask(taskID));
618+
}
603619
}
604620

605621
this[PropertySymbol.connectToNode](null);

packages/happy-dom/src/nodes/node/Node.ts

+17-1
Original file line numberDiff line numberDiff line change
@@ -551,7 +551,23 @@ export default class Node extends EventTarget {
551551
}
552552

553553
if (isConnected && this.connectedCallback) {
554-
this.connectedCallback();
554+
const result = <void | Promise<void>>this.connectedCallback();
555+
/**
556+
* It is common to import dependencies in the connectedCallback() method of web components.
557+
* As Happy DOM doesn't have support for dynamic imports yet, this is a temporary solution to wait for imports in connectedCallback().
558+
*
559+
* @see https://github.com/capricorn86/happy-dom/issues/1442
560+
*/
561+
if (result instanceof Promise) {
562+
const asyncTaskManager =
563+
this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow][
564+
PropertySymbol.asyncTaskManager
565+
];
566+
const taskID = asyncTaskManager.startTask();
567+
result
568+
.then(() => asyncTaskManager.endTask(taskID))
569+
.catch(() => asyncTaskManager.endTask(taskID));
570+
}
555571
} else if (!isConnected && this.disconnectedCallback) {
556572
this.disconnectedCallback();
557573
}

packages/happy-dom/src/window/BrowserWindow.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ import RangeImplementation from '../range/Range.js';
153153
import INodeJSGlobal from './INodeJSGlobal.js';
154154
import CrossOriginBrowserWindow from './CrossOriginBrowserWindow.js';
155155
import Response from '../fetch/Response.js';
156+
import AsyncTaskManager from '../async-task-manager/AsyncTaskManager.js';
156157

157158
const TIMER = {
158159
setTimeout: globalThis.setTimeout.bind(globalThis),
@@ -495,6 +496,7 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal
495496
public [PropertySymbol.captureEventListenerCount]: { [eventType: string]: number } = {};
496497
public readonly [PropertySymbol.mutationObservers]: MutationObserver[] = [];
497498
public readonly [PropertySymbol.readyStateManager] = new DocumentReadyStateManager(this);
499+
public [PropertySymbol.asyncTaskManager]: AsyncTaskManager | null = null;
498500
public [PropertySymbol.location]: Location;
499501
public [PropertySymbol.history]: History;
500502
public [PropertySymbol.navigator]: Navigator;
@@ -520,6 +522,8 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal
520522
constructor(browserFrame: IBrowserFrame, options?: { url?: string }) {
521523
super();
522524

525+
const asyncTaskManager = browserFrame[PropertySymbol.asyncTaskManager];
526+
523527
this.#browserFrame = browserFrame;
524528

525529
this.customElements = new CustomElementRegistry(this);
@@ -529,13 +533,13 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal
529533
this[PropertySymbol.sessionStorage] = StorageFactory.createStorage();
530534
this[PropertySymbol.localStorage] = StorageFactory.createStorage();
531535
this[PropertySymbol.location] = new Location(this.#browserFrame, options?.url ?? 'about:blank');
536+
this[PropertySymbol.asyncTaskManager] = asyncTaskManager;
532537

533538
this.console = browserFrame.page.console;
534539

535540
WindowBrowserSettingsReader.setSettings(this, this.#browserFrame.page.context.browser.settings);
536541

537542
const window = this;
538-
const asyncTaskManager = this.#browserFrame[PropertySymbol.asyncTaskManager];
539543

540544
this[PropertySymbol.setupVMContext]();
541545

@@ -1302,6 +1306,7 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal
13021306
}
13031307

13041308
(<boolean>this.closed) = true;
1309+
this[PropertySymbol.asyncTaskManager] = null;
13051310
this.Audio[PropertySymbol.ownerDocument] = null;
13061311
this.Image[PropertySymbol.ownerDocument] = null;
13071312
this.DocumentFragment[PropertySymbol.ownerDocument] = null;

packages/happy-dom/test/window/DetachedWindowAPI.test.ts

+49-3
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,13 @@ describe('DetachedWindowAPI', () => {
8787
});
8888
window.clearInterval(intervalID);
8989
window.setTimeout(() => {
90-
tasksDone++;
90+
window.setTimeout(() => {
91+
window.setTimeout(() => {
92+
window.setTimeout(() => {
93+
tasksDone++;
94+
});
95+
});
96+
});
9197
});
9298
window.setTimeout(() => {
9399
tasksDone++;
@@ -100,16 +106,56 @@ describe('DetachedWindowAPI', () => {
100106
});
101107
window.fetch('/url/1/').then((response) => {
102108
response.json().then(() => {
103-
tasksDone++;
109+
window.fetch('/url/1/').then((response) => {
110+
response.json().then(() => {
111+
window.fetch('/url/1/').then((response) => {
112+
response.json().then(() => {
113+
window.fetch('/url/1/').then((response) => {
114+
response.json().then(() => {
115+
tasksDone++;
116+
});
117+
});
118+
});
119+
});
120+
});
121+
});
104122
});
105123
});
106124
window.fetch('/url/2/').then((response) => {
107125
response.text().then(() => {
108126
tasksDone++;
109127
});
110128
});
129+
130+
/**
131+
* It is common to import dependencies in the connectedCallback() method of web components.
132+
* As Happy DOM doesn't have support for dynamic imports yet, this is a temporary solution to wait for imports in connectedCallback().
133+
*
134+
* @see https://github.com/capricorn86/happy-dom/issues/1442
135+
*/
136+
class CustomElement extends window.HTMLElement {
137+
/** */
138+
public async connectedCallback(): Promise<void> {
139+
await new Promise((resolve) => setTimeout(resolve, 200));
140+
tasksDone++;
141+
}
142+
}
143+
/** */
144+
class CustomElement2 extends window.HTMLElement {
145+
/** */
146+
public async connectedCallback(): Promise<void> {
147+
await new Promise((resolve) => setTimeout(resolve, 100));
148+
tasksDone++;
149+
}
150+
}
151+
152+
window.customElements.define('custom-element', CustomElement);
153+
window.document.body.appendChild(new CustomElement());
154+
window.document.body.appendChild(window.document.createElement('custom-element-2'));
155+
window.customElements.define('custom-element-2', CustomElement2);
156+
111157
await window.happyDOM?.waitUntilComplete();
112-
expect(tasksDone).toBe(6);
158+
expect(tasksDone).toBe(8);
113159
expect(isFirstWhenAsyncCompleteCalled).toBe(true);
114160
});
115161
});

0 commit comments

Comments
 (0)