Skip to content

Commit

Permalink
fix: [#1442] It is common do make dynamic imports in connectedCallbac…
Browse files Browse the repository at this point in the history
…k() 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().
  • Loading branch information
capricorn86 committed May 27, 2024
1 parent f0d6091 commit 818d13f
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 6 deletions.
18 changes: 17 additions & 1 deletion packages/happy-dom/src/nodes/html-element/HTMLElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,23 @@ export default class HTMLElement extends Element {
}

if (newElement[PropertySymbol.isConnected] && newElement.connectedCallback) {
newElement.connectedCallback();
const result = <void | Promise<void>>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);
Expand Down
18 changes: 17 additions & 1 deletion packages/happy-dom/src/nodes/node/Node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -551,7 +551,23 @@ export default class Node extends EventTarget {
}

if (isConnected && this.connectedCallback) {
this.connectedCallback();
const result = <void | Promise<void>>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();
}
Expand Down
7 changes: 6 additions & 1 deletion packages/happy-dom/src/window/BrowserWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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;
Expand All @@ -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);
Expand All @@ -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]();

Expand Down Expand Up @@ -1302,6 +1306,7 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal
}

(<boolean>this.closed) = true;
this[PropertySymbol.asyncTaskManager] = null;
this.Audio[PropertySymbol.ownerDocument] = null;
this.Image[PropertySymbol.ownerDocument] = null;
this.DocumentFragment[PropertySymbol.ownerDocument] = null;
Expand Down
52 changes: 49 additions & 3 deletions packages/happy-dom/test/window/DetachedWindowAPI.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,13 @@ describe('DetachedWindowAPI', () => {
});
window.clearInterval(intervalID);
window.setTimeout(() => {
tasksDone++;
window.setTimeout(() => {
window.setTimeout(() => {
window.setTimeout(() => {
tasksDone++;
});
});
});
});
window.setTimeout(() => {
tasksDone++;
Expand All @@ -100,16 +106,56 @@ 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) => {
response.text().then(() => {
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<void> {
await new Promise((resolve) => setTimeout(resolve, 200));
tasksDone++;
}
}
/** */
class CustomElement2 extends window.HTMLElement {
/** */
public async connectedCallback(): Promise<void> {
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);
});
});
Expand Down

0 comments on commit 818d13f

Please sign in to comment.