Skip to content

Commit c80a08f

Browse files
authored
fix: [#1620] Fixes issue related to CSS pseudo selector :scope (#1911)
1 parent 220df23 commit c80a08f

5 files changed

Lines changed: 49 additions & 19 deletions

File tree

packages/happy-dom/src/nodes/document/Document.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1472,7 +1472,7 @@ export default class Document extends Node {
14721472
): NodeList<IHTMLElementTagNameMap[K]>;
14731473

14741474
/**
1475-
* Query CSS selector to find matching elments.
1475+
* Query CSS selector to find matching elements.
14761476
*
14771477
* @param selector CSS selector.
14781478
* @returns Matching elements.
@@ -1482,15 +1482,15 @@ export default class Document extends Node {
14821482
): NodeList<ISVGElementTagNameMap[K]>;
14831483

14841484
/**
1485-
* Query CSS selector to find matching elments.
1485+
* Query CSS selector to find matching elements.
14861486
*
14871487
* @param selector CSS selector.
14881488
* @returns Matching elements.
14891489
*/
14901490
public querySelectorAll(selector: string): NodeList<Element>;
14911491

14921492
/**
1493-
* Query CSS selector to find matching elments.
1493+
* Query CSS selector to find matching elements.
14941494
*
14951495
* @param selector CSS selector.
14961496
* @returns Matching elements.

packages/happy-dom/src/query-selector/QuerySelector.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,11 @@ export default class QuerySelector {
115115
}
116116
}
117117

118-
const groups = SelectorParser.getSelectorGroups(selector, { scope: node });
118+
const scope =
119+
node[PropertySymbol.nodeType] === NodeTypeEnum.documentNode
120+
? (<Document>node).documentElement
121+
: node;
122+
const groups = SelectorParser.getSelectorGroups(selector, { scope });
119123
const items: Element[] = [];
120124
const nodeList = new NodeList<Element>(PropertySymbol.illegalConstructor, items);
121125
const matchesMap: Map<string, Element> = new Map();
@@ -251,7 +255,11 @@ export default class QuerySelector {
251255

252256
const matchesMap: Map<string, Element> = new Map();
253257
const matchedPositions: string[] = [];
254-
for (const items of SelectorParser.getSelectorGroups(selector, { scope: node })) {
258+
const scope =
259+
node[PropertySymbol.nodeType] === NodeTypeEnum.documentNode
260+
? (<Document>node).documentElement
261+
: node;
262+
for (const items of SelectorParser.getSelectorGroups(selector, { scope })) {
255263
const match =
256264
node[PropertySymbol.nodeType] === NodeTypeEnum.elementNode
257265
? this.findFirst(<Element>node, [<Element>node], items, cachedItem)
@@ -350,9 +358,14 @@ export default class QuerySelector {
350358
);
351359
}
352360

361+
const scopeOrElement = options?.scope || element;
362+
const scope =
363+
scopeOrElement[PropertySymbol.nodeType] === NodeTypeEnum.documentNode
364+
? (<Document>scopeOrElement).documentElement
365+
: scopeOrElement;
353366
for (const items of SelectorParser.getSelectorGroups(selector, {
354367
...options,
355-
scope: options?.scope || element
368+
scope
356369
})) {
357370
const result = this.matchSelector(element, items.reverse(), cachedItem);
358371

packages/happy-dom/src/query-selector/SelectorItem.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import SelectorCombinatorEnum from './SelectorCombinatorEnum.js';
55
import ISelectorAttribute from './ISelectorAttribute.js';
66
import ISelectorMatch from './ISelectorMatch.js';
77
import ISelectorPseudo from './ISelectorPseudo.js';
8-
import Document from '../nodes/document/Document.js';
98
import DocumentFragment from '../nodes/document-fragment/DocumentFragment.js';
109

1110
const SPACE_REGEXP = /\s+/;
@@ -14,7 +13,8 @@ const SPACE_REGEXP = /\s+/;
1413
* Selector item.
1514
*/
1615
export default class SelectorItem {
17-
public scope: Element | Document | DocumentFragment | null;
16+
public root: Element | DocumentFragment | null;
17+
public scope: Element | DocumentFragment | null;
1818
public tagName: string | null;
1919
public id: string | null;
2020
public classNames: string[] | null;
@@ -39,7 +39,7 @@ export default class SelectorItem {
3939
* @param [options.ignoreErrors] Ignore errors.
4040
*/
4141
constructor(options?: {
42-
scope?: Element | Document | DocumentFragment;
42+
scope?: Element | DocumentFragment;
4343
tagName?: string;
4444
id?: string;
4545
classNames?: string[];
@@ -49,6 +49,7 @@ export default class SelectorItem {
4949
combinator?: SelectorCombinatorEnum;
5050
ignoreErrors?: boolean;
5151
}) {
52+
this.root = options?.scope ? options.scope[PropertySymbol.ownerDocument].documentElement : null;
5253
this.scope = options?.scope || null;
5354
this.tagName = options?.tagName || null;
5455
this.id = options?.id || null;
@@ -251,7 +252,7 @@ export default class SelectorItem {
251252
? { priorityWeight: 10 }
252253
: null;
253254
case 'root':
254-
return element[PropertySymbol.tagName] === 'HTML' ? { priorityWeight: 10 } : null;
255+
return this.root && element === this.root ? { priorityWeight: 10 } : null;
255256
case 'not':
256257
for (const selectorItem of pseudo.selectorItems!) {
257258
if (selectorItem.match(element)) {
@@ -385,7 +386,7 @@ export default class SelectorItem {
385386
? { priorityWeight: 10 }
386387
: null;
387388
case 'scope':
388-
return this.scope === element ? { priorityWeight: 10 } : null;
389+
return this.scope && this.scope === element ? { priorityWeight: 10 } : null;
389390
default:
390391
return null;
391392
}

packages/happy-dom/src/query-selector/SelectorParser.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import SelectorCombinatorEnum from './SelectorCombinatorEnum.js';
33
import DOMException from '../exception/DOMException.js';
44
import ISelectorPseudo from './ISelectorPseudo.js';
55
import Element from '../nodes/element/Element.js';
6-
import Document from '../nodes/document/Document.js';
76
import DocumentFragment from '../nodes/document-fragment/DocumentFragment.js';
87

98
/**
@@ -72,13 +71,16 @@ export default class SelectorParser {
7271
*
7372
* @param selector Selector.
7473
* @param options Options.
75-
* @param options.scope Scope.
74+
* @param [options.scope] Scope.
7675
* @param [options.ignoreErrors] Ignores errors.
7776
* @returns Selector item.
7877
*/
7978
public static getSelectorItem(
8079
selector: string,
81-
options?: { scope?: Element | Document | DocumentFragment; ignoreErrors?: boolean }
80+
options?: {
81+
scope?: Element | DocumentFragment;
82+
ignoreErrors?: boolean;
83+
}
8284
): SelectorItem {
8385
return this.getSelectorGroups(selector, options)[0][0];
8486
}
@@ -88,13 +90,16 @@ export default class SelectorParser {
8890
*
8991
* @param selector Selector.
9092
* @param options Options.
91-
* @param options.scope Scope.
93+
* @param [options.scope] Scope.
9294
* @param [options.ignoreErrors] Ignores errors.
9395
* @returns Selector groups.
9496
*/
9597
public static getSelectorGroups(
9698
selector: string,
97-
options?: { scope?: Element | Document | DocumentFragment; ignoreErrors?: boolean }
99+
options?: {
100+
scope?: Element | DocumentFragment;
101+
ignoreErrors?: boolean;
102+
}
98103
): Array<Array<SelectorItem>> {
99104
selector = selector.trim();
100105
const ignoreErrors = options?.ignoreErrors;
@@ -296,15 +301,18 @@ export default class SelectorParser {
296301
*
297302
* @param name Pseudo name.
298303
* @param args Pseudo arguments.
299-
* @param options Options.
300-
* @param options.scope Scope.
304+
* @param [options] Options.
305+
* @param [options.scope] Scope.
301306
* @param [options.ignoreErrors] Ignores errors.
302307
* @returns Pseudo.
303308
*/
304309
private static getPseudo(
305310
name: string,
306311
args: string | null | undefined,
307-
options?: { scope?: Element | Document | DocumentFragment; ignoreErrors?: boolean }
312+
options?: {
313+
scope?: Element | DocumentFragment;
314+
ignoreErrors?: boolean;
315+
}
308316
): ISelectorPseudo {
309317
const lowerName = name.toLowerCase();
310318

packages/happy-dom/test/query-selector/QuerySelector.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1369,6 +1369,14 @@ describe('QuerySelector', () => {
13691369
expect(links[1].textContent).toBe('Link 2');
13701370
});
13711371

1372+
it('Returns all elements for pseudo selector ":scope > child" in XML document', () => {
1373+
const xml = '<root><child>Content</child></root>';
1374+
const parser = new window.DOMParser();
1375+
const xmlDoc = parser.parseFromString(xml, 'application/xml');
1376+
expect(xmlDoc.querySelectorAll(':scope > child').length).toBe(1);
1377+
expect(xmlDoc.querySelectorAll(':root > child').length).toBe(1);
1378+
});
1379+
13721380
it('Returns all elements for pseudo selector ":root"', () => {
13731381
const root = document.querySelectorAll(':root');
13741382
expect(root.length).toBe(1);

0 commit comments

Comments
 (0)