From 9a0eec3be43a9e4d7a63fcd41314d17700cc7c84 Mon Sep 17 00:00:00 2001 From: Jalil Arfaoui Date: Fri, 23 Jan 2026 01:09:55 +0100 Subject: [PATCH] fix: [#2034] QuerySelector fails to match attribute values containing quotes When using querySelector with an attribute selector, values containing apostrophes in double-quoted selectors (or double quotes in single-quoted selectors) were not matched correctly. For example, [data-value="it's a test"] would fail to find elements with that attribute value. The issue was in the regex that captures attribute values: it excluded both quote types regardless of which delimiter was used. The fix uses lookbehind/lookahead assertions to correctly match the value based on the actual delimiter used. --- .../src/query-selector/SelectorParser.ts | 2 +- .../test/query-selector/QuerySelector.test.ts | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/happy-dom/src/query-selector/SelectorParser.ts b/packages/happy-dom/src/query-selector/SelectorParser.ts index 95c3e3e73..7c490d44b 100644 --- a/packages/happy-dom/src/query-selector/SelectorParser.ts +++ b/packages/happy-dom/src/query-selector/SelectorParser.ts @@ -27,7 +27,7 @@ import DocumentFragment from '../nodes/document-fragment/DocumentFragment.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*["']((?<=")[^"]*(?=")|(?<=')[^']*(?='))["']\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. diff --git a/packages/happy-dom/test/query-selector/QuerySelector.test.ts b/packages/happy-dom/test/query-selector/QuerySelector.test.ts index 05be773cf..b8b87b1a4 100644 --- a/packages/happy-dom/test/query-selector/QuerySelector.test.ts +++ b/packages/happy-dom/test/query-selector/QuerySelector.test.ts @@ -502,6 +502,24 @@ describe('QuerySelector', () => { expect(elements[1] === container.children[0].children[1].children[1]).toBe(true); }); + it('Returns elements with attribute values containing apostrophes using double quotes as delimiter.', () => { + const container = document.createElement('div'); + container.innerHTML = `
Content
`; + + const elements = container.querySelectorAll('[data-value="it\'s a test"]'); + expect(elements.length).toBe(1); + expect(elements[0] === container.children[0]).toBe(true); + }); + + it('Returns elements with attribute values containing double quotes using apostrophes as delimiter.', () => { + const container = document.createElement('div'); + container.innerHTML = `
Content
`; + + const elements = container.querySelectorAll('[data-value=\'say "hello"\']'); + expect(elements.length).toBe(1); + expect(elements[0] === container.children[0]).toBe(true); + }); + it('Returns all elements with tag name and matching attributes using "span[_attr1]".', () => { const container = document.createElement('div'); container.innerHTML = QuerySelectorHTML.replace(/ attr1/gm, '_attr1');