Repository: capricorn86/happy-dom (ts)

Issue:
`composedPath` is incorrect for composed bubbling events originating from `HTMLAnchorElement`
**Describe the bug**
There's an issue where `Event.composedPath` will be constructed incorrectly if `event.target` is `HTMLAnchorElement` with `href` attribute being set.

**To Reproduce**
Steps to reproduce the behavior:
1. Create an <a> element with `href` attribute set to a valid domain, e.g. `https://example.com` and it attach to `body`.
2. Add event listener for created element, e.g. `click`.
3. Trigger `click` event on this element. Event should have both `bubbles` and `composed` set to `true`.
4. Inspect value of `event.composedPath()` inside `click` event handler.

**Expected behavior**
Composed path should be the following array: `[HTMLAnchorElement, document.body, document.documentElement, document, window]`.

**Additional context**
Looks like `composedPath` checks `eventTarget.host` to detect `IShadowRoot` boundary. Unfortunately `HTMLAnchorElement` also has [`host` property](https://developer.mozilla.org/en-US/docs/Web/API/HTMLAnchorElement/host) which breaks `while` loop through parent nodes of the event target.

https://github.com/capricorn86/happy-dom/blob/62085eba63a46c5e7716e52d50af82b6ee499fee/packages/happy-dom/src/event/Event.ts#L71-L78

Pull request:
#709@patch: Properly detect `ShadowRoot` boundary when constructing `composedPath`
Fixes #709.

Not exactly sure about this `instanceof ShadowRoot` check with an extra import as I couldn't find other similar usages the codebase. Will be happy to update this PR If there's a more desirable pattern.

Patch:
diff --git a/packages/happy-dom/src/css/declaration/utilities/CSSStyleDeclarationElementStyle.ts b/packages/happy-dom/src/css/declaration/utilities/CSSStyleDeclarationElementStyle.ts
index 06a55856..2850d158 100644
--- a/packages/happy-dom/src/css/declaration/utilities/CSSStyleDeclarationElementStyle.ts
+++ b/packages/happy-dom/src/css/declaration/utilities/CSSStyleDeclarationElementStyle.ts
@@ -129,7 +129,10 @@ export default class CSSStyleDeclarationElementStyle {
 				}
 
 				styleAndElement = { element: null, cssTexts: [] };
-			} else if ((<IShadowRoot>styleAndElement.element).host) {
+			} else if (
+				styleAndElement.element.nodeType === NodeTypeEnum.documentFragmentNode &&
+				(<IShadowRoot>styleAndElement.element).host
+			) {
 				const styleSheets = <INodeList<IHTMLStyleElement>>(
 					(<IShadowRoot>styleAndElement.element).querySelectorAll('style,link[rel="stylesheet"]')
 				);
diff --git a/packages/happy-dom/src/event/Event.ts b/packages/happy-dom/src/event/Event.ts
index fead294b..451df20c 100644
--- a/packages/happy-dom/src/event/Event.ts
+++ b/packages/happy-dom/src/event/Event.ts
@@ -3,6 +3,7 @@ import INode from '../nodes/node/INode';
 import IWindow from '../window/IWindow';
 import IShadowRoot from '../nodes/shadow-root/IShadowRoot';
 import IEventTarget from './IEventTarget';
+import NodeTypeEnum from '../nodes/node/NodeTypeEnum';
 
 /**
  * Event.
@@ -69,7 +70,11 @@ export default class Event {
 			composedPath.push(eventTarget);
 
 			if (this.bubbles) {
-				if (this.composed && (<IShadowRoot>eventTarget).host) {
+				if (
+					this.composed &&
+					(<INode>eventTarget).nodeType === NodeTypeEnum.documentFragmentNode &&
+					(<IShadowRoot>eventTarget).host
+				) {
 					eventTarget = (<IShadowRoot>eventTarget).host;
 				} else if ((<INode>(<unknown>this.target)).ownerDocument === eventTarget) {
 					eventTarget = (<INode>(<unknown>this.target)).ownerDocument.defaultView;
diff --git a/packages/happy-dom/src/nodes/node/Node.ts b/packages/happy-dom/src/nodes/node/Node.ts
index 2d75e98e..a493f405 100644
--- a/packages/happy-dom/src/nodes/node/Node.ts
+++ b/packages/happy-dom/src/nodes/node/Node.ts
@@ -482,7 +482,7 @@ export default class Node extends EventTarget implements INode {
 			}
 
 			// eslint-disable-next-line
-			if (event.composed && (<any>this).host) {
+			if (event.composed && this.nodeType === NodeTypeEnum.documentFragmentNode && (<any>this).host) {
 				// eslint-disable-next-line
 				return (<any>this).host.dispatchEvent(event);
 			}


Test patch:
diff --git a/packages/happy-dom/test/event/Event.test.ts b/packages/happy-dom/test/event/Event.test.ts
index fa7ea261..fb1b5d95 100644
--- a/packages/happy-dom/test/event/Event.test.ts
+++ b/packages/happy-dom/test/event/Event.test.ts
@@ -99,5 +99,32 @@ describe('Event', () => {
 				customELement.shadowRoot
 			]);
 		});
+
+		it('Returns correct composed for HTMLAnchorElement event target and composed is set to "true".', () => {
+			const anchor = document.createElement('a');
+			anchor.setAttribute('href', 'https://example.com');
+			let composedPath = null;
+
+			document.body.appendChild(anchor);
+
+			anchor.addEventListener('click', (event: Event) => {
+				composedPath = event.composedPath();
+			});
+
+			anchor.dispatchEvent(
+				new Event('click', {
+					bubbles: true,
+					composed: true
+				})
+			);
+
+			expect(composedPath).toEqual([
+				anchor,
+				document.body,
+				document.documentElement,
+				document,
+				window
+			]);
+		});
 	});
 });