Skip to content

Commit 2cc0443

Browse files
fix: [#1892] Clears event listeners on all Nodes when a window to prevent memory leaks (#1901)
* fix: [#1892] Clear event listeners that caused memory leaks * chore: [#1892] Adds unit tests and some improvements --------- Co-authored-by: David Ortner <[email protected]>
1 parent 70dde49 commit 2cc0443

File tree

7 files changed

+129
-10
lines changed

7 files changed

+129
-10
lines changed

packages/happy-dom/src/browser/utilities/BrowserFrameFactory.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@ export default class BrowserFrameFactory {
6161
frame[PropertySymbol.openerFrame] = null;
6262
frame[PropertySymbol.openerWindow] = null;
6363

64+
// Clear navigation listeners
65+
if (frame[PropertySymbol.listeners]) {
66+
frame[PropertySymbol.listeners].navigation = [];
67+
}
68+
6469
resolve();
6570
})
6671
.catch((error) => reject(error));
@@ -83,6 +88,11 @@ export default class BrowserFrameFactory {
8388
frame[PropertySymbol.openerFrame] = null;
8489
frame[PropertySymbol.openerWindow] = null;
8590

91+
// Clear navigation listeners
92+
if (frame[PropertySymbol.listeners]) {
93+
frame[PropertySymbol.listeners].navigation = [];
94+
}
95+
8696
resolve();
8797
})
8898
.catch((error) => reject(error));

packages/happy-dom/src/event/EventTarget.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,16 @@ export default class EventTarget {
166166
this.removeEventListener(type.replace('on', ''), listener);
167167
}
168168

