@remote-dom/polyfill 1.5.1 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (304) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/README.md +51 -3
  3. package/build/cjs/Attr.cjs +33 -4
  4. package/build/cjs/CharacterData.cjs +10 -0
  5. package/build/cjs/ChildNode.cjs +79 -13
  6. package/build/cjs/Comment.cjs +1 -1
  7. package/build/cjs/CustomElementRegistry.cjs +27 -2
  8. package/build/cjs/CustomEvent.cjs +2 -2
  9. package/build/cjs/Document.cjs +56 -32
  10. package/build/cjs/DocumentFragment.cjs +6 -2
  11. package/build/cjs/Element.cjs +68 -8
  12. package/build/cjs/Event.cjs +29 -27
  13. package/build/cjs/EventTarget.cjs +128 -58
  14. package/build/cjs/HTMLTemplateElement.cjs +1 -0
  15. package/build/cjs/MutationObserver.cjs +167 -1
  16. package/build/cjs/NamedNodeMap.cjs +82 -45
  17. package/build/cjs/Node.cjs +47 -11
  18. package/build/cjs/NodeList.cjs +9 -1
  19. package/build/cjs/ParentNode.cjs +319 -77
  20. package/build/cjs/SVGElement.cjs +1 -1
  21. package/build/cjs/Text.cjs +1 -1
  22. package/build/cjs/Window.cjs +131 -38
  23. package/build/cjs/attribute-reactions.cjs +20 -0
  24. package/build/cjs/constants.cjs +48 -24
  25. package/build/cjs/custom-element-reactions.cjs +52 -0
  26. package/build/cjs/dom-exception.cjs +15 -0
  27. package/build/cjs/hook-effects.cjs +51 -0
  28. package/build/cjs/names.cjs +61 -0
  29. package/build/cjs/selectors.cjs +213 -122
  30. package/build/cjs/serialization.cjs +168 -65
  31. package/build/cjs/shared.cjs +177 -28
  32. package/build/esm/Attr.mjs +34 -5
  33. package/build/esm/CharacterData.mjs +10 -0
  34. package/build/esm/ChildNode.mjs +76 -15
  35. package/build/esm/Comment.mjs +2 -2
  36. package/build/esm/CustomElementRegistry.mjs +27 -2
  37. package/build/esm/CustomEvent.mjs +2 -2
  38. package/build/esm/Document.mjs +60 -35
  39. package/build/esm/DocumentFragment.mjs +7 -3
  40. package/build/esm/Element.mjs +69 -9
  41. package/build/esm/Event.mjs +27 -27
  42. package/build/esm/EventTarget.mjs +130 -60
  43. package/build/esm/HTMLTemplateElement.mjs +2 -1
  44. package/build/esm/MutationObserver.mjs +166 -2
  45. package/build/esm/NamedNodeMap.mjs +83 -46
  46. package/build/esm/Node.mjs +48 -12
  47. package/build/esm/NodeList.mjs +9 -1
  48. package/build/esm/ParentNode.mjs +322 -81
  49. package/build/esm/SVGElement.mjs +2 -2
  50. package/build/esm/Text.mjs +2 -2
  51. package/build/esm/Window.mjs +132 -39
  52. package/build/esm/attribute-reactions.mjs +16 -0
  53. package/build/esm/constants.mjs +28 -23
  54. package/build/esm/custom-element-reactions.mjs +47 -0
  55. package/build/esm/dom-exception.mjs +11 -0
  56. package/build/esm/hook-effects.mjs +46 -0
  57. package/build/esm/names.mjs +54 -0
  58. package/build/esm/selectors.mjs +202 -124
  59. package/build/esm/serialization.mjs +169 -66
  60. package/build/esm/shared.mjs +173 -30
  61. package/build/esnext/Attr.esnext +45 -5
  62. package/build/esnext/CharacterData.esnext +10 -0
  63. package/build/esnext/ChildNode.esnext +84 -15
  64. package/build/esnext/Comment.esnext +2 -2
  65. package/build/esnext/CustomElementRegistry.esnext +42 -0
  66. package/build/esnext/CustomEvent.esnext +2 -2
  67. package/build/esnext/Document.esnext +69 -32
  68. package/build/esnext/DocumentFragment.esnext +6 -2
  69. package/build/esnext/Element.esnext +78 -9
  70. package/build/esnext/Event.esnext +36 -37
  71. package/build/esnext/EventTarget.esnext +133 -61
  72. package/build/esnext/HTMLTemplateElement.esnext +3 -1
  73. package/build/esnext/MutationObserver.esnext +163 -2
  74. package/build/esnext/NamedNodeMap.esnext +110 -42
  75. package/build/esnext/Node.esnext +50 -12
  76. package/build/esnext/NodeList.esnext +1 -1
  77. package/build/esnext/ParentNode.esnext +352 -79
  78. package/build/esnext/SVGElement.esnext +2 -2
  79. package/build/esnext/Text.esnext +2 -2
  80. package/build/esnext/Window.esnext +120 -16
  81. package/build/esnext/attribute-reactions.esnext +21 -0
  82. package/build/esnext/constants.esnext +34 -21
  83. package/build/esnext/custom-element-reactions.esnext +49 -0
  84. package/build/esnext/dom-exception.esnext +11 -0
  85. package/build/esnext/hook-effects.esnext +46 -0
  86. package/build/esnext/names.esnext +61 -0
  87. package/build/esnext/selectors.esnext +202 -100
  88. package/build/esnext/serialization.esnext +171 -48
  89. package/build/esnext/shared.esnext +181 -34
  90. package/build/tsconfig.tsbuildinfo +1 -1
  91. package/build/typescript/Attr.d.ts +6 -4
  92. package/build/typescript/Attr.d.ts.map +1 -1
  93. package/build/typescript/CharacterData.d.ts.map +1 -1
  94. package/build/typescript/ChildNode.d.ts +5 -0
  95. package/build/typescript/ChildNode.d.ts.map +1 -1
  96. package/build/typescript/ClipboardEvent.d.ts.map +1 -1
  97. package/build/typescript/Comment.d.ts +2 -2
  98. package/build/typescript/Comment.d.ts.map +1 -1
  99. package/build/typescript/CustomElementRegistry.d.ts +1 -0
  100. package/build/typescript/CustomElementRegistry.d.ts.map +1 -1
  101. package/build/typescript/CustomEvent.d.ts.map +1 -1
  102. package/build/typescript/Document.d.ts +10 -5
  103. package/build/typescript/Document.d.ts.map +1 -1
  104. package/build/typescript/DocumentFragment.d.ts +2 -1
  105. package/build/typescript/DocumentFragment.d.ts.map +1 -1
  106. package/build/typescript/Element.d.ts +16 -7
  107. package/build/typescript/Element.d.ts.map +1 -1
  108. package/build/typescript/ErrorEvent.d.ts.map +1 -1
  109. package/build/typescript/Event.d.ts +9 -10
  110. package/build/typescript/Event.d.ts.map +1 -1
  111. package/build/typescript/EventTarget.d.ts +20 -3
  112. package/build/typescript/EventTarget.d.ts.map +1 -1
  113. package/build/typescript/FocusEvent.d.ts.map +1 -1
  114. package/build/typescript/HTMLTemplateElement.d.ts.map +1 -1
  115. package/build/typescript/MutationObserver.d.ts +42 -0
  116. package/build/typescript/MutationObserver.d.ts.map +1 -1
  117. package/build/typescript/NamedNodeMap.d.ts +5 -3
  118. package/build/typescript/NamedNodeMap.d.ts.map +1 -1
  119. package/build/typescript/Node.d.ts +4 -3
  120. package/build/typescript/Node.d.ts.map +1 -1
  121. package/build/typescript/NodeList.d.ts +3 -2
  122. package/build/typescript/NodeList.d.ts.map +1 -1
  123. package/build/typescript/ParentNode.d.ts +29 -10
  124. package/build/typescript/ParentNode.d.ts.map +1 -1
  125. package/build/typescript/PromiseRejectionEvent.d.ts.map +1 -1
  126. package/build/typescript/SVGElement.d.ts +1 -1
  127. package/build/typescript/SVGElement.d.ts.map +1 -1
  128. package/build/typescript/Text.d.ts +1 -1
  129. package/build/typescript/Text.d.ts.map +1 -1
  130. package/build/typescript/ToggleEvent.d.ts.map +1 -1
  131. package/build/typescript/Window.d.ts +5 -1
  132. package/build/typescript/Window.d.ts.map +1 -1
  133. package/build/typescript/attribute-reactions.d.ts +4 -0
  134. package/build/typescript/attribute-reactions.d.ts.map +1 -0
  135. package/build/typescript/constants.d.ts +27 -18
  136. package/build/typescript/constants.d.ts.map +1 -1
  137. package/build/typescript/custom-element-reactions.d.ts +6 -0
  138. package/build/typescript/custom-element-reactions.d.ts.map +1 -0
  139. package/build/typescript/dom-exception.d.ts +2 -0
  140. package/build/typescript/dom-exception.d.ts.map +1 -0
  141. package/build/typescript/extensions.d.ts +4 -0
  142. package/build/typescript/extensions.d.ts.map +1 -0
  143. package/build/typescript/hook-effects.d.ts +6 -0
  144. package/build/typescript/hook-effects.d.ts.map +1 -0
  145. package/build/typescript/hooks.d.ts +3 -3
  146. package/build/typescript/hooks.d.ts.map +1 -1
  147. package/build/typescript/index.d.ts +1 -0
  148. package/build/typescript/index.d.ts.map +1 -1
  149. package/build/typescript/names.d.ts +11 -0
  150. package/build/typescript/names.d.ts.map +1 -0
  151. package/build/typescript/selectors.d.ts +21 -22
  152. package/build/typescript/selectors.d.ts.map +1 -1
  153. package/build/typescript/serialization.d.ts.map +1 -1
  154. package/build/typescript/shared.d.ts +10 -0
  155. package/build/typescript/shared.d.ts.map +1 -1
  156. package/build/typescript/tests/ChildNode.test.d.ts +2 -0
  157. package/build/typescript/tests/ChildNode.test.d.ts.map +1 -0
  158. package/build/typescript/tests/MutationObserver.test.d.ts +2 -0
  159. package/build/typescript/tests/MutationObserver.test.d.ts.map +1 -0
  160. package/build/typescript/tests/adopt-inserted-subtrees.test.d.ts +2 -0
  161. package/build/typescript/tests/adopt-inserted-subtrees.test.d.ts.map +1 -0
  162. package/build/typescript/tests/attr-mutation.test.d.ts +2 -0
  163. package/build/typescript/tests/attr-mutation.test.d.ts.map +1 -0
  164. package/build/typescript/tests/child-node-before-after.test.d.ts +2 -0
  165. package/build/typescript/tests/child-node-before-after.test.d.ts.map +1 -0
  166. package/build/typescript/tests/child-node-replace-with.test.d.ts +2 -0
  167. package/build/typescript/tests/child-node-replace-with.test.d.ts.map +1 -0
  168. package/build/typescript/tests/clone-import-namespaces.test.d.ts +2 -0
  169. package/build/typescript/tests/clone-import-namespaces.test.d.ts.map +1 -0
  170. package/build/typescript/tests/constants.test.d.ts +2 -0
  171. package/build/typescript/tests/constants.test.d.ts.map +1 -0
  172. package/build/typescript/tests/create-element-namespaces.test.d.ts +2 -0
  173. package/build/typescript/tests/create-element-namespaces.test.d.ts.map +1 -0
  174. package/build/typescript/tests/create-text-node.test.d.ts +2 -0
  175. package/build/typescript/tests/create-text-node.test.d.ts.map +1 -0
  176. package/build/typescript/tests/custom-element-reactions.test.d.ts +2 -0
  177. package/build/typescript/tests/custom-element-reactions.test.d.ts.map +1 -0
  178. package/build/typescript/tests/custom-element-registry.test.d.ts +2 -0
  179. package/build/typescript/tests/custom-element-registry.test.d.ts.map +1 -0
  180. package/build/typescript/tests/custom-event.test.d.ts +2 -0
  181. package/build/typescript/tests/custom-event.test.d.ts.map +1 -0
  182. package/build/typescript/tests/document-clone-import-contract.test.d.ts +2 -0
  183. package/build/typescript/tests/document-clone-import-contract.test.d.ts.map +1 -0
  184. package/build/typescript/tests/document.test.d.ts +2 -0
  185. package/build/typescript/tests/document.test.d.ts.map +1 -0
  186. package/build/typescript/tests/dom-mutation-selector-errors.test.d.ts +2 -0
  187. package/build/typescript/tests/dom-mutation-selector-errors.test.d.ts.map +1 -0
  188. package/build/typescript/tests/event-dispatch.test.d.ts +2 -0
  189. package/build/typescript/tests/event-dispatch.test.d.ts.map +1 -0
  190. package/build/typescript/tests/event-target.test.d.ts +2 -0
  191. package/build/typescript/tests/event-target.test.d.ts.map +1 -0
  192. package/build/typescript/tests/event.test.d.ts +2 -0
  193. package/build/typescript/tests/event.test.d.ts.map +1 -0
  194. package/build/typescript/tests/extensions.test.d.ts +2 -0
  195. package/build/typescript/tests/extensions.test.d.ts.map +1 -0
  196. package/build/typescript/tests/get-elements-by-tag-name.test.d.ts +2 -0
  197. package/build/typescript/tests/get-elements-by-tag-name.test.d.ts.map +1 -0
  198. package/build/typescript/tests/getElementsByClassName.test.d.ts +2 -0
  199. package/build/typescript/tests/getElementsByClassName.test.d.ts.map +1 -0
  200. package/build/typescript/tests/hooks.test.d.ts +2 -0
  201. package/build/typescript/tests/hooks.test.d.ts.map +1 -0
  202. package/build/typescript/tests/inner-html-parsing.test.d.ts +2 -0
  203. package/build/typescript/tests/inner-html-parsing.test.d.ts.map +1 -0
  204. package/build/typescript/tests/is-default-namespace.test.d.ts +2 -0
  205. package/build/typescript/tests/is-default-namespace.test.d.ts.map +1 -0
  206. package/build/typescript/tests/name-and-namespace-normalization.test.d.ts +2 -0
  207. package/build/typescript/tests/name-and-namespace-normalization.test.d.ts.map +1 -0
  208. package/build/typescript/tests/named-node-map.test.d.ts +2 -0
  209. package/build/typescript/tests/named-node-map.test.d.ts.map +1 -0
  210. package/build/typescript/tests/namespaced-attribute-reactions.test.d.ts +2 -0
  211. package/build/typescript/tests/namespaced-attribute-reactions.test.d.ts.map +1 -0
  212. package/build/typescript/tests/node.test.d.ts +2 -0
  213. package/build/typescript/tests/node.test.d.ts.map +1 -0
  214. package/build/typescript/tests/parent-node-mutation-return-values.test.d.ts +2 -0
  215. package/build/typescript/tests/parent-node-mutation-return-values.test.d.ts.map +1 -0
  216. package/build/typescript/tests/parent-node-mutation-validation.test.d.ts +2 -0
  217. package/build/typescript/tests/parent-node-mutation-validation.test.d.ts.map +1 -0
  218. package/build/typescript/tests/parent-node.test.d.ts +2 -0
  219. package/build/typescript/tests/parent-node.test.d.ts.map +1 -0
  220. package/build/typescript/tests/serialization-template.test.d.ts +2 -0
  221. package/build/typescript/tests/serialization-template.test.d.ts.map +1 -0
  222. package/build/typescript/tests/slot-attribute.test.d.ts +2 -0
  223. package/build/typescript/tests/slot-attribute.test.d.ts.map +1 -0
  224. package/build/typescript/tests/text-content.test.d.ts +2 -0
  225. package/build/typescript/tests/text-content.test.d.ts.map +1 -0
  226. package/build/typescript/tests/toggle-attribute.test.d.ts +2 -0
  227. package/build/typescript/tests/toggle-attribute.test.d.ts.map +1 -0
  228. package/build/typescript/tests/wide-tree-traversal.test.d.ts +2 -0
  229. package/build/typescript/tests/wide-tree-traversal.test.d.ts.map +1 -0
  230. package/build/typescript/tests/window-set-global.test.d.ts +2 -0
  231. package/build/typescript/tests/window-set-global.test.d.ts.map +1 -0
  232. package/package.json +1 -1
  233. package/source/Attr.ts +60 -8
  234. package/source/CharacterData.ts +13 -0
  235. package/source/ChildNode.ts +107 -14
  236. package/source/Comment.ts +2 -2
  237. package/source/CustomElementRegistry.ts +56 -0
  238. package/source/CustomEvent.ts +2 -2
  239. package/source/Document.ts +104 -38
  240. package/source/DocumentFragment.ts +12 -2
  241. package/source/Element.ts +129 -14
  242. package/source/Event.ts +34 -32
  243. package/source/EventTarget.ts +203 -79
  244. package/source/HTMLTemplateElement.ts +2 -1
  245. package/source/MutationObserver.ts +279 -1
  246. package/source/NamedNodeMap.ts +141 -60
  247. package/source/Node.ts +70 -14
  248. package/source/NodeList.ts +13 -3
  249. package/source/ParentNode.ts +500 -88
  250. package/source/SVGElement.ts +2 -2
  251. package/source/Text.ts +2 -2
  252. package/source/Window.ts +156 -17
  253. package/source/attribute-reactions.ts +32 -0
  254. package/source/constants.ts +40 -17
  255. package/source/custom-element-reactions.ts +64 -0
  256. package/source/dom-exception.ts +14 -0
  257. package/source/extensions.ts +4 -0
  258. package/source/hook-effects.ts +56 -0
  259. package/source/hooks.ts +11 -3
  260. package/source/index.ts +1 -0
  261. package/source/names.ts +94 -0
  262. package/source/selectors.ts +301 -138
  263. package/source/serialization.ts +224 -66
  264. package/source/shared.ts +269 -40
  265. package/source/tests/ChildNode.test.ts +178 -0
  266. package/source/tests/MutationObserver.test.ts +303 -0
  267. package/source/tests/adopt-inserted-subtrees.test.ts +584 -0
  268. package/source/tests/attr-mutation.test.ts +112 -0
  269. package/source/tests/child-node-before-after.test.ts +421 -0
  270. package/source/tests/child-node-replace-with.test.ts +251 -0
  271. package/source/tests/clone-import-namespaces.test.ts +365 -0
  272. package/source/tests/constants.test.ts +46 -0
  273. package/source/tests/create-element-namespaces.test.ts +142 -0
  274. package/source/tests/create-text-node.test.ts +38 -0
  275. package/source/tests/custom-element-reactions.test.ts +600 -0
  276. package/source/tests/custom-element-registry.test.ts +137 -0
  277. package/source/tests/custom-event.test.ts +26 -0
  278. package/source/tests/document-clone-import-contract.test.ts +77 -0
  279. package/source/tests/document.test.ts +122 -0
  280. package/source/tests/dom-mutation-selector-errors.test.ts +484 -0
  281. package/source/tests/event-dispatch.test.ts +270 -0
  282. package/source/tests/event-target.test.ts +208 -0
  283. package/source/tests/event.test.ts +98 -0
  284. package/source/tests/extensions.test.ts +159 -0
  285. package/source/tests/get-elements-by-tag-name.test.ts +79 -0
  286. package/source/tests/getElementsByClassName.test.ts +109 -0
  287. package/source/tests/hooks.test.ts +43 -0
  288. package/source/tests/inner-html-parsing.test.ts +461 -0
  289. package/source/tests/is-default-namespace.test.ts +111 -0
  290. package/source/tests/name-and-namespace-normalization.test.ts +319 -0
  291. package/source/tests/named-node-map.test.ts +122 -0
  292. package/source/tests/namespaced-attribute-reactions.test.ts +141 -0
  293. package/source/tests/node.test.ts +56 -0
  294. package/source/tests/parent-node-mutation-return-values.test.ts +63 -0
  295. package/source/tests/parent-node-mutation-validation.test.ts +362 -0
  296. package/source/tests/parent-node.test.ts +169 -0
  297. package/source/tests/selectors.test.ts +533 -22
  298. package/source/tests/serialization-template.test.ts +232 -0
  299. package/source/tests/slot-attribute.test.ts +133 -0
  300. package/source/tests/text-content.test.ts +440 -0
  301. package/source/tests/toggle-attribute.test.ts +113 -0
  302. package/source/tests/wide-tree-traversal.test.ts +699 -0
  303. package/source/tests/window-set-global.test.ts +321 -0
  304. package/tsconfig.json +1 -1
