From 530f535c01f72de9190d9e67d0c3540d3c44a5fa Mon Sep 17 00:00:00 2001 From: ocavue Date: Thu, 29 Aug 2024 09:41:59 +1000 Subject: [PATCH] fix: [#1472] Fixes issue with matching nested element using > selector combinator (e.g. .x > .x) in matches() and closest() (#1473) * fix: [#1472] Fix child combinator matching in nested element * chore: [#1472] Improves logic to hopefully be less confusing --------- Co-authored-by: David Ortner --- .../src/query-selector/QuerySelector.ts | 47 ++++++++++--------- .../test/nodes/element/Element.test.ts | 29 +++++++++++- 2 files changed, 52 insertions(+), 24 deletions(-) diff --git a/packages/happy-dom/src/query-selector/QuerySelector.ts b/packages/happy-dom/src/query-selector/QuerySelector.ts index 0aebf3bab..c86b492f8 100644 --- a/packages/happy-dom/src/query-selector/QuerySelector.ts +++ b/packages/happy-dom/src/query-selector/QuerySelector.ts @@ -333,7 +333,7 @@ export default class QuerySelector { element[PropertySymbol.cache].matches.set(selector, cachedItem); for (const items of SelectorParser.getSelectorGroups(selector, options)) { - const result = this.matchSelector(element, element, items.reverse(), cachedItem); + const result = this.matchSelector(element, items.reverse(), cachedItem); if (result) { cachedItem.result.match = result; @@ -347,22 +347,23 @@ export default class QuerySelector { /** * Checks if a node matches a selector. * - * @param targetElement Target element. - * @param currentElement Current element. + * @param element Target element. + * @param currentElement * @param selectorItems Selector items. * @param cachedItem Cached item. + * @param [previousSelectorItem] Previous selector item. * @param [priorityWeight] Priority weight. * @returns Result. */ private static matchSelector( - targetElement: Element, - currentElement: Element, + element: Element, selectorItems: SelectorItem[], cachedItem: ICachedMatchesItem, + previousSelectorItem: SelectorItem | null = null, priorityWeight = 0 ): ISelectorMatch | null { const selectorItem = selectorItems[0]; - const result = selectorItem.match(currentElement); + const result = selectorItem.match(element); if (result) { if (selectorItems.length === 1) { @@ -373,14 +374,14 @@ export default class QuerySelector { switch (selectorItem.combinator) { case SelectorCombinatorEnum.adjacentSibling: - const previousElementSibling = currentElement.previousElementSibling; + const previousElementSibling = element.previousElementSibling; if (previousElementSibling) { previousElementSibling[PropertySymbol.affectsCache].push(cachedItem); const match = this.matchSelector( - targetElement, previousElementSibling, selectorItems.slice(1), cachedItem, + selectorItem, priorityWeight + result.priorityWeight ); @@ -391,16 +392,18 @@ export default class QuerySelector { break; case SelectorCombinatorEnum.child: case SelectorCombinatorEnum.descendant: - const parentElement = currentElement.parentElement; + const parentElement = element.parentElement; if (parentElement) { parentElement[PropertySymbol.affectsCache].push(cachedItem); + const match = this.matchSelector( - targetElement, parentElement, selectorItems.slice(1), cachedItem, + selectorItem, priorityWeight + result.priorityWeight ); + if (match) { return match; } @@ -409,19 +412,17 @@ export default class QuerySelector { } } - const parentElement = currentElement.parentElement; - if ( - selectorItem.combinator === SelectorCombinatorEnum.descendant && - targetElement !== currentElement && - parentElement - ) { - return this.matchSelector( - targetElement, - parentElement, - selectorItems, - cachedItem, - priorityWeight - ); + if (previousSelectorItem?.combinator === SelectorCombinatorEnum.descendant) { + const parentElement = element.parentElement; + if (parentElement) { + return this.matchSelector( + parentElement, + selectorItems, + cachedItem, + previousSelectorItem, + priorityWeight + ); + } } return null; diff --git a/packages/happy-dom/test/nodes/element/Element.test.ts b/packages/happy-dom/test/nodes/element/Element.test.ts index 1968547ee..35af62701 100644 --- a/packages/happy-dom/test/nodes/element/Element.test.ts +++ b/packages/happy-dom/test/nodes/element/Element.test.ts @@ -877,7 +877,7 @@ describe('Element', () => { grandparentElement.setAttribute('role', 'alert'); const parentElement = document.createElement('div'); - grandparentElement.setAttribute('role', 'status'); + parentElement.setAttribute('role', 'status'); grandparentElement.appendChild(parentElement); const element = document.createElement('div'); @@ -888,6 +888,33 @@ describe('Element', () => { expect(element.matches('div[role="alert"] > div.active')).toBe(false); expect(grandparentElement.matches('div > div[role="alert"]')).toBe(false); }); + + it('Checks if the ancestor element matches with a child combinator using ".x > .x"', () => { + const a = document.createElement('div'); + a.classList.add('a'); + + const b = document.createElement('div'); + b.classList.add('b'); + + const c = document.createElement('div'); + c.classList.add('c'); + + const d = document.createElement('div'); + d.classList.add('d'); + + a.appendChild(b); + b.appendChild(c); + c.appendChild(d); + + a.classList.add('x'); + b.classList.add('x'); + d.classList.add('x'); + + expect(a.matches('.x > .x')).toBe(false); + expect(b.matches('.x > .x')).toBe(true); + expect(c.matches('.x > .x')).toBe(false); + expect(d.matches('.x > .x')).toBe(false); + }); }); describe('closest()', () => {