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