Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: [#1472] Fix child combinator matching in nested element #1473

Merged
merged 3 commits into from
Aug 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading