Skip to content

Commit

Permalink
chore: [capricorn86#1683] Improves performance and adds support for m…
Browse files Browse the repository at this point in the history
…atches
  • Loading branch information
capricorn86 committed Jan 14, 2025
1 parent 123a765 commit 700f295
Show file tree
Hide file tree
Showing 2 changed files with 154 additions and 33 deletions.
50 changes: 40 additions & 10 deletions packages/happy-dom/src/query-selector/QuerySelector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,7 @@ export default class QuerySelector {
const previousElementSibling = element.previousElementSibling;
if (previousElementSibling) {
previousElementSibling[PropertySymbol.affectsCache].push(cachedItem);

const match = this.matchSelector(
previousElementSibling,
selectorItems.slice(1),
Expand Down Expand Up @@ -419,6 +420,33 @@ export default class QuerySelector {
}
}
break;
case SelectorCombinatorEnum.subsequentSibling:
const siblingParentElement = element.parentElement;
if (siblingParentElement) {
const siblings = siblingParentElement[PropertySymbol.elementArray];
const index = siblings.indexOf(element);

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

for (let i = index - 1; i >= 0; i--) {
const sibling = siblings[i];

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

const match = this.matchSelector(
sibling,
selectorItems.slice(1),
cachedItem,
selectorItem,
priorityWeight + result.priorityWeight
);

if (match) {
return match;
}
}
}
break;
}
}

Expand Down Expand Up @@ -477,11 +505,12 @@ export default class QuerySelector {
} else {
switch (nextSelectorItem.combinator) {
case SelectorCombinatorEnum.adjacentSibling:
if (child.nextElementSibling) {
const nextElementSibling = child.nextElementSibling;
if (nextElementSibling) {
matched = matched.concat(
this.findAll(
rootElement,
[child.nextElementSibling],
[nextElementSibling],
selectorItems.slice(1),
cachedItem,
position
Expand All @@ -502,12 +531,12 @@ export default class QuerySelector {
);
break;
case SelectorCombinatorEnum.subsequentSibling:
let sibling = child.nextElementSibling;
while (sibling) {
const index = children.indexOf(child);
for (let j = index + 1; j < children.length; j++) {
const sibling = children[j];
matched = matched.concat(
this.findAll(rootElement, [sibling], selectorItems.slice(1), cachedItem, position)
);
sibling = sibling.nextElementSibling;
}
break;
}
Expand Down Expand Up @@ -555,10 +584,11 @@ export default class QuerySelector {
} else {
switch (nextSelectorItem.combinator) {
case SelectorCombinatorEnum.adjacentSibling:
if (child.nextElementSibling) {
const nextElementSibling = child.nextElementSibling;
if (nextElementSibling) {
const match = this.findFirst(
rootElement,
[child.nextElementSibling],
[nextElementSibling],
selectorItems.slice(1),
cachedItem
);
Expand All @@ -580,8 +610,9 @@ export default class QuerySelector {
}
break;
case SelectorCombinatorEnum.subsequentSibling:
let sibling = child.nextElementSibling;
while (sibling) {
const index = children.indexOf(child);
for (let i = index + 1; i < children.length; i++) {
const sibling = children[i];
const match = this.findFirst(
rootElement,
[sibling],
Expand All @@ -591,7 +622,6 @@ export default class QuerySelector {
if (match) {
return match;
}
sibling = sibling.nextElementSibling;
}
break;
}
Expand Down
137 changes: 114 additions & 23 deletions packages/happy-dom/test/query-selector/QuerySelector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -386,29 +386,6 @@ describe('QuerySelector', () => {
expect(firstDivC === null).toBe(true);
});

it('subsequentSibling combinator should select subsequent siblings', () => {
const div = document.createElement('div');

div.innerHTML = `
<div class="a">a1</div>
<div class="b">b1</div>
<div class="c">c1</div>
<div class="a">a2</div>
<div class="b">b2</div>
<div class="a">a3</div>
`;
const firstDivB = div.querySelector('.a ~ .b');
const secondChildren = div.children[1];

expect(secondChildren === firstDivB).toBe(true);
expect(firstDivB?.textContent).toBe('b1');

const allSubsequentSiblingsA = QuerySelector.querySelectorAll(div, '.a ~ .a');
expect(allSubsequentSiblingsA.length).toBe(2);
expect(allSubsequentSiblingsA[0].textContent).toBe('a2');
expect(allSubsequentSiblingsA[1].textContent).toBe('a3');
});

it('Returns all elements with matching attributes using "[attr1="value1"]".', () => {
const container = document.createElement('div');
container.innerHTML = QuerySelectorHTML;
Expand Down Expand Up @@ -1235,6 +1212,45 @@ describe('QuerySelector', () => {
container.children[0]
]);
});

it('Returns all elements for subsequent sibling selector using ".a ~ .a"', () => {
const div = document.createElement('div');

div.innerHTML = `
<div class="a">a1</div>
<div class="b">b1</div>
<div class="c">c1</div>
<div class="a">a2</div>
<div class="b">b2</div>
<div class="a">a3</div>
`;

const subsequentSiblings = QuerySelector.querySelectorAll(div, '.a ~ .a');
expect(subsequentSiblings.length).toBe(2);
expect(subsequentSiblings[0].textContent).toBe('a2');
expect(subsequentSiblings[1].textContent).toBe('a3');

// Test cache 1
subsequentSiblings[0].className = 'z';

const subsequentSiblings2 = QuerySelector.querySelectorAll(div, '.a ~ .a');
expect(subsequentSiblings2.length).toBe(1);
expect(subsequentSiblings2[0].textContent).toBe('a3');

// Test cache 2
div.innerHTML = `
<div class="a">a1</div>
<div class="b">b1</div>
<div class="c">c1</div>
<div class="a">a2</div>
<div class="b">b2</div>
<div class="a">a3</div>
`;
const subsequentSiblings3 = QuerySelector.querySelectorAll(div, '.a ~ .a');
expect(subsequentSiblings3.length).toBe(2);
expect(subsequentSiblings3[0].textContent).toBe('a2');
expect(subsequentSiblings3[1].textContent).toBe('a3');
});
});

describe('querySelector()', () => {
Expand Down Expand Up @@ -1661,6 +1677,42 @@ describe('QuerySelector', () => {

expect(div.querySelector('.class1.class2')).toBe(div.children[0]);
});

it('Returns element matching subsequent sibling selector using ".a ~ .b"', () => {
const div = document.createElement('div');

div.innerHTML = `
<div class="a">a1</div>
<div class="b">b1</div>
<div class="c">c1</div>
<div class="a">a2</div>
<div class="b">b2</div>
<div class="a">a3</div>
`;

const sibling = <HTMLElement>div.querySelector('.a ~ .b');
const secondChild = div.children[1];

expect(secondChild === sibling).toBe(true);
expect(sibling.textContent).toBe('b1');

// Test cache 1
sibling.setAttribute('class', 'z');

expect(div.querySelector('.a ~ .b')).toBe(div.children[4]);

// Test cache 2
div.innerHTML = `
<div class="a">a1</div>
<div class="b">b1</div>
<div class="c">c1</div>
<div class="a">a2</div>
<div class="b">b2</div>
<div class="a">a3</div>
`;

expect(div.querySelector('.a ~ .b')).toBe(div.children[1]);
});
});

describe('matches()', () => {
Expand Down Expand Up @@ -1840,5 +1892,44 @@ describe('QuerySelector', () => {
expect(QuerySelector.matches(element, ':where', { ignoreErrors: true })).toBe(null);
expect(QuerySelector.matches(element, 'div:not', { ignoreErrors: true })).toBe(null);
});

it('Returns element matching subsequent sibling selector using ".a ~ .b"', () => {
const div = document.createElement('div');

div.innerHTML = `
<div class="a">a1</div>
<div class="b">b1</div>
<div class="c">c1</div>
<div class="a">a2</div>
<div class="b">b2</div>
<div class="a">a3</div>
`;

const sibling = <HTMLElement>div.querySelector('.a ~ .b');

expect(sibling.matches('.a ~ .b')).toBe(true);
expect(sibling.matches('.a ~ .z')).toBe(false);

// Test cache 1
sibling.setAttribute('class', 'z');

expect(sibling.matches('.a ~ .b')).toBe(false);
expect(sibling.matches('.a ~ .z')).toBe(true);

// Test cache 2
div.innerHTML = `
<div class="a">a1</div>
<div class="b">b1</div>
<div class="c">c1</div>
<div class="a">a2</div>
<div class="b">b2</div>
<div class="a">a3</div>
`;

const sibling2 = <HTMLElement>div.querySelector('.a ~ .b');

expect(sibling2.matches('.a ~ .b')).toBe(true);
expect(sibling2.matches('.a ~ .z')).toBe(false);
});
});
});

0 comments on commit 700f295

Please sign in to comment.