From f3d4e58b7045bd38786ed4cd1d3d569d7e3522cd Mon Sep 17 00:00:00 2001
From: David Ortner <david@ortner.se>
Date: Wed, 8 Jan 2025 02:42:47 +0100
Subject: [PATCH] fix: [#1577] Event.target should be the target element after
 dispatching event

---
 packages/happy-dom/src/PropertySymbol.ts             |  1 +
 packages/happy-dom/src/event/Event.ts                |  1 +
 packages/happy-dom/src/event/EventTarget.ts          | 12 ++++++++----
 packages/happy-dom/src/window/BrowserWindow.ts       |  4 ++--
 packages/happy-dom/test/event/Event.test.ts          |  2 +-
 packages/happy-dom/test/event/EventTarget.test.ts    | 10 ++++++++++
 .../happy-dom/test/nodes/document/Document.test.ts   |  4 ++--
 .../test/nodes/html-element/HTMLElement.test.ts      |  2 +-
 .../nodes/html-element/HTMLElementUtility.test.ts    | 10 +++++-----
 .../nodes/html-link-element/HTMLLinkElement.test.ts  |  4 ++--
 .../html-script-element/HTMLScriptElement.test.ts    |  4 ++--
 packages/happy-dom/test/nodes/node/Node.test.ts      |  4 ++--
 packages/happy-dom/test/window/BrowserWindow.test.ts | 12 ++++++------
 13 files changed, 43 insertions(+), 27 deletions(-)

diff --git a/packages/happy-dom/src/PropertySymbol.ts b/packages/happy-dom/src/PropertySymbol.ts
index 32fbe3e4a..be9bee7b5 100644
--- a/packages/happy-dom/src/PropertySymbol.ts
+++ b/packages/happy-dom/src/PropertySymbol.ts
@@ -378,3 +378,4 @@ export const xmlProcessingInstruction = Symbol('xmlProcessingInstruction');
 export const root = Symbol('root');
 export const filterNode = Symbol('filterNode');
 export const customElementReactionStack = Symbol('customElementReactionStack');
+export const dispatching = Symbol('dispatching');
diff --git a/packages/happy-dom/src/event/Event.ts b/packages/happy-dom/src/event/Event.ts
index ba18a159c..333cfabe4 100644
--- a/packages/happy-dom/src/event/Event.ts
+++ b/packages/happy-dom/src/event/Event.ts
@@ -25,6 +25,7 @@ export default class Event {
 	public [PropertySymbol.timeStamp] = performance.now();
 	public [PropertySymbol.type]: string;
 
+	public [PropertySymbol.dispatching] = false;
 	public [PropertySymbol.immediatePropagationStopped] = false;
 	public [PropertySymbol.propagationStopped] = false;
 	public [PropertySymbol.target]: EventTarget = null;
diff --git a/packages/happy-dom/src/event/EventTarget.ts b/packages/happy-dom/src/event/EventTarget.ts
index 769318036..f93341c20 100644
--- a/packages/happy-dom/src/event/EventTarget.ts
+++ b/packages/happy-dom/src/event/EventTarget.ts
@@ -110,11 +110,18 @@ export default class EventTarget {
 	 * @returns The return value is false if event is cancelable and at least one of the event handlers which handled this event called Event.preventDefault().
 	 */
 	public dispatchEvent(event: Event): boolean {
-		if (!event[PropertySymbol.target]) {
+		// The "load" event is a special case. It should not bubble up to the window from the document.
+		if (
+			!event[PropertySymbol.dispatching] &&
+			(event[PropertySymbol.type] !== 'load' || !event[PropertySymbol.target])
+		) {
+			event[PropertySymbol.dispatching] = true;
 			event[PropertySymbol.target] = this[PropertySymbol.proxy] || this;
 
 			this.#goThroughDispatchEventPhases(event);
 
+			event[PropertySymbol.dispatching] = false;
+
 			return !(event[PropertySymbol.cancelable] && event[PropertySymbol.defaultPrevented]);
 		}
 
@@ -172,7 +179,6 @@ export default class EventTarget {
 				event[PropertySymbol.immediatePropagationStopped]
 			) {
 				event[PropertySymbol.eventPhase] = EventPhaseEnum.none;
-				event[PropertySymbol.target] = null;
 				event[PropertySymbol.currentTarget] = null;
 				return;
 			}
@@ -201,7 +207,6 @@ export default class EventTarget {
 					event[PropertySymbol.immediatePropagationStopped]
 				) {
 					event[PropertySymbol.eventPhase] = EventPhaseEnum.none;
-					event[PropertySymbol.target] = null;
 					event[PropertySymbol.currentTarget] = null;
 					return;
 				}
@@ -210,7 +215,6 @@ export default class EventTarget {
 
 		// None phase (done)
 		event[PropertySymbol.eventPhase] = EventPhaseEnum.none;
-		event[PropertySymbol.target] = null;
 		event[PropertySymbol.currentTarget] = null;
 	}
 
diff --git a/packages/happy-dom/src/window/BrowserWindow.ts b/packages/happy-dom/src/window/BrowserWindow.ts
index a345c7761..e0722fd22 100644
--- a/packages/happy-dom/src/window/BrowserWindow.ts
+++ b/packages/happy-dom/src/window/BrowserWindow.ts
@@ -845,15 +845,15 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal
 			// Not sure why target is set to document here, but this is how it works in the browser
 			const loadEvent = new Event('load');
 
-			loadEvent[PropertySymbol.currentTarget] = this.document;
+			loadEvent[PropertySymbol.currentTarget] = this;
 			loadEvent[PropertySymbol.target] = this.document;
 			loadEvent[PropertySymbol.eventPhase] = EventPhaseEnum.atTarget;
 
 			this.dispatchEvent(loadEvent);
 
-			loadEvent[PropertySymbol.target] = null;
 			loadEvent[PropertySymbol.currentTarget] = null;
 			loadEvent[PropertySymbol.eventPhase] = EventPhaseEnum.none;
+			loadEvent[PropertySymbol.dispatching] = false;
 		});
 
 		this[PropertySymbol.bindMethods]();
diff --git a/packages/happy-dom/test/event/Event.test.ts b/packages/happy-dom/test/event/Event.test.ts
index 00d17a69d..db78d23b6 100644
--- a/packages/happy-dom/test/event/Event.test.ts
+++ b/packages/happy-dom/test/event/Event.test.ts
@@ -36,7 +36,7 @@ describe('Event', () => {
 			});
 			span.dispatchEvent(event);
 
-			expect(event.target).toBe(null);
+			expect(event.target).toBe(span);
 			expect(target).toBe(span);
 		});
 	});