@@ -1,7 +1,33 @@
1
1
  import {Window} from '../index.ts';
2
- import {parseSelector} from '../selectors.ts';
3
-
4
- import {describe, it, expect, beforeEach} from 'vitest';
2
+ import {NodeList} from '../NodeList.ts';
3
+ import type {Element as PolyfillElement} from '../Element.ts';
4
+ import {
5
+ MATCHER_CLASS,
6
+ MATCHER_ELEMENT,
7
+ MATCHER_ID,
8
+ MATCHER_UNKNOWN,
9
+ parseSelector,
10
+ querySelector,
11
+ querySelectorAll,
12
+ } from '../selectors.ts';
13
+ import type {ParentNode} from '../ParentNode.ts';
14
+
15
+ const MatcherType = {
16
+ Unknown: MATCHER_UNKNOWN,
17
+ Element: MATCHER_ELEMENT,
18
+ Id: MATCHER_ID,
19
+ Class: MATCHER_CLASS,
20
+ } as const;
21
+
22
+ const ASCII_WHITESPACE = [
23
+ ['SPACE', ' '],
24
+ ['TAB', '\t'],
25
+ ['LINE FEED', '\n'],
26
+ ['FORM FEED', '\f'],
27
+ ['CARRIAGE RETURN', '\r'],
28
+ ] as const;
29
+
30
+ import {describe, it, expect, expectTypeOf, beforeEach} from 'vitest';
5
31
 
