Skip to content

Commit

Permalink
fix: [#1472] Fixes issue with matching nested element using > selecto…
Browse files Browse the repository at this point in the history
…r 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 <david@ortner.se>
  • Loading branch information
ocavue and capricorn86 committed Aug 28, 2024
1 parent a99364f commit 530f535
Show file tree
Hide file tree
Showing 2 changed files with 52 additions and 24 deletions.
47 changes: 24 additions & 23 deletions packages/happy-dom/src/query-selector/QuerySelector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) {
Expand All @@ -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
);

Expand All @@ -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;
}
Expand All @@ -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;
Expand Down
29 changes: 28 additions & 1 deletion packages/happy-dom/test/nodes/element/Element.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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()', () => {
Expand Down

0 comments on commit 530f535

Please sign in to comment.