diff --git a/packages/happy-dom/test/event/EventTarget.test.ts b/packages/happy-dom/test/event/EventTarget.test.ts
index 4bceb98ed..a00e93e97 100644
--- a/packages/happy-dom/test/event/EventTarget.test.ts
+++ b/packages/happy-dom/test/event/EventTarget.test.ts
@@ -3,6 +3,7 @@ import Window from '../../src/window/Window.js';
 import EventTarget from '../../src/event/EventTarget.js';
 import Event from '../../src/event/Event.js';
 import CustomEvent from '../../src/event/events/CustomEvent.js';
+import * as PropertySymbol from '../../src/PropertySymbol.js';
 import { beforeEach, describe, it, expect } from 'vitest';
 
 const EVENT_TYPE = 'click';
@@ -160,6 +161,10 @@ describe('EventTarget', () => {
 			expect(recievedEvent).toBe(dispatchedEvent);
 			expect(recievedTarget).toBe(eventTarget);
 			expect(recievedCurrentTarget).toBe(eventTarget);
+			expect(dispatchedEvent.target).toBe(eventTarget);
+			expect(dispatchedEvent.currentTarget).toBe(null);
+			expect(dispatchedEvent.defaultPrevented).toBe(false);
+			expect(dispatchedEvent[PropertySymbol.dispatching]).toBe(false);
 		});
 
 		it('Triggers all listeners, even though listeners are removed while dispatching.', () => {
@@ -196,6 +201,11 @@ describe('EventTarget', () => {
 			expect(recievedCurrentTarget1).toBe(eventTarget);
 			expect(recievedTarget2).toBe(eventTarget);
 			expect(recievedCurrentTarget2).toBe(eventTarget);
+
+			expect(dispatchedEvent.target).toBe(eventTarget);
+			expect(dispatchedEvent.currentTarget).toBe(null);
+			expect(dispatchedEvent.defaultPrevented).toBe(false);
+			expect(dispatchedEvent[PropertySymbol.dispatching]).toBe(false);
 		});
 	});
 
