@remote-dom/polyfill 1.4.6 → 1.5.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.
- package/CHANGELOG.md +12 -0
- package/build/cjs/ClipboardEvent.cjs +15 -0
- package/build/cjs/FocusEvent.cjs +15 -0
- package/build/cjs/ToggleEvent.cjs +16 -0
- package/build/cjs/Window.cjs +6 -0
- package/build/cjs/selectors.cjs +48 -13
- package/build/esm/ClipboardEvent.mjs +11 -0
- package/build/esm/FocusEvent.mjs +11 -0
- package/build/esm/ToggleEvent.mjs +12 -0
- package/build/esm/Window.mjs +6 -0
- package/build/esm/selectors.mjs +48 -13
- package/build/esnext/ClipboardEvent.esnext +11 -0
- package/build/esnext/FocusEvent.esnext +11 -0
- package/build/esnext/ToggleEvent.esnext +13 -0
- package/build/esnext/Window.esnext +6 -0
- package/build/esnext/selectors.esnext +43 -13
- package/build/tsconfig.tsbuildinfo +1 -1
- package/build/typescript/ClipboardEvent.d.ts +6 -0
- package/build/typescript/ClipboardEvent.d.ts.map +1 -0
- package/build/typescript/FocusEvent.d.ts +6 -0
- package/build/typescript/FocusEvent.d.ts.map +1 -0
- package/build/typescript/ToggleEvent.d.ts +7 -0
- package/build/typescript/ToggleEvent.d.ts.map +1 -0
- package/build/typescript/Window.d.ts +6 -0
- package/build/typescript/Window.d.ts.map +1 -1
- package/build/typescript/selectors.d.ts.map +1 -1
- package/build/typescript/tests/selectors.test.d.ts +2 -0
- package/build/typescript/tests/selectors.test.d.ts.map +1 -0
- package/package.json +1 -1
- package/source/ClipboardEvent.ts +12 -0
- package/source/FocusEvent.ts +12 -0
- package/source/ToggleEvent.ts +14 -0
- package/source/Window.ts +6 -0
- package/source/selectors.ts +52 -13
- package/source/tests/selectors.test.ts +280 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import {Window} from '../index.ts';
|
|
2
|
+
import {parseSelector} from '../selectors.ts';
|
|
3
|
+
|
|
4
|
+
import {describe, it, expect, beforeEach} from 'vitest';
|
|
5
|
+
|
|
6
|
+
describe('selector parsing and matching', () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
const window = new Window();
|
|
9
|
+
Window.setGlobalThis(window);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe('parseSelector', () => {
|
|
13
|
+
it('parses element selectors', () => {
|
|
14
|
+
const parts = parseSelector('div');
|
|
15
|
+
expect(parts).toHaveLength(1);
|
|
16
|
+
expect(parts[0]!.matchers).toHaveLength(1);
|
|
17
|
+
expect(parts[0]!.matchers[0]!).toMatchObject({
|
|
18
|
+
type: 1, // MatcherType.Element
|
|
19
|
+
name: 'div',
|
|
20
|
+
value: 'div',
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('parses ID selectors', () => {
|
|
25
|
+
const parts = parseSelector('#myid');
|
|
26
|
+
expect(parts).toHaveLength(1);
|
|
27
|
+
expect(parts[0]!.matchers[0]!).toMatchObject({
|
|
28
|
+
type: 2, // MatcherType.Id
|
|
29
|
+
name: 'myid',
|
|
30
|
+
value: 'myid',
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('parses class selectors', () => {
|
|
35
|
+
const parts = parseSelector('.myclass');
|
|
36
|
+
expect(parts).toHaveLength(1);
|
|
37
|
+
expect(parts[0]!.matchers[0]!).toMatchObject({
|
|
38
|
+
type: 3, // MatcherType.Class
|
|
39
|
+
name: 'myclass',
|
|
40
|
+
value: 'myclass',
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('parses attribute selectors without values', () => {
|
|
45
|
+
const parts = parseSelector('[disabled]');
|
|
46
|
+
expect(parts).toHaveLength(1);
|
|
47
|
+
expect(parts[0]!.matchers[0]!).toMatchObject({
|
|
48
|
+
type: 4, // MatcherType.Attribute
|
|
49
|
+
name: 'disabled',
|
|
50
|
+
value: undefined,
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('parses attribute selectors with values', () => {
|
|
55
|
+
const parts = parseSelector('[type="button"]');
|
|
56
|
+
expect(parts).toHaveLength(1);
|
|
57
|
+
expect(parts[0]!.matchers[0]!).toMatchObject({
|
|
58
|
+
type: 4, // MatcherType.Attribute
|
|
59
|
+
name: 'type',
|
|
60
|
+
value: 'button',
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
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
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('parses function selectors', () => {
|
|
75
|
+
const parts = parseSelector(':has(div)');
|
|
76
|
+
expect(parts).toHaveLength(1);
|
|
77
|
+
expect(parts[0]!.matchers[0]!).toMatchObject({
|
|
78
|
+
type: 6, // MatcherType.Function
|
|
79
|
+
name: 'has',
|
|
80
|
+
value: 'div',
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('parses :not() function selectors', () => {
|
|
85
|
+
const parts = parseSelector(':not(.hidden)');
|
|
86
|
+
expect(parts).toHaveLength(1);
|
|
87
|
+
expect(parts[0]!.matchers[0]!).toMatchObject({
|
|
88
|
+
type: 6, // MatcherType.Function
|
|
89
|
+
name: 'not',
|
|
90
|
+
value: '.hidden',
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('parses compound selectors', () => {
|
|
95
|
+
const parts = parseSelector('div.myclass#myid[type="button"]');
|
|
96
|
+
expect(parts).toHaveLength(1);
|
|
97
|
+
expect(parts[0]!.matchers).toHaveLength(4);
|
|
98
|
+
expect(parts[0]!.matchers[0]!.name).toBe('div');
|
|
99
|
+
expect(parts[0]!.matchers[1]!.name).toBe('myclass');
|
|
100
|
+
expect(parts[0]!.matchers[2]!.name).toBe('myid');
|
|
101
|
+
expect(parts[0]!.matchers[3]!.name).toBe('type');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('parses child combinator', () => {
|
|
105
|
+
const parts = parseSelector('div > span');
|
|
106
|
+
expect(parts).toHaveLength(2);
|
|
107
|
+
expect(parts[0]!.combinator).toBe(1); // Combinator.Child
|
|
108
|
+
expect(parts[0]!.matchers[0]!.name).toBe('div');
|
|
109
|
+
expect(parts[1]!.matchers[0]!.name).toBe('span');
|
|
110
|
+
});
|
|
111
|
+
|
|
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
|
+
});
|
|
119
|
+
|
|
120
|
+
it('parses adjacent sibling combinator', () => {
|
|
121
|
+
const parts = parseSelector('h1 + p');
|
|
122
|
+
expect(parts).toHaveLength(2);
|
|
123
|
+
expect(parts[0]!.combinator).toBe(3); // Combinator.Adjacent
|
|
124
|
+
expect(parts[0]!.matchers[0]!.name).toBe('h1');
|
|
125
|
+
expect(parts[1]!.combinator).toBe(4); // Combinator.Inner
|
|
126
|
+
expect(parts[1]!.matchers[0]!.name).toBe('p');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('parses general sibling combinator', () => {
|
|
130
|
+
const parts = parseSelector('h1 ~ p');
|
|
131
|
+
expect(parts).toHaveLength(2);
|
|
132
|
+
expect(parts[0]!.combinator).toBe(2); // Combinator.Sibling
|
|
133
|
+
expect(parts[0]!.matchers[0]!.name).toBe('h1');
|
|
134
|
+
expect(parts[1]!.combinator).toBe(4); // Combinator.Inner
|
|
135
|
+
expect(parts[1]!.matchers[0]!.name).toBe('p');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('parses complex selectors', () => {
|
|
139
|
+
const parts = parseSelector('article > .header + .content:not(.hidden)');
|
|
140
|
+
expect(parts).toHaveLength(3);
|
|
141
|
+
expect(parts[0]!.combinator).toBe(1); // Child
|
|
142
|
+
expect(parts[1]!.combinator).toBe(3); // Adjacent
|
|
143
|
+
expect(parts[2]!.combinator).toBe(4); // Inner
|
|
144
|
+
expect(parts[0]!.matchers[0]!.name).toBe('article');
|
|
145
|
+
expect(parts[1]!.matchers[0]!.name).toBe('header');
|
|
146
|
+
expect(parts[2]!.matchers).toHaveLength(2);
|
|
147
|
+
expect(parts[2]!.matchers[0]!.name).toBe('content');
|
|
148
|
+
expect(parts[2]!.matchers[1]!.name).toBe('not');
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe('querySelector and querySelectorAll', () => {
|
|
153
|
+
let container: Element;
|
|
154
|
+
|
|
155
|
+
beforeEach(() => {
|
|
156
|
+
container = document.createElement('div');
|
|
157
|
+
container.innerHTML = `
|
|
158
|
+
<article class="post" id="main-post">
|
|
159
|
+
<h1 class="title">Main Title</h1>
|
|
160
|
+
<div class="content">
|
|
161
|
+
<p class="text">First paragraph</p>
|
|
162
|
+
<p class="text hidden">Hidden paragraph</p>
|
|
163
|
+
<span class="highlight">Highlighted text</span>
|
|
164
|
+
</div>
|
|
165
|
+
<aside class="sidebar">
|
|
166
|
+
<ul class="nav">
|
|
167
|
+
<li><a href="#" class="link active">Active Link</a></li>
|
|
168
|
+
<li><a href="#" class="link">Regular Link</a></li>
|
|
169
|
+
</ul>
|
|
170
|
+
</aside>
|
|
171
|
+
</article>
|
|
172
|
+
<footer class="footer">
|
|
173
|
+
<div class="content">
|
|
174
|
+
<p class="text">Footer text</p>
|
|
175
|
+
</div>
|
|
176
|
+
</footer>
|
|
177
|
+
`;
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('selects by element name', () => {
|
|
181
|
+
const articles = container.querySelectorAll('article');
|
|
182
|
+
expect(articles).toHaveLength(1);
|
|
183
|
+
expect(articles[0]!.getAttribute('class')).toBe('post');
|
|
184
|
+
|
|
185
|
+
const paragraphs = container.querySelectorAll('p');
|
|
186
|
+
expect(paragraphs).toHaveLength(3);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('selects by ID', () => {
|
|
190
|
+
const main = container.querySelector('#main-post');
|
|
191
|
+
expect(main?.tagName.toLowerCase()).toBe('article');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('selects by class', () => {
|
|
195
|
+
const texts = container.querySelectorAll('.text');
|
|
196
|
+
expect(texts).toHaveLength(3);
|
|
197
|
+
|
|
198
|
+
const hidden = container.querySelector('.hidden');
|
|
199
|
+
expect(hidden?.textContent?.trim()).toBe('Hidden paragraph');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('selects by attribute', () => {
|
|
203
|
+
const links = container.querySelectorAll('[href]');
|
|
204
|
+
expect(links).toHaveLength(2);
|
|
205
|
+
|
|
206
|
+
const activeLinks = container.querySelectorAll('[href="#"]');
|
|
207
|
+
expect(activeLinks).toHaveLength(2);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('selects by compound selectors', () => {
|
|
211
|
+
const activeLink = container.querySelector('a.link.active');
|
|
212
|
+
expect(activeLink?.textContent?.trim()).toBe('Active Link');
|
|
213
|
+
|
|
214
|
+
const hiddenText = container.querySelector('p.text.hidden');
|
|
215
|
+
expect(hiddenText?.textContent?.trim()).toBe('Hidden paragraph');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('selects with descendant combinator', () => {
|
|
219
|
+
const contentTexts = container.querySelectorAll('.content p');
|
|
220
|
+
expect(contentTexts).toHaveLength(3);
|
|
221
|
+
|
|
222
|
+
const sidebarLinks = container.querySelectorAll('.sidebar a');
|
|
223
|
+
expect(sidebarLinks).toHaveLength(2);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('selects with child combinator', () => {
|
|
227
|
+
const directContentChildren = container.querySelectorAll('.content > p');
|
|
228
|
+
expect(directContentChildren).toHaveLength(3);
|
|
229
|
+
|
|
230
|
+
const directArticleChildren = container.querySelectorAll('article > h1');
|
|
231
|
+
expect(directArticleChildren).toHaveLength(1);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('selects with adjacent sibling combinator', () => {
|
|
235
|
+
const titleSibling = container.querySelector('h1 + div');
|
|
236
|
+
expect(titleSibling?.getAttribute('class')).toBe('content');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('selects with general sibling combinator', () => {
|
|
240
|
+
const titleSiblings = container.querySelectorAll('h1 ~ div');
|
|
241
|
+
expect(titleSiblings).toHaveLength(1);
|
|
242
|
+
|
|
243
|
+
const contentSiblings = container.querySelectorAll('.content ~ aside');
|
|
244
|
+
expect(contentSiblings).toHaveLength(1);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('selects with :has() pseudo-class', () => {
|
|
248
|
+
const hasLinks = container.querySelectorAll(':has(a)');
|
|
249
|
+
expect(hasLinks.length).toBeGreaterThan(0);
|
|
250
|
+
|
|
251
|
+
const hasActiveLink = container.querySelector(':has(.active)');
|
|
252
|
+
expect(hasActiveLink).toBeTruthy();
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('handles complex selectors', () => {
|
|
256
|
+
const complexSelector = container.querySelectorAll(
|
|
257
|
+
'article .content > p.text',
|
|
258
|
+
);
|
|
259
|
+
expect(complexSelector).toHaveLength(2);
|
|
260
|
+
|
|
261
|
+
const deepSelector = container.querySelector('.sidebar ul li a.active');
|
|
262
|
+
expect(deepSelector?.textContent?.trim()).toBe('Active Link');
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('returns null/empty for non-matching selectors', () => {
|
|
266
|
+
expect(container.querySelector('.nonexistent')).toBeNull();
|
|
267
|
+
expect(container.querySelectorAll('.nonexistent')).toHaveLength(0);
|
|
268
|
+
|
|
269
|
+
expect(container.querySelector('table')).toBeNull();
|
|
270
|
+
expect(container.querySelector('#nonexistent-id')).toBeNull();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('handles edge cases', () => {
|
|
274
|
+
expect(container.querySelectorAll('')).toHaveLength(0);
|
|
275
|
+
|
|
276
|
+
const allElements = container.querySelectorAll('*');
|
|
277
|
+
expect(allElements.length).toBeGreaterThan(0);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
});
|