Skip to content

Commit

Permalink
fix: [#1710] - Ensure querySelector returns the first item that appea…
Browse files Browse the repository at this point in the history
…rs in the DOM for grouped selectors (#1713)
  • Loading branch information
christiango authored Feb 8, 2025
1 parent fbf5adb commit 5551267
Show file tree
Hide file tree
Showing 2 changed files with 34 additions and 8 deletions.
27 changes: 19 additions & 8 deletions packages/happy-dom/src/query-selector/QuerySelector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,18 +249,25 @@ export default class QuerySelector {
(node[PropertySymbol.ownerDocument] || node)[PropertySymbol.affectsCache].push(cachedItem);
}

const matchesMap: Map<string, Element> = new Map();
const matchedPositions: string[] = [];
for (const items of SelectorParser.getSelectorGroups(selector)) {
const match =
node[PropertySymbol.nodeType] === NodeTypeEnum.elementNode
? this.findFirst(<Element>node, [<Element>node], items, cachedItem)
: this.findFirst(null, (<Element>node)[PropertySymbol.elementArray], items, cachedItem);

if (match) {
cachedItem.result = new WeakRef(match);
return match;
if (match && !matchesMap.has(match.documentPosition)) {
matchesMap.set(match.documentPosition, match.element);
matchedPositions.push(match.documentPosition);
}
}

if (matchedPositions.length > 0) {
const keys = matchedPositions.sort();
return matchesMap.get(keys[0]);
}

return null;
}

Expand Down Expand Up @@ -560,26 +567,30 @@ export default class QuerySelector {
* @param children Child elements.
* @param selectorItems Selector items.
* @param cachedItem Cached item.
* @returns HTML element.
* @param [documentPosition] Document position of the element.
* @returns Document position and element map.
*/
private static findFirst(
rootElement: Element,
children: Element[],
selectorItems: SelectorItem[],
cachedItem: ICachedQuerySelectorItem
): Element {
cachedItem: ICachedQuerySelectorItem,
documentPosition?: string
): DocumentPositionAndElement | null {
const selectorItem = selectorItems[0];
const nextSelectorItem = selectorItems[1];

for (const child of children) {
for (let i = 0, max = children.length; i < max; i++) {
const child = children[i];
const childrenOfChild = (<Element>child)[PropertySymbol.elementArray];
const position = (documentPosition ? documentPosition + '>' : '') + String.fromCharCode(i);

child[PropertySymbol.affectsCache].push(cachedItem);

if (selectorItem.match(child)) {
if (!nextSelectorItem) {
if (rootElement !== child) {
return child;
return { documentPosition: position, element: child };
}
} else {
switch (nextSelectorItem.combinator) {
Expand Down
15 changes: 15 additions & 0 deletions packages/happy-dom/test/query-selector/QuerySelector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1931,5 +1931,20 @@ describe('QuerySelector', () => {
expect(sibling2.matches('.a ~ .b')).toBe(true);
expect(sibling2.matches('.a ~ .z')).toBe(false);
});

it('Matches grouped selectors in the right order', () => {
const div = document.createElement('div');

div.innerHTML = `
<div class>
<h1><span>Here is a heading</span></h1>
<div class="a">
<span>With a child span</span>
</div>
</div>
`;

expect(div.querySelector('.a,h1')).toBe(div.children[0].children[0]);
});
});
});

0 comments on commit 5551267

Please sign in to comment.