diff --git a/packages/happy-dom/test/nodes/document/Document.test.ts b/packages/happy-dom/test/nodes/document/Document.test.ts
index a5a5c3305..b59c27e35 100644
--- a/packages/happy-dom/test/nodes/document/Document.test.ts
+++ b/packages/happy-dom/test/nodes/document/Document.test.ts
@@ -1397,7 +1397,7 @@ describe('Document', () => {
 				expect(document.readyState).toBe(DocumentReadyStateEnum.interactive);
 
 				setTimeout(() => {
-					expect((<Event>event).target).toBe(null);
+					expect((<Event>event).target).toBe(document);
 					expect(target).toBe(document);
 					expect(currentTarget).toBe(document);
 					expect(document.readyState).toBe(DocumentReadyStateEnum.complete);
@@ -1456,7 +1456,7 @@ describe('Document', () => {
 					expect(resourceFetchCSSURL).toBe(cssURL);
 					expect(resourceFetchJSWindow).toBe(window);
 					expect(resourceFetchJSURL).toBe(jsURL);
-					expect((<Event>event).target).toBe(null);
+					expect((<Event>event).target).toBe(document);
 					expect(target).toBe(document);
 					expect(currentTarget).toBe(document);
 					expect(document.readyState).toBe(DocumentReadyStateEnum.complete);
diff --git a/packages/happy-dom/test/nodes/html-element/HTMLElement.test.ts b/packages/happy-dom/test/nodes/html-element/HTMLElement.test.ts
index dccc01064..9c05718ef 100644
--- a/packages/happy-dom/test/nodes/html-element/HTMLElement.test.ts
+++ b/packages/happy-dom/test/nodes/html-element/HTMLElement.test.ts
@@ -470,7 +470,7 @@ describe('HTMLElement', () => {
 			expect((<PointerEvent>(<unknown>event)).composed).toBe(true);
 			expect((<PointerEvent>(<unknown>event)).width).toBe(1);
 			expect((<PointerEvent>(<unknown>event)).height).toBe(1);
-			expect((<PointerEvent>(<unknown>event)).target).toBe(null);
+			expect((<PointerEvent>(<unknown>event)).target).toBe(element);
 			expect((<PointerEvent>(<unknown>event)).currentTarget).toBe(null);
 			expect(target).toBe(element);
 			expect(currentTarget).toBe(element);
diff --git a/packages/happy-dom/test/nodes/html-element/HTMLElementUtility.test.ts b/packages/happy-dom/test/nodes/html-element/HTMLElementUtility.test.ts
index 7a6849837..99e197407 100644
--- a/packages/happy-dom/test/nodes/html-element/HTMLElementUtility.test.ts
+++ b/packages/happy-dom/test/nodes/html-element/HTMLElementUtility.test.ts
@@ -48,14 +48,14 @@ describe('HTMLElementUtility', () => {
 				expect((<FocusEvent>(<unknown>blurEvent)).type).toBe('blur');
 				expect((<FocusEvent>(<unknown>blurEvent)).bubbles).toBe(false);
 				expect((<FocusEvent>(<unknown>blurEvent)).composed).toBe(true);
-				expect((<FocusEvent>(<unknown>blurEvent)).target).toBe(null);
+				expect((<FocusEvent>(<unknown>blurEvent)).target).toBe(element);
 				expect(blurTarget).toBe(element);
 				expect(blurCurrentTarget).toBe(element);
 
 				expect((<FocusEvent>(<unknown>focusOutEvent)).type).toBe('focusout');
 				expect((<FocusEvent>(<unknown>focusOutEvent)).bubbles).toBe(true);
 				expect((<FocusEvent>(<unknown>focusOutEvent)).composed).toBe(true);
-				expect((<FocusEvent>(<unknown>focusOutEvent)).target).toBe(null);
+				expect((<FocusEvent>(<unknown>focusOutEvent)).target).toBe(element);
 				expect(focusOutTarget).toBe(element);
 				expect(focusOutCurrentTarget).toBe(element);
 
@@ -133,14 +133,14 @@ describe('HTMLElementUtility', () => {
 				expect((<FocusEvent>(<unknown>focusEvent)).type).toBe('focus');
 				expect((<FocusEvent>(<unknown>focusEvent)).bubbles).toBe(false);
 				expect((<FocusEvent>(<unknown>focusEvent)).composed).toBe(true);
-				expect((<FocusEvent>(<unknown>focusEvent)).target).toBe(null);
+				expect((<FocusEvent>(<unknown>focusEvent)).target).toBe(element);
 				expect(focusTarget).toBe(element);
 				expect(focusCurrentTarget).toBe(element);
 
 				expect((<FocusEvent>(<unknown>focusInEvent)).type).toBe('focusin');
 				expect((<FocusEvent>(<unknown>focusInEvent)).bubbles).toBe(true);
 				expect((<FocusEvent>(<unknown>focusInEvent)).composed).toBe(true);
-				expect((<FocusEvent>(<unknown>focusInEvent)).target).toBe(null);
+				expect((<FocusEvent>(<unknown>focusInEvent)).target).toBe(element);
 				expect(focusInTarget).toBe(element);
 				expect(focusInCurrentTarget).toBe(element);
 
@@ -212,7 +212,7 @@ describe('HTMLElementUtility', () => {
 				expect((<FocusEvent>(<unknown>event)).type).toBe('blur');
 				expect((<FocusEvent>(<unknown>event)).bubbles).toBe(false);
 				expect((<FocusEvent>(<unknown>event)).composed).toBe(true);
-				expect((<FocusEvent>(<unknown>event)).target).toBe(null);
+				expect((<FocusEvent>(<unknown>event)).target).toBe(previousElement);
 				expect(target).toBe(previousElement);
 				expect(currentTarget).toBe(previousElement);
 			}
diff --git a/packages/happy-dom/test/nodes/html-link-element/HTMLLinkElement.test.ts b/packages/happy-dom/test/nodes/html-link-element/HTMLLinkElement.test.ts
index b715dd7fe..95477be16 100644
--- a/packages/happy-dom/test/nodes/html-link-element/HTMLLinkElement.test.ts
+++ b/packages/happy-dom/test/nodes/html-link-element/HTMLLinkElement.test.ts
@@ -117,7 +117,7 @@ describe('HTMLLinkElement', () => {
 			expect(loadedURL).toBe('https://localhost:8080/test/path/file.css');
 			expect(element.sheet.cssRules.length).toBe(1);
 			expect(element.sheet.cssRules[0].cssText).toBe('div { background: red; }');
-			expect((<Event>(<unknown>loadEvent)).target).toBe(null);
+			expect((<Event>(<unknown>loadEvent)).target).toBe(element);
 			expect(loadEventTarget).toBe(element);
 			expect(loadEventCurrentTarget).toBe(element);
 		});
@@ -198,7 +198,7 @@ describe('HTMLLinkElement', () => {
 			expect(loadedURL).toBe('https://localhost:8080/test/path/file.css');
 			expect(element.sheet.cssRules.length).toBe(1);
 			expect(element.sheet.cssRules[0].cssText).toBe('div { background: red; }');
-			expect((<Event>(<unknown>loadEvent)).target).toBe(null);
+			expect((<Event>(<unknown>loadEvent)).target).toBe(element);
 			expect(loadEventTarget).toBe(element);
 			expect(loadEventCurrentTarget).toBe(element);
 		});
diff --git a/packages/happy-dom/test/nodes/html-script-element/HTMLScriptElement.test.ts b/packages/happy-dom/test/nodes/html-script-element/HTMLScriptElement.test.ts
index 85c9c957f..08ae8b252 100644
--- a/packages/happy-dom/test/nodes/html-script-element/HTMLScriptElement.test.ts
+++ b/packages/happy-dom/test/nodes/html-script-element/HTMLScriptElement.test.ts
@@ -208,7 +208,7 @@ describe('HTMLScriptElement', () => {
 
 				await window.happyDOM?.waitUntilComplete();
 
-				expect((<Event>(<unknown>loadEvent)).target).toBe(null);
+				expect((<Event>(<unknown>loadEvent)).target).toBe(script);
 				expect(loadEventTarget).toBe(script);
 				expect(loadEventCurrentTarget).toBe(script);
 				expect(fetchedURL).toBe('https://localhost:8080/path/to/script.js');
@@ -269,7 +269,7 @@ describe('HTMLScriptElement', () => {
 
 			window.document.body.appendChild(script);
 
-			expect((<Event>(<unknown>loadEvent)).target).toBe(null);
+			expect((<Event>(<unknown>loadEvent)).target).toBe(script);
 			expect(loadEventTarget).toBe(script);
 			expect(loadEventCurrentTarget).toBe(script);
 			expect(fetchedWindow).toBe(window);
diff --git a/packages/happy-dom/test/nodes/node/Node.test.ts b/packages/happy-dom/test/nodes/node/Node.test.ts
index 5a5e2b0a7..faf0fc519 100644
--- a/packages/happy-dom/test/nodes/node/Node.test.ts
+++ b/packages/happy-dom/test/nodes/node/Node.test.ts
@@ -887,7 +887,7 @@ describe('Node', () => {
 			expect(child.dispatchEvent(event)).toBe(true);
 
 			expect(childEvent).toBe(event);
-			expect((<Event>(<unknown>childEvent)).target).toBe(null);
+			expect((<Event>(<unknown>childEvent)).target).toBe(child);
 			expect((<Event>(<unknown>childEvent)).currentTarget).toBe(null);
 			expect(childEventTarget).toBe(child);
 			expect(childEventCurrentTarget).toBe(child);
@@ -922,7 +922,7 @@ describe('Node', () => {
 
 			expect(childEvent).toBe(event);
 			expect(parentEvent).toBe(event);
-			expect((<Event>(<unknown>parentEvent)).target).toBe(null);
+			expect((<Event>(<unknown>parentEvent)).target).toBe(child);
 			expect((<Event>(<unknown>parentEvent)).currentTarget).toBe(null);
 			expect(childEventTarget).toBe(child);
 			expect(childEventCurrentTarget).toBe(child);
diff --git a/packages/happy-dom/test/window/BrowserWindow.test.ts b/packages/happy-dom/test/window/BrowserWindow.test.ts
index 1377d7fe5..a0cb3c94c 100644
--- a/packages/happy-dom/test/window/BrowserWindow.test.ts
+++ b/packages/happy-dom/test/window/BrowserWindow.test.ts
@@ -1556,11 +1556,11 @@ describe('BrowserWindow', () => {
 				});
 
 				setTimeout(() => {
-					expect((<Event>event).target).toBe(null);
+					expect((<Event>event).target).toBe(document);
 					expect((<Event>event).currentTarget).toBe(null);
 					expect((<Event>event).eventPhase).toBe(EventPhaseEnum.none);
 					expect(target).toBe(document);
-					expect(currentTarget).toBe(document);
+					expect(currentTarget).toBe(window);
 					resolve(null);
 				}, 20);
 			});
@@ -1614,11 +1614,11 @@ describe('BrowserWindow', () => {
 					expect(resourceFetchCSSURL).toBe(cssURL);
 					expect(resourceFetchJSWindow === window).toBe(true);
 					expect(resourceFetchJSURL).toBe(jsURL);
-					expect((<Event>loadEvent).target).toBe(null);
+					expect((<Event>loadEvent).target).toBe(document);
 					expect((<Event>loadEvent).currentTarget).toBe(null);
 					expect((<Event>loadEvent).eventPhase).toBe(EventPhaseEnum.none);
 					expect(loadEventTarget).toBe(document);
-					expect(loadEventCurrentTarget).toBe(document);
+					expect(loadEventCurrentTarget).toBe(window);
 					expect(document.styleSheets.length).toBe(1);
 					expect(document.styleSheets[0].cssRules[0].cssText).toBe(cssResponse);
 
@@ -1648,9 +1648,9 @@ describe('BrowserWindow', () => {
 
 				setTimeout(() => {
 					expect(errorEvents.length).toBe(2);
-					expect(errorEvents[0].target).toBe(null);
+					expect(errorEvents[0].target).toBe(window);
 					expect((<Error>errorEvents[0].error).message).toBe('Script error');
-					expect(errorEvents[1].target).toBe(null);
+					expect(errorEvents[1].target).toBe(window);
 					expect((<Error>errorEvents[1].error).message).toBe('Timeout error');
 
 					resolve(null);