From 123a76546603d03ed0e59a940fcc27f832cb257d Mon Sep 17 00:00:00 2001 From: karpiuMG Date: Sun, 12 Jan 2025 19:26:14 +0100 Subject: [PATCH] fix: [#1683] Fix not recognizing subsequent sibling combinator in QuerySelector --- .../src/query-selector/QuerySelector.ts | 24 +++++++++++++++++++ .../query-selector/SelectorCombinatorEnum.ts | 3 ++- .../src/query-selector/SelectorParser.ts | 9 ++++++- .../test/query-selector/QuerySelector.test.ts | 23 ++++++++++++++++++ 4 files changed, 57 insertions(+), 2 deletions(-) diff --git a/packages/happy-dom/src/query-selector/QuerySelector.ts b/packages/happy-dom/src/query-selector/QuerySelector.ts index a6d428750..e6879251b 100644 --- a/packages/happy-dom/src/query-selector/QuerySelector.ts +++ b/packages/happy-dom/src/query-selector/QuerySelector.ts @@ -501,6 +501,15 @@ export default class QuerySelector { ) ); break; + case SelectorCombinatorEnum.subsequentSibling: + let sibling = child.nextElementSibling; + while (sibling) { + matched = matched.concat( + this.findAll(rootElement, [sibling], selectorItems.slice(1), cachedItem, position) + ); + sibling = sibling.nextElementSibling; + } + break; } } } @@ -570,6 +579,21 @@ export default class QuerySelector { return match; } break; + case SelectorCombinatorEnum.subsequentSibling: + let sibling = child.nextElementSibling; + while (sibling) { + const match = this.findFirst( + rootElement, + [sibling], + selectorItems.slice(1), + cachedItem + ); + if (match) { + return match; + } + sibling = sibling.nextElementSibling; + } + break; } } } diff --git a/packages/happy-dom/src/query-selector/SelectorCombinatorEnum.ts b/packages/happy-dom/src/query-selector/SelectorCombinatorEnum.ts index 07a18a8d5..af5de8b90 100644 --- a/packages/happy-dom/src/query-selector/SelectorCombinatorEnum.ts +++ b/packages/happy-dom/src/query-selector/SelectorCombinatorEnum.ts @@ -1,7 +1,8 @@ enum SelectorCombinatorEnum { descendant = 'descendant', child = 'child', - adjacentSibling = 'adjacentSibling' + adjacentSibling = 'adjacentSibling', + subsequentSibling = 'subsequentSibling' } export default SelectorCombinatorEnum; diff --git a/packages/happy-dom/src/query-selector/SelectorParser.ts b/packages/happy-dom/src/query-selector/SelectorParser.ts index c04ca4c85..566b0f391 100644 --- a/packages/happy-dom/src/query-selector/SelectorParser.ts +++ b/packages/happy-dom/src/query-selector/SelectorParser.ts @@ -25,7 +25,7 @@ import ISelectorPseudo from './ISelectorPseudo.js'; * Group 17: Combinator. */ const SELECTOR_REGEXP = - /(\*)|([a-zA-Z0-9-]+)|#((?:[a-zA-Z0-9-_]|\\.)+)|\.((?:[a-zA-Z0-9-_]|\\.)+)|\[([a-zA-Z0-9-_\\:]+)\]|\[([a-zA-Z0-9-_\\:]+)\s*([~|^$*]{0,1})\s*=\s*["']{1}([^"']*)["']{1}\s*(s|i){0,1}\]|\[([a-zA-Z0-9-_]+)\s*([~|^$*]{0,1})\s*=\s*([^\]]*)\]|:([a-zA-Z-]+)\s*\(([^)]+)\){0,1}|:([a-zA-Z-]+)|::([a-zA-Z-]+)|([\s,+>]*)/gm; + /(\*)|([a-zA-Z0-9-]+)|#((?:[a-zA-Z0-9-_]|\\.)+)|\.((?:[a-zA-Z0-9-_]|\\.)+)|\[([a-zA-Z0-9-_\\:]+)\]|\[([a-zA-Z0-9-_\\:]+)\s*([~|^$*]{0,1})\s*=\s*["']{1}([^"']*)["']{1}\s*(s|i){0,1}\]|\[([a-zA-Z0-9-_]+)\s*([~|^$*]{0,1})\s*=\s*([^\]]*)\]|:([a-zA-Z-]+)\s*\(([^)]+)\){0,1}|:([a-zA-Z-]+)|::([a-zA-Z-]+)|([\s,+>~]*)/gm; /** * Escaped Character RegExp. @@ -193,6 +193,13 @@ export default class SelectorParser { }); currentGroup.push(currentSelectorItem); break; + case '~': + currentSelectorItem = new SelectorItem({ + combinator: SelectorCombinatorEnum.subsequentSibling, + ignoreErrors + }); + currentGroup.push(currentSelectorItem); + break; case '': currentSelectorItem = new SelectorItem({ combinator: SelectorCombinatorEnum.descendant, diff --git a/packages/happy-dom/test/query-selector/QuerySelector.test.ts b/packages/happy-dom/test/query-selector/QuerySelector.test.ts index 7e4f9ea8b..c0d2aaa0a 100644 --- a/packages/happy-dom/test/query-selector/QuerySelector.test.ts +++ b/packages/happy-dom/test/query-selector/QuerySelector.test.ts @@ -386,6 +386,29 @@ 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;