From 5b2a828f5ef59f1e366a552478b1338e2d62ddcd Mon Sep 17 00:00:00 2001 From: David Ortner Date: Fri, 30 Aug 2024 18:04:18 +0200 Subject: [PATCH] chore: [#1507] Fixes selector validation, so that it converts values to string --- .../src/query-selector/QuerySelector.ts | 110 +++++++++--------- .../test/query-selector/QuerySelector.test.ts | 80 +++++++++++++ 2 files changed, 138 insertions(+), 52 deletions(-) diff --git a/packages/happy-dom/src/query-selector/QuerySelector.ts b/packages/happy-dom/src/query-selector/QuerySelector.ts index 6a2aaff4f..752ea310b 100644 --- a/packages/happy-dom/src/query-selector/QuerySelector.ts +++ b/packages/happy-dom/src/query-selector/QuerySelector.ts @@ -78,27 +78,32 @@ export default class QuerySelector { node: Element | Document | DocumentFragment, selector: string ): NodeList { - if (selector === null || selector === undefined) { - return new NodeList(PropertySymbol.illegalConstructor, []); - } - const window = node[PropertySymbol.window]; if (selector === '') { - throw new window.Error( + throw new window.DOMException( `Failed to execute 'querySelectorAll' on '${node.constructor.name}': The provided selector is empty.` ); } - if (typeof selector !== 'string' && typeof selector !== 'boolean') { + if (typeof selector === 'function') { throw new window.DOMException( - `Failed to execute 'querySelectorAll' on '${node.constructor.name}': '${selector}' is not a valid selector.`, - 'SyntaxError' + `Failed to execute 'querySelectorAll' on '${node.constructor.name}': '${selector}' is not a valid selector.` ); } + if (typeof selector === 'symbol') { + throw new window.TypeError(`Cannot convert a Symbol value to a string`); + } + selector = String(selector); + if (INVALID_SELECTOR_REGEXP.test(selector)) { + throw new window.DOMException( + `Failed to execute 'querySelectorAll' on '${node.constructor.name}': '${selector}' is not a valid selector.` + ); + } + const cache = node[PropertySymbol.cache].querySelectorAll; const cachedResult = cache.get(selector); @@ -109,12 +114,6 @@ export default class QuerySelector { } } - if (INVALID_SELECTOR_REGEXP.test(selector)) { - throw new window.Error( - `Failed to execute 'querySelectorAll' on '${node.constructor.name}': '${selector}' is not a valid selector.` - ); - } - const groups = SelectorParser.getSelectorGroups(selector); const items: Element[] = []; const nodeList = new NodeList(PropertySymbol.illegalConstructor, items); @@ -199,27 +198,38 @@ export default class QuerySelector { node: Element | Document | DocumentFragment, selector: string ): Element | null { - if (selector === null || selector === undefined) { - return null; - } - const window = node[PropertySymbol.window]; if (selector === '') { - throw new window.Error( + throw new window.DOMException( `Failed to execute 'querySelector' on '${node.constructor.name}': The provided selector is empty.` ); } - if (typeof selector !== 'string' && typeof selector !== 'boolean') { + if (typeof selector === 'function' || typeof selector === 'symbol') { throw new window.DOMException( - `Failed to execute 'querySelector' on '${node.constructor.name}': '${selector}' is not a valid selector.`, - 'SyntaxError' + `Failed to execute 'querySelector' on '${node.constructor.name}': '${selector}' is not a valid selector.` + ); + } + + if (typeof selector === 'function') { + throw new window.DOMException( + `Failed to execute 'querySelector' on '${node.constructor.name}': '${selector}' is not a valid selector.` ); } + if (typeof selector === 'symbol') { + throw new window.TypeError(`Cannot convert a Symbol value to a string`); + } + selector = String(selector); + if (INVALID_SELECTOR_REGEXP.test(selector)) { + throw new window.DOMException( + `Failed to execute 'querySelector' on '${node.constructor.name}': '${selector}' is not a valid selector.` + ); + } + const cachedResult = node[PropertySymbol.cache].querySelector.get(selector); if (cachedResult?.result) { @@ -229,12 +239,6 @@ export default class QuerySelector { } } - if (INVALID_SELECTOR_REGEXP.test(selector)) { - throw new window.Error( - `Failed to execute 'querySelector' on '${node.constructor.name}': '${selector}' is not a valid selector.` - ); - } - const cachedItem: ICachedQuerySelectorItem = { result: >{ deref: () => null @@ -277,55 +281,57 @@ export default class QuerySelector { selector: string, options?: { ignoreErrors?: boolean } ): ISelectorMatch | null { - if (!selector) { - return null; - } + const ignoreErrors = options?.ignoreErrors; + const window = element[PropertySymbol.window]; - if (selector === null || selector === undefined) { + if (selector === '*') { return { - priorityWeight: 0 + priorityWeight: 1 }; } - const window = element[PropertySymbol.window]; - if (selector === '') { - throw new window.Error( + if (ignoreErrors) { + return null; + } + throw new window.DOMException( `Failed to execute 'matches' on '${element.constructor.name}': The provided selector is empty.` ); } - if (typeof selector !== 'string' && typeof selector !== 'boolean') { + if (typeof selector === 'function') { + if (ignoreErrors) { + return null; + } throw new window.DOMException( - `Failed to execute 'matches' on '${element.constructor.name}': '${selector}' is not a valid selector.`, - DOMExceptionNameEnum.syntaxError + `Failed to execute 'matches' on '${element.constructor.name}': '${selector}' is not a valid selector.` ); } - selector = String(selector); - - if (selector === '*') { - return { - priorityWeight: 1 - }; + if (typeof selector === 'symbol') { + if (ignoreErrors) { + return null; + } + throw new window.TypeError(`Cannot convert a Symbol value to a string`); } - const ignoreErrors = options?.ignoreErrors; - const cachedResult = element[PropertySymbol.cache].matches.get(selector); - - if (cachedResult?.result) { - return cachedResult.result.match; - } + selector = String(selector); if (INVALID_SELECTOR_REGEXP.test(selector)) { if (ignoreErrors) { return null; } - throw new window.Error( + throw new window.DOMException( `Failed to execute 'matches' on '${element.constructor.name}': '${selector}' is not a valid selector.` ); } + const cachedResult = element[PropertySymbol.cache].matches.get(selector); + + if (cachedResult?.result) { + return cachedResult.result.match; + } + const cachedItem: ICachedMatchesItem = { result: { match: null } }; diff --git a/packages/happy-dom/test/query-selector/QuerySelector.test.ts b/packages/happy-dom/test/query-selector/QuerySelector.test.ts index 9716d67a2..ab4efbf6d 100644 --- a/packages/happy-dom/test/query-selector/QuerySelector.test.ts +++ b/packages/happy-dom/test/query-selector/QuerySelector.test.ts @@ -39,6 +39,38 @@ describe('QuerySelector', () => { expect(() => container.querySelectorAll((true))).not.toThrow(); }); + it('Converts selector values to string.', () => { + const container = document.createElement('div'); + container.innerHTML = ` + + + + + + + `; + + const elements = container.querySelectorAll((['false'])); + expect(elements.length).toBe(1); + expect(elements[0] === container.children[0].children[0]).toBe(true); + + const elements2 = container.querySelectorAll((false)); + expect(elements2.length).toBe(1); + expect(elements2[0] === container.children[0].children[0]).toBe(true); + + const elements3 = container.querySelectorAll((true)); + expect(elements3.length).toBe(1); + expect(elements3[0] === container.children[0].children[1]).toBe(true); + + const elements4 = container.querySelectorAll((null)); + expect(elements4.length).toBe(1); + expect(elements4[0] === container.children[0].children[2]).toBe(true); + + const elements5 = container.querySelectorAll((undefined)); + expect(elements5.length).toBe(1); + expect(elements5[0] === container.children[0].children[3]).toBe(true); + }); + it('Returns all span elements.', () => { const container = document.createElement('div'); container.innerHTML = QuerySelectorHTML; @@ -1134,6 +1166,38 @@ describe('QuerySelector', () => { expect(() => container.querySelector((true))).not.toThrow(); }); + it('Converts selector values to string.', () => { + const container = document.createElement('div'); + container.innerHTML = ` + + + + + + + `; + + expect(container.querySelector((['false']))).toBe( + container.children[0].children[0] + ); + + expect(container.querySelector(((false)))).toBe( + container.children[0].children[0] + ); + + expect(container.querySelector(((true)))).toBe( + container.children[0].children[1] + ); + + expect(container.querySelector(((null)))).toBe( + container.children[0].children[2] + ); + + expect(container.querySelector(((undefined)))).toBe( + container.children[0].children[3] + ); + }); + it('Returns a span matching "span".', () => { const div1 = document.createElement('div'); const div2 = document.createElement('div'); @@ -1513,6 +1577,22 @@ describe('QuerySelector', () => { expect(() => container.matches((true))).not.toThrow(); }); + it('Converts selector values to string.', () => { + const container = document.createElement('div'); + container.innerHTML = ` + + + + + `; + + expect(container.children[0].matches((['false']))).toBe(true); + expect(container.children[0].matches((false))).toBe(true); + expect(container.children[1].matches((true))).toBe(true); + expect(container.children[2].matches((null))).toBe(true); + expect(container.children[3].matches((undefined))).toBe(true); + }); + it('Returns true when the element matches the selector', () => { const div = document.createElement('div'); div.innerHTML = '
';