169+
/**
170+
* Destroys the node.
171+
*/
172+
public [PropertySymbol.destroy](): void {
173+
this[PropertySymbol.listeners].capturing.clear();
174+
this[PropertySymbol.listeners].bubbling.clear();
175+
this[PropertySymbol.listenerOptions].capturing.clear();
176+
this[PropertySymbol.listenerOptions].bubbling.clear();
177+
}
178+
169179
/**
170180
* Goes through dispatch event phases.
171181
*

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1065,6 +1065,41 @@ export default class Node extends EventTarget {
10651065
}
10661066
}
10671067

1068+
/**
1069+
* Destroys the node.
1070+
*/
1071+
public [PropertySymbol.destroy](): void {
1072+
super[PropertySymbol.destroy]();
1073+
1074+
this[PropertySymbol.isConnected] = false;
1075+
1076+
while (this[PropertySymbol.nodeArray].length > 0) {
1077+
const node = this[PropertySymbol.nodeArray][this[PropertySymbol.nodeArray].length - 1];
1078+
1079+
// Makes sure that something won't be triggered by the disconnect.
1080+
if ((<any>node).disconnectedCallback) {
1081+
delete (<any>node).disconnectedCallback;
1082+
}
1083+
1084+
this[PropertySymbol.removeChild](node);
1085+
node[PropertySymbol.destroy]();
1086+
}
1087+
1088+
this[PropertySymbol.parentNode] = null;
1089+
this[PropertySymbol.rootNode] = null;
1090+
this[PropertySymbol.styleNode] = null;
1091+
this[PropertySymbol.textAreaNode] = null;
1092+
this[PropertySymbol.formNode] = null;
1093+
this[PropertySymbol.selectNode] = null;
1094+
this[PropertySymbol.mutationListeners] = [];
1095+
this[PropertySymbol.nodeArray] = [];
1096+
this[PropertySymbol.elementArray] = [];
1097+
this[PropertySymbol.childNodes] = null;
1098+
this[PropertySymbol.assignedToSlot] = null;
1099+
1100+
this[PropertySymbol.clearCache]();
1101+
}
1102+
10681103
/**
10691104
* Reports the position of its argument node relative to the node on which it is called.
10701105
*

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

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1884,6 +1884,8 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal
18841884
return;
18851885
}
18861886

1887+
super[PropertySymbol.destroy]();
1888+
18871889
(<boolean>this.closed) = true;
18881890

18891891
const mutationObservers = this[PropertySymbol.mutationObservers];
@@ -1896,16 +1898,7 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal
18961898

18971899
this[PropertySymbol.mutationObservers] = [];
18981900

1899-
// Disconnects nodes from the document, so that they can be garbage collected.
1900-
const childNodes = this.document[PropertySymbol.nodeArray];
1901-
1902-
while (childNodes.length > 0) {
1903-
// Makes sure that something won't be triggered by the disconnect.
1904-
if ((<HTMLElement>childNodes[0]).disconnectedCallback) {
1905-
delete (<HTMLElement>childNodes[0]).disconnectedCallback;
1906-
}
1907-
this.document[PropertySymbol.removeChild](childNodes[0]);
1908-
}
1901+
this.document[PropertySymbol.destroy]();
19091902

19101903
// Create some empty elements for scripts that are still running.
19111904
const htmlElement = this.document.createElement('html');
@@ -1940,6 +1933,10 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal
19401933
this.document[PropertySymbol.currentScript] = null;
19411934
this.document[PropertySymbol.selection] = null;
19421935

1936+
// Clear parent/top references to break circular references
1937+
this[PropertySymbol.parent] = null;
1938+
this[PropertySymbol.top] = null;
1939+
19431940
WindowBrowserContext.removeWindowBrowserFrameRelation(this);
19441941
}
19451942

packages/happy-dom/test/browser/BrowserPage.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,48 @@ describe('BrowserPage', () => {
242242
expect(mainFrameWindow[PropertySymbol.modules].css.size).toBe(0);
243243
expect(mainFrameWindow[PropertySymbol.modules].json.size).toBe(0);
244244
});
245+
246+
it('Clears event listeners of nodes when closing.', async () => {
247+
const browser = new Browser({ console });
248+
const page = browser.defaultContext.newPage();
249+
const mainFrame = page.mainFrame;
250+
const frame1 = BrowserFrameFactory.createChildFrame(page.mainFrame);
251+
const frame2 = BrowserFrameFactory.createChildFrame(page.mainFrame);
252+
253+
const div1 = mainFrame.document.createElement('div');
254+
const div2 = frame1.document.createElement('div');
255+
const div3 = frame2.document.createElement('div');
256+
let mainFrameDivClicked = false;
257+
let frame1DivClicked = false;
258+
let frame2DivClicked = false;
259+
260+
div1.addEventListener('click', () => {
261+
mainFrameDivClicked = true;
262+
});
263+
264+
div2.addEventListener('click', () => {
265+
frame1DivClicked = true;
266+
});
267+
268+
div3.addEventListener('click', () => {
269+
frame2DivClicked = true;
270+
});
271+
272+
mainFrame.document.body.appendChild(div1);
273+
frame1.document.body.appendChild(div2);
274+
frame2.document.body.appendChild(div3);
275+
276+
await page.close();
277+
278+
// Simulate clicks after page is closed
279+
div1.dispatchEvent(new Event('click'));
280+
div2.dispatchEvent(new Event('click'));
281+
div3.dispatchEvent(new Event('click'));
282+
283+
expect(mainFrameDivClicked).toBe(false);
284+
expect(frame1DivClicked).toBe(false);
285+
expect(frame2DivClicked).toBe(false);
286+
});
245287
});
246288

247289
describe('waitUntilComplete()', () => {

packages/happy-dom/test/event/EventTarget.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,4 +264,28 @@ describe('EventTarget', () => {
264264
expect(eventTarget[Symbol.toStringTag]).toBe(description);
265265
});
266266
});
267+
268+
describe('[PropertySymbol.destroy]', () => {
269+
it('Destroys the event target', () => {
270+
let count = 0;
271+
const listener = (): void => {
272+
count++;
273+
};
274+
const dispatchedEvent = new Event(EVENT_TYPE);
275+
276+
eventTarget.addEventListener(EVENT_TYPE, listener);
277+
278+
eventTarget.dispatchEvent(dispatchedEvent);
279+
280+
expect(count).toBe(1);
281+
282+
count = 0;
283+
284+
eventTarget[PropertySymbol.destroy]();
285+
286+
eventTarget.dispatchEvent(dispatchedEvent);
287+
288+
expect(count).toBe(0);
289+
});
290+
});
267291
});

packages/happy-dom/test/mutation-observer/MutationObserver.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,7 @@ describe('MutationObserver', () => {
385385
]);
386386

387387
text.textContent = 'new3';
388+
div.appendChild(span);
388389
div.removeChild(span);
389390
div.setAttribute('attr', 'value3');
390391

0 commit comments

Comments
 (0)