diff --git a/packages/happy-dom/src/query-selector/QuerySelector.ts b/packages/happy-dom/src/query-selector/QuerySelector.ts index e6879251b..c3c79121c 100644 --- a/packages/happy-dom/src/query-selector/QuerySelector.ts +++ b/packages/happy-dom/src/query-selector/QuerySelector.ts @@ -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), @@ -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; } } @@ -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 @@ -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; } @@ -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 ); @@ -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], @@ -591,7 +622,6 @@ export default class QuerySelector { if (match) { return match; } - sibling = sibling.nextElementSibling; } break; } diff --git a/packages/happy-dom/test/query-selector/QuerySelector.test.ts b/packages/happy-dom/test/query-selector/QuerySelector.test.ts index c0d2aaa0a..9fbd8913d 100644 --- a/packages/happy-dom/test/query-selector/QuerySelector.test.ts +++ b/packages/happy-dom/test/query-selector/QuerySelector.test.ts @@ -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 = ` -
a1
-
b1
-
c1
-
a2
-
b2
-
a3
- `; - 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; @@ -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 = ` +
a1
+
b1
+
c1
+
a2
+
b2
+
a3
+ `; + + 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 = ` +
a1
+
b1
+
c1
+
a2
+
b2
+
a3
+ `; + 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()', () => { @@ -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 = ` +
a1
+
b1
+
c1
+
a2
+
b2
+
a3
+ `; + + const sibling = 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 = ` +
a1
+
b1
+
c1
+
a2
+
b2
+
a3
+ `; + + expect(div.querySelector('.a ~ .b')).toBe(div.children[1]); + }); }); describe('matches()', () => { @@ -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 = ` +
a1
+
b1
+
c1
+
a2
+
b2
+
a3
+ `; + + const sibling = 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 = ` +
a1
+
b1
+
c1
+
a2
+
b2
+
a3
+ `; + + const sibling2 = div.querySelector('.a ~ .b'); + + expect(sibling2.matches('.a ~ .b')).toBe(true); + expect(sibling2.matches('.a ~ .z')).toBe(false); + }); }); });