6
32
  describe('selector parsing and matching', () => {
7
33
  beforeEach(() => {
@@ -41,6 +67,19 @@ describe('selector parsing and matching', () => {
41
67
  });
42
68
  });
43
69
 
70
+ it('keeps non-ASCII whitespace in class selector names', () => {
71
+ const parts = parseSelector('.left\u00a0right');
72
+
73
+ expect(parts).toHaveLength(1);
74
+ expect(parts[0]!.matchers).toEqual([
75
+ {
76
+ type: MatcherType.Class,
77
+ name: 'left\u00a0right',
78
+ value: 'left\u00a0right',
79
+ },
80
+ ]);
81
+ });
82
+
44
83
  it('parses attribute selectors without values', () => {
45
84
  const parts = parseSelector('[disabled]');
46
85
  expect(parts).toHaveLength(1);
@@ -61,14 +100,10 @@ describe('selector parsing and matching', () => {
61
100
  });
62
101
  });
63
102
 
64
- it('parses pseudo-class selectors', () => {
65
- const parts = parseSelector(':hover');
66
- expect(parts).toHaveLength(1);
67
- expect(parts[0]!.matchers[0]!).toMatchObject({
68
- type: 5, // MatcherType.Pseudo
69
- name: 'hover',
70
- value: undefined,
71
- });
103
+ it('rejects unsupported pseudo-class selectors', () => {
104
+ expect(() => parseSelector(':hover')).toThrowError(
105
+ expect.objectContaining({name: 'SyntaxError'}),
106
+ );
72
107
  });
73
108
 
74
109
  it('parses function selectors', () => {
@@ -91,6 +126,36 @@ describe('selector parsing and matching', () => {
91
126
  });
92
127
  });
93
128
 
129
+ it.each([
130
+ [':not(:has(.missing))', ':has(.missing)'],
131
+ [':has(span:not(.missing))', 'span:not(.missing)'],
132
+ [':has(> .hit)', '> .hit'],
133
+ [':has([data-label=")value("])', '[data-label=")value("]'],
134
+ ])('parses the balanced argument in %s', (selector, value) => {
135
+ expect(parseSelector(selector)[0]!.matchers[0]!.value).toBe(value);
136
+ });
137
+
138
+ it.each([':has(:has(.active))', ':has(:not(:has(.active)))'])(
139
+ 'rejects nested :has() in %s',
140
+ (selector) => {
141
+ expect(() => parseSelector(selector)).toThrow();
142
+ },
143
+ );
144
+
145
+ it.each([
146
+ [':HAS(div)', 6, 'has', 'div'],
147
+ [':Not(.Hidden)', 6, 'not', '.Hidden'],
148
+ ])(
149
+ 'ASCII-lowercases only the pseudo-class name in %s',
150
+ (selector, type, name, value) => {
151
+ expect(parseSelector(selector)[0]!.matchers[0]!).toMatchObject({
152
+ type,
153
+ name,
154
+ value,
155
+ });
156
+ },
157
+ );
158
+
94
159
  it('parses compound selectors', () => {
95
160
  const parts = parseSelector('div.myclass#myid[type="button"]');
96
161
  expect(parts).toHaveLength(1);
@@ -109,13 +174,16 @@ describe('selector parsing and matching', () => {
109
174
  expect(parts[1]!.matchers[0]!.name).toBe('span');
110
175
  });
111
176
 
112
- it('parses descendant combinator', () => {
113
- const parts = parseSelector('div span');
114
- expect(parts).toHaveLength(2);
115
- expect(parts[0]!.combinator).toBe(0); // Combinator.Descendant
116
- expect(parts[0]!.matchers[0]!.name).toBe('div');
117
- expect(parts[1]!.matchers[0]!.name).toBe('span');
118
- });
177
+ it.each(ASCII_WHITESPACE)(
178
+ 'parses %s as a descendant combinator',
179
+ (_name, whitespace) => {
180
+ const parts = parseSelector(`div${whitespace}span`);
181
+ expect(parts).toHaveLength(2);
182
+ expect(parts[0]!.combinator).toBe(0); // Combinator.Descendant
183
+ expect(parts[0]!.matchers[0]!.name).toBe('div');
184
+ expect(parts[1]!.matchers[0]!.name).toBe('span');
185
+ },
186
+ );
119
187
 
120
188
  it('parses adjacent sibling combinator', () => {
121
189
  const parts = parseSelector('h1 + p');
@@ -175,10 +243,29 @@ describe('selector parsing and matching', () => {
175
243
  </div>
176
244
  </footer>
177
245
  `;
246
+ container.querySelector('article')!.setAttribute('DATA-STATE', 'Ready');
247
+ container.querySelector('.highlight')!.setAttribute('data-label', 'a)b');
248
+ });
249
+
250
+ it('types instance query results as elements', () => {
251
+ const matches = new Window().document
252
+ .createElement('div')
253
+ .querySelectorAll('p');
254
+
255
+ expectTypeOf(matches).toEqualTypeOf<NodeList<PolyfillElement>>();
256
+ expectTypeOf(matches[0]!).toEqualTypeOf<PolyfillElement>();
257
+ expectTypeOf(matches.item(0)).toEqualTypeOf<PolyfillElement | null>();
178
258
  });
179
259
 
180
- it('selects by element name', () => {
181
- const articles = container.querySelectorAll('article');
260
+ it('returns the polyfill collection with item() access', () => {
261
+ const matches = container.querySelectorAll('.text');
262
+
263
+ expect(matches).toBeInstanceOf(NodeList);
264
+ expect(matches.item(0)).toBe(matches[0]);
265
+ });
266
+
267
+ it('selects HTML element names case-insensitively', () => {
268
+ const articles = container.querySelectorAll('ARTICLE');
182
269
  expect(articles).toHaveLength(1);
183
270
  expect(articles[0]!.getAttribute('class')).toBe('post');
184
271
 
@@ -186,6 +273,69 @@ describe('selector parsing and matching', () => {
186
273
  expect(paragraphs).toHaveLength(3);
187
274
  });
188
275
 
276
+ it('returns a static NodeList-compatible collection', () => {
277
+ const matches = container.querySelectorAll('.text');
278
+ const text = [...matches].map((element) => element.textContent?.trim());
279
+ const visited: string[] = [];
280
+
281
+ matches.forEach((element) => visited.push(element.textContent?.trim()!));
282
+ container
283
+ .appendChild(document.createElement('p'))
284
+ .setAttribute('class', 'text');
285
+
286
+ expect(container.querySelectorAll('.text')).toHaveLength(4);
287
+ expect(matches).toBeInstanceOf(NodeList);
288
+ expect(matches).toHaveLength(3);
289
+ expect(matches.item(0)).toBe(matches[0]);
290
+ expect(matches.item(-1)).toBeNull();
291
+ expect(matches.item(matches.length)).toBeNull();
292
+ expect(
293
+ [...matches].map((element) => element.textContent?.trim()),
294
+ ).toEqual(text);
295
+ expect(visited).toEqual(text);
296
+ });
297
+
298
+ it('does not fold stored createElementNS HTML names', () => {
299
+ const uppercase = document.createElementNS(
300
+ 'http://www.w3.org/1999/xhtml',
301
+ 'I',
302
+ );
303
+ container.appendChild(uppercase);
304
+
305
+ expect(container.querySelectorAll('i')).toHaveLength(0);
306
+ expect(container.querySelectorAll('I')).toHaveLength(0);
307
+
308
+ const normalized = document.createElement('I');
309
+ container.appendChild(normalized);
310
+
311
+ expect(container.querySelectorAll('i')).toEqual([normalized]);
312
+ expect(container.querySelectorAll('I')).toEqual([normalized]);
313
+ });
314
+
315
+ it.each(['\u1680', '\ufeff'])(
316
+ 'does not treat %j as CSS whitespace',
317
+ (separator) => {
318
+ const ordinary = document.createElement('article');
319
+ container.appendChild(ordinary);
320
+ const selector = `${separator}article`;
321
+
322
+ expect(container.querySelector(selector)).toBeNull();
323
+
324
+ const literal = document.createElement(selector);
325
+ container.appendChild(literal);
326
+ expect(container.querySelector(selector)).toBe(literal);
327
+ expect(container.querySelectorAll(selector)).toEqual([literal]);
328
+
329
+ const classed = document.createElement('div');
330
+ classed.setAttribute('class', `first${separator}second`);
331
+ container.appendChild(classed);
332
+ expect(container.querySelector('.first')).toBeNull();
333
+ expect(container.querySelector(`.first${separator}second`)).toBe(
334
+ classed,
335
+ );
336
+ },
337
+ );
338
+
189
339
  it('selects by ID', () => {
190
340
  const main = container.querySelector('#main-post');
191
341
  expect(main?.tagName.toLowerCase()).toBe('article');
@@ -199,6 +349,33 @@ describe('selector parsing and matching', () => {
199
349
  expect(hidden?.textContent?.trim()).toBe('Hidden paragraph');
200
350
  });
201
351
 
352
+ it('keeps non-ASCII whitespace in class names', () => {
353
+ const literal = document.createElement('div');
354
+ literal.setAttribute('class', 'left\u00a0right');
355
+
356
+ const separated = document.createElement('div');
357
+ separated.setAttribute('class', 'left right');
358
+
359
+ container.append(literal, separated);
360
+
361
+ const literalMatches = container.querySelectorAll('.left\u00a0right');
362
+ expect(literalMatches).toHaveLength(1);
363
+ expect(literalMatches[0]).toBe(literal);
364
+ expect(container.querySelector('.left')).toBe(separated);
365
+ });
366
+
367
+ it('selects Unicode and double-hyphen identifiers', () => {
368
+ const article = container.querySelector('article')!;
369
+ const unicode = document.createElement('span');
370
+ unicode.setAttribute('class', 'é');
371
+ unicode.id = '--foo';
372
+ article.appendChild(unicode);
373
+
374
+ expect(container.querySelector('.é')).toBe(unicode);
375
+ expect(container.querySelector('#--foo')).toBe(unicode);
376
+ expect(container.querySelector('article:has(> .é)')).toBe(article);
377
+ });
378
+
202
379
  it('selects by attribute', () => {
203
380
  const links = container.querySelectorAll('[href]');
204
381
  expect(links).toHaveLength(2);
@@ -206,6 +383,23 @@ describe('selector parsing and matching', () => {
206
383
  const activeLinks = container.querySelectorAll('[href="#"]');
207
384
  expect(activeLinks).toHaveLength(2);
208
385
  });
386
+ it('selects by an unquoted exact attribute value', () => {
387
+ expect(container.querySelectorAll('[class=content]')).toHaveLength(2);
388
+ });
389
+
390
+ it('rejects a different unquoted exact attribute value', () => {
391
+ expect(container.querySelector('[class=contents]')).toBeNull();
392
+ });
393
+
394
+ it.each([
395
+ ['quoted', 'article[data-state="Ready"]', true],
396
+ ['unquoted', 'article[data-state=Ready]', true],
397
+ ['normalized HTML name', 'article[DATA-STATE=Ready]', true],
398
+ ['quoted case-sensitive value', 'article[data-state="ready"]', false],
399
+ ['unquoted case-sensitive value', 'article[data-state=ready]', false],
400
+ ])('matches %s exact attribute equality', (_name, selector, matches) => {
401
+ expect(container.querySelector(selector) != null).toBe(matches);
402
+ });
209
403
 
210
404
  it('selects by compound selectors', () => {
211
405
  const activeLink = container.querySelector('a.link.active');
@@ -230,6 +424,33 @@ describe('selector parsing and matching', () => {
230
424
  const directArticleChildren = container.querySelectorAll('article > h1');
231
425
  expect(directArticleChildren).toHaveLength(1);
232
426
  });
427
+ it('preserves the matched ancestor through chained combinators', () => {
428
+ const activeLink = container.querySelector('article > .sidebar a.active');
429
+
430
+ expect(activeLink?.textContent?.trim()).toBe('Active Link');
431
+ });
432
+
433
+ it('does not restart chained combinators from the leaf', () => {
434
+ expect(container.querySelector('li > .sidebar a.active')).toBeNull();
435
+ });
436
+
437
+ it.each([
438
+ ['child', 'article > .sidebar > .nav > li > a.active'],
439
+ ['descendant', 'article .sidebar .nav li a.active'],
440
+ ['adjacent sibling', 'h1 + .content + .sidebar'],
441
+ ['general sibling', 'h1 ~ .content ~ .sidebar'],
442
+ ])('preserves state across a 3+ part %s chain', (_name, selector) => {
443
+ expect(container.querySelector(selector)).not.toBeNull();
444
+ });
445
+
446
+ it.each([
447
+ ['child', 'article > .sidebar > li > a.active'],
448
+ ['descendant', 'footer .sidebar .nav a.active'],
449
+ ['adjacent sibling', 'h1 + .sidebar + .content'],
450
+ ['general sibling', '.sidebar ~ .content ~ footer'],
451
+ ])('rejects an invalid 3+ part %s chain', (_name, selector) => {
452
+ expect(container.querySelector(selector)).toBeNull();
453
+ });
233
454
 
234
455
  it('selects with adjacent sibling combinator', () => {
235
456
  const titleSibling = container.querySelector('h1 + div');
@@ -251,6 +472,103 @@ describe('selector parsing and matching', () => {
251
472
  const hasActiveLink = container.querySelector(':has(.active)');
252
473
  expect(hasActiveLink).toBeTruthy();
253
474
  });
475
+ it('matches :has() against descendants', () => {
476
+ expect(container.querySelector('article:has(.active)')?.id).toBe(
477
+ 'main-post',
478
+ );
479
+ });
480
+
481
+ it('does not match :has() without a matching descendant', () => {
482
+ expect(container.querySelector('footer:has(.active)')).toBeNull();
483
+ });
484
+
485
+ it.each([
486
+ ['direct child', 'article:has(> h1)'],
487
+ ['child with descendant', 'article:has(> .content .highlight)'],
488
+ ['adjacent sibling', 'article:has(+ footer)'],
489
+ ['general sibling', 'article:has(~ footer)'],
490
+ ])(
491
+ 'matches scoped :has() with a leading %s relation',
492
+ (_name, selector) => {
493
+ document.body.appendChild(container);
494
+ expect(document.body.querySelector(selector)?.id).toBe('main-post');
495
+ },
496
+ );
497
+
498
+ it.each([
499
+ ['outside ancestor', 'article:has(body .active)'],
500
+ ['scope as explicit ancestor', 'article:has(article .active)'],
501
+ ['scope id as explicit ancestor', 'article:has(#main-post .active)'],
502
+ ['non-child descendant', 'article:has(> .active)'],
503
+ ['wrong adjacent direction', 'footer:has(+ article)'],
504
+ ])('rejects :has() with %s', (_name, selector) => {
505
+ document.body.appendChild(container);
506
+ expect(document.body.querySelector(selector)).toBeNull();
507
+ });
508
+
509
+ it.each(['article:has(:has(.active))', 'article:has(:not(:has(.active)))'])(
510
+ 'rejects nested :has() before walking candidates in %s',
511
+ (selector) => {
512
+ const empty = document.createElement('div');
513
+ for (const root of [empty, container]) {
514
+ expect(() => root.querySelector(selector)).toThrowError(
515
+ expect.objectContaining({name: 'SyntaxError'}),
516
+ );
517
+ expect(() => root.querySelectorAll(selector)).toThrowError(
518
+ expect.objectContaining({name: 'SyntaxError'}),
519
+ );
520
+ }
521
+ },
522
+ );
523
+
524
+ it('preserves valid :has() nesting controls', () => {
525
+ expect(() => parseSelector(':not(:has(.missing))')).not.toThrow();
526
+ expect(() => parseSelector(':has(span:not(.missing))')).not.toThrow();
527
+ expect(() => parseSelector(':has(.active):has(h1)')).not.toThrow();
528
+ expect(() =>
529
+ parseSelector(':has([data-label=":has(.active)"])'),
530
+ ).not.toThrow();
531
+ });
532
+
533
+ it('matches nested functional pseudo-classes', () => {
534
+ expect(container.querySelector('article:not(:has(.missing))')?.id).toBe(
535
+ 'main-post',
536
+ );
537
+ expect(container.querySelector('article:not(:has(.active))')).toBeNull();
538
+ expect(
539
+ container.querySelector('article:has(span:not(.missing))')?.id,
540
+ ).toBe('main-post');
541
+ expect(
542
+ container.querySelector('footer:has(span:not(.missing))'),
543
+ ).toBeNull();
544
+ });
545
+
546
+ it.each([
547
+ ['uppercase simple function', 'article:HAS(.active)'],
548
+ ['mixed-case simple function', 'article:Has(.active)'],
549
+ ['mixed-case negation', 'article:NOT(.footer)'],
550
+ ['nested functions', 'article:NOT(:HAS(.missing))'],
551
+ ['nested descendant function', 'article:HAS(span:NoT(.missing))'],
552
+ ['relative child function', 'article:HAS(> h1)'],
553
+ ['relative sibling function', 'article:hAs(+ footer)'],
554
+ ])('matches %s names ASCII-case-insensitively', (_name, selector) => {
555
+ expect(container.querySelector(selector)?.id).toBe('main-post');
556
+ });
557
+
558
+ it.each([
559
+ ['mixed-case negation result', 'article:NoT(.post)'],
560
+ ['class name', 'article:HAS(.ACTIVE)'],
561
+ ['ID', '#MAIN-POST'],
562
+ ['attribute value', 'article:HAS([data-label="A)B"])'],
563
+ ])('does not fold the %s', (_name, selector) => {
564
+ expect(container.querySelector(selector)).toBeNull();
565
+ });
566
+
567
+ it('keeps quoted attribute values balanced inside :has()', () => {
568
+ expect(
569
+ container.querySelector('article:has([data-label="a)b"])')?.id,
570
+ ).toBe('main-post');
571
+ });
254
572
 
255
573
  it('handles complex selectors', () => {
256
574
  const complexSelector = container.querySelectorAll(
@@ -269,12 +587,205 @@ describe('selector parsing and matching', () => {
269
587
  expect(container.querySelector('table')).toBeNull();
270
588
  expect(container.querySelector('#nonexistent-id')).toBeNull();
271
589
  });
590
+ it('ignores leading selector whitespace', () => {
591
+ expect(container.querySelector(' \n\tarticle')?.id).toBe('main-post');
592
+ });
593
+
594
+ it('does not turn leading whitespace into a match', () => {
595
+ expect(container.querySelector(' \n\tsection')).toBeNull();
596
+ });
272
597
 
273
- it('handles edge cases', () => {
274
- expect(container.querySelectorAll('')).toHaveLength(0);
598
+ it.each([
599
+ ['leading and trailing', ' \n\tarticle ', true],
600
+ ['internal child', 'article \n >\t .sidebar ', true],
601
+ ['internal descendant', 'article .nav\t a.active ', true],
602
+ ['non-match with whitespace', ' \n footer > .sidebar\t ', false],
603
+ ])('handles %s whitespace', (_name, selector, matches) => {
604
+ expect(container.querySelector(selector) != null).toBe(matches);
605
+ });
275
606
 
607
+ it('selects all elements with the universal selector', () => {
276
608
  const allElements = container.querySelectorAll('*');
277
609
  expect(allElements.length).toBeGreaterThan(0);
278
610
  });
279
611
  });
612
+
613
+ describe('querySelector and querySelectorAll with Matcher[] argument', () => {
614
+ let container: Element;
615
+
616
+ beforeEach(() => {
617
+ container = document.createElement('div');
618
+ container.innerHTML = `
619
+ <article class="post" id="main-post">
620
+ <h1 class="title">Main Title</h1>
621
+ <div class="content">
622
+ <p class="text">First paragraph</p>
623
+ <p class="text hidden">Hidden paragraph</p>
624
+ <span class="highlight">Highlighted text</span>
625
+ </div>
626
+ </article>
627
+ `;
628
+ });
629
+
630
+ // The standalone querySelector/querySelectorAll functions expect the polyfill's
631
+ // ParentNode, but `container` is typed as the global Element (lib.dom.d.ts).
632
+ // At runtime, Window.setGlobalThis replaces globals with polyfill instances.
633
+ const asPolyfill = (node: Element) => node as unknown as ParentNode;
634
+
635
+ it('selects by ID matcher without parsing', () => {
636
+ const main = querySelector(asPolyfill(container), [
637
+ {type: MatcherType.Id, name: 'main-post'},
638
+ ]);
639
+ expect(main?.tagName.toLowerCase()).toBe('article');
640
+ });
641
+
642
+ it('types standalone query results as elements', () => {
643
+ const matches = querySelectorAll(asPolyfill(container), [
644
+ {type: MatcherType.Element, name: 'p'},
645
+ ]);
646
+
647
+ expectTypeOf(matches).toEqualTypeOf<NodeList<PolyfillElement>>();
648
+ expectTypeOf(matches[0]!).toEqualTypeOf<PolyfillElement>();
649
+ expectTypeOf(matches.item(0)).toEqualTypeOf<PolyfillElement | null>();
650
+ });
651
+
652
+ it('selects by element matcher without parsing', () => {
653
+ const paragraphs = querySelectorAll(asPolyfill(container), [
654
+ {type: MatcherType.Element, name: 'p'},
655
+ ]);
656
+ expect(paragraphs).toHaveLength(2);
657
+ });
658
+
659
+ it('matches ids with special characters literally (no CSS escaping)', () => {
660
+ const element = document.createElement('div');
661
+ element.id = 'a.b:c#d';
662
+ container.appendChild(element);
663
+
664
+ const result = querySelector(asPolyfill(container), [
665
+ {type: MatcherType.Id, name: 'a.b:c#d'},
666
+ ]);
667
+ expect(result).toBe(element);
668
+ });
669
+
670
+ it('matches classes with selector punctuation literally', () => {
671
+ const element = document.createElement('div');
672
+ element.setAttribute('class', 'has.dot has:colon has#hash');
673
+ container.appendChild(element);
674
+
675
+ for (const name of ['has.dot', 'has:colon', 'has#hash']) {
676
+ expect(
677
+ querySelector(asPolyfill(container), [
678
+ {type: MatcherType.Class, name},
679
+ ]),
680
+ ).toBe(element);
681
+ }
682
+ });
683
+
684
+ it.each(ASCII_WHITESPACE)(
685
+ 'splits class attributes on %s',
686
+ (_name, whitespace) => {
687
+ const element = document.createElement('div');
688
+ element.setAttribute('class', `left${whitespace}right`);
689
+ container.appendChild(element);
690
+
691
+ expect(
692
+ querySelector(asPolyfill(container), [
693
+ {type: MatcherType.Class, name: 'right'},
694
+ ]),
695
+ ).toBe(element);
696
+ },
697
+ );
698
+
699
+ it('does not split class attributes on non-ASCII whitespace', () => {
700
+ const literal = document.createElement('div');
701
+ literal.setAttribute('class', 'left\u00a0right');
702
+
703
+ const separated = document.createElement('div');
704
+ separated.setAttribute('class', 'left right');
705
+
706
+ container.append(literal, separated);
707
+
708
+ expect(
709
+ querySelector(asPolyfill(container), [
710
+ {type: MatcherType.Class, name: 'left\u00a0right'},
711
+ ]),
712
+ ).toBe(literal);
713
+ expect(
714
+ querySelector(asPolyfill(container), [
715
+ {type: MatcherType.Class, name: 'left'},
716
+ ]),
717
+ ).toBe(separated);
718
+ });
719
+
720
+ it('requires every class matcher to match', () => {
721
+ const complete = document.createElement('div');
722
+ complete.setAttribute('class', 'one two three');
723
+
724
+ const partial = document.createElement('div');
725
+ partial.setAttribute('class', 'one two');
726
+
727
+ container.append(complete, partial);
728
+
729
+ const matches = querySelectorAll(asPolyfill(container), [
730
+ {type: MatcherType.Class, name: 'one'},
731
+ {type: MatcherType.Class, name: 'three'},
732
+ ]);
733
+ expect(matches).toHaveLength(1);
734
+ expect(matches[0]).toBe(complete);
735
+
736
+ expect(
737
+ querySelector(asPolyfill(container), [
738
+ {type: MatcherType.Class, name: 'one'},
739
+ {type: MatcherType.Class, name: 'missing'},
740
+ ]),
741
+ ).toBeNull();
742
+ });
743
+
744
+ it('matches HTML tag names case-insensitively via element matcher', () => {
745
+ const upper = querySelectorAll(asPolyfill(container), [
746
+ {type: MatcherType.Element, name: 'ARTICLE'},
747
+ ]);
748
+ expect(upper).toHaveLength(1);
749
+
750
+ const mixed = querySelectorAll(asPolyfill(container), [
751
+ {type: MatcherType.Element, name: 'SpAn'},
752
+ ]);
753
+ expect(mixed).toHaveLength(1);
754
+ expect(mixed[0]!.getAttribute('class')).toBe('highlight');
755
+ });
756
+
757
+ it('returns same results as string selectors for compound queries', () => {
758
+ const byString = container.querySelectorAll('p.text.hidden');
759
+ const byObject = querySelectorAll(asPolyfill(container), [
760
+ {type: MatcherType.Element, name: 'p'},
761
+ {type: MatcherType.Class, name: 'text'},
762
+ {type: MatcherType.Class, name: 'hidden'},
763
+ ]);
764
+
765
+ expect(byObject).toHaveLength(byString.length);
766
+ expect(byObject[0]?.textContent?.trim()).toBe('Hidden paragraph');
767
+ });
768
+
769
+ it('wildcard matcher returns all elements', () => {
770
+ const all = querySelectorAll(asPolyfill(container), [
771
+ {type: MatcherType.Unknown, name: '*'},
772
+ ]);
773
+ expect(all.length).toBeGreaterThan(0);
774
+ // Should include article, h1, div, p, p, span
775
+ expect(all.length).toBe(6);
776
+ });
777
+
778
+ it('returns null/empty for non-matching matchers', () => {
779
+ expect(
780
+ querySelector(asPolyfill(container), [
781
+ {type: MatcherType.Id, name: 'nope'},
782
+ ]),
783
+ ).toBeNull();
784
+ expect(
785
+ querySelectorAll(asPolyfill(container), [
786
+ {type: MatcherType.Element, name: 'table'},
787
+ ]),
788
+ ).toHaveLength(0);
789
+ });
790
+ });
280
791
  });