zero-query 1.1.1 → 1.2.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 (154) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +2 -0
  3. package/cli/args.js +33 -33
  4. package/cli/commands/build-api.js +443 -442
  5. package/cli/commands/build.js +254 -247
  6. package/cli/commands/bundle.js +1228 -1224
  7. package/cli/commands/create.js +137 -121
  8. package/cli/commands/dev/devtools/index.js +56 -56
  9. package/cli/commands/dev/devtools/js/components.js +49 -49
  10. package/cli/commands/dev/devtools/js/core.js +423 -423
  11. package/cli/commands/dev/devtools/js/elements.js +421 -421
  12. package/cli/commands/dev/devtools/js/network.js +166 -166
  13. package/cli/commands/dev/devtools/js/performance.js +73 -73
  14. package/cli/commands/dev/devtools/js/router.js +105 -105
  15. package/cli/commands/dev/devtools/js/source.js +132 -132
  16. package/cli/commands/dev/devtools/js/stats.js +35 -35
  17. package/cli/commands/dev/devtools/js/tabs.js +79 -79
  18. package/cli/commands/dev/devtools/panel.html +95 -95
  19. package/cli/commands/dev/devtools/styles.css +244 -244
  20. package/cli/commands/dev/index.js +107 -107
  21. package/cli/commands/dev/logger.js +75 -75
  22. package/cli/commands/dev/overlay.js +858 -858
  23. package/cli/commands/dev/server.js +220 -220
  24. package/cli/commands/dev/validator.js +94 -94
  25. package/cli/commands/dev/watcher.js +172 -172
  26. package/cli/help.js +114 -112
  27. package/cli/index.js +52 -52
  28. package/cli/scaffold/default/LICENSE +21 -21
  29. package/cli/scaffold/default/app/app.js +207 -207
  30. package/cli/scaffold/default/app/components/about.js +201 -201
  31. package/cli/scaffold/default/app/components/api-demo.js +143 -143
  32. package/cli/scaffold/default/app/components/contact-card.js +231 -231
  33. package/cli/scaffold/default/app/components/contacts/contacts.css +706 -706
  34. package/cli/scaffold/default/app/components/contacts/contacts.html +200 -200
  35. package/cli/scaffold/default/app/components/contacts/contacts.js +196 -196
  36. package/cli/scaffold/default/app/components/counter.js +127 -127
  37. package/cli/scaffold/default/app/components/home.js +249 -249
  38. package/cli/scaffold/default/app/components/not-found.js +16 -16
  39. package/cli/scaffold/default/app/components/playground/playground.css +115 -115
  40. package/cli/scaffold/default/app/components/playground/playground.html +161 -161
  41. package/cli/scaffold/default/app/components/playground/playground.js +116 -116
  42. package/cli/scaffold/default/app/components/todos.js +225 -225
  43. package/cli/scaffold/default/app/components/toolkit/toolkit.css +97 -97
  44. package/cli/scaffold/default/app/components/toolkit/toolkit.html +146 -146
  45. package/cli/scaffold/default/app/components/toolkit/toolkit.js +280 -280
  46. package/cli/scaffold/default/app/routes.js +15 -15
  47. package/cli/scaffold/default/app/store.js +101 -101
  48. package/cli/scaffold/default/global.css +552 -552
  49. package/cli/scaffold/default/index.html +99 -99
  50. package/cli/scaffold/minimal/app/app.js +85 -85
  51. package/cli/scaffold/minimal/app/components/about.js +68 -68
  52. package/cli/scaffold/minimal/app/components/counter.js +122 -122
  53. package/cli/scaffold/minimal/app/components/home.js +68 -68
  54. package/cli/scaffold/minimal/app/components/not-found.js +16 -16
  55. package/cli/scaffold/minimal/app/routes.js +9 -9
  56. package/cli/scaffold/minimal/app/store.js +36 -36
  57. package/cli/scaffold/minimal/global.css +300 -300
  58. package/cli/scaffold/minimal/index.html +44 -44
  59. package/cli/scaffold/ssr/app/app.js +41 -41
  60. package/cli/scaffold/ssr/app/components/about.js +55 -55
  61. package/cli/scaffold/ssr/app/components/blog/index.js +65 -65
  62. package/cli/scaffold/ssr/app/components/blog/post.js +86 -86
  63. package/cli/scaffold/ssr/app/components/home.js +37 -37
  64. package/cli/scaffold/ssr/app/components/not-found.js +15 -15
  65. package/cli/scaffold/ssr/app/routes.js +8 -8
  66. package/cli/scaffold/ssr/global.css +228 -228
  67. package/cli/scaffold/ssr/index.html +37 -37
  68. package/cli/scaffold/ssr/package.json +8 -8
  69. package/cli/scaffold/ssr/server/data/posts.js +144 -144
  70. package/cli/scaffold/ssr/server/index.js +213 -213
  71. package/cli/scaffold/webrtc/app/app.js +11 -0
  72. package/cli/scaffold/webrtc/app/components/video-room.js +295 -0
  73. package/cli/scaffold/webrtc/app/lib/room.js +252 -0
  74. package/cli/scaffold/webrtc/assets/.gitkeep +0 -0
  75. package/cli/scaffold/webrtc/global.css +250 -0
  76. package/cli/scaffold/webrtc/index.html +21 -0
  77. package/cli/utils.js +305 -287
  78. package/dist/API.md +661 -0
  79. package/dist/zquery.dist.zip +0 -0
  80. package/dist/zquery.js +10313 -6614
  81. package/dist/zquery.min.js +8 -631
  82. package/index.d.ts +570 -371
  83. package/index.js +311 -240
  84. package/package.json +76 -70
  85. package/src/component.js +1709 -1691
  86. package/src/core.js +921 -921
  87. package/src/diff.js +497 -497
  88. package/src/errors.js +209 -209
  89. package/src/expression.js +922 -922
  90. package/src/http.js +242 -242
  91. package/src/package.json +1 -1
  92. package/src/reactive.js +255 -255
  93. package/src/router.js +843 -843
  94. package/src/ssr.js +418 -418
  95. package/src/store.js +318 -318
  96. package/src/utils.js +515 -515
  97. package/src/webrtc/e2ee.js +351 -0
  98. package/src/webrtc/errors.js +116 -0
  99. package/src/webrtc/ice.js +301 -0
  100. package/src/webrtc/index.js +131 -0
  101. package/src/webrtc/joinToken.js +119 -0
  102. package/src/webrtc/observe.js +172 -0
  103. package/src/webrtc/peer.js +351 -0
  104. package/src/webrtc/reactive.js +268 -0
  105. package/src/webrtc/room.js +625 -0
  106. package/src/webrtc/sdp.js +302 -0
  107. package/src/webrtc/sfu/index.js +43 -0
  108. package/src/webrtc/sfu/livekit.js +131 -0
  109. package/src/webrtc/sfu/mediasoup.js +150 -0
  110. package/src/webrtc/signaling.js +373 -0
  111. package/src/webrtc/turn.js +237 -0
  112. package/tests/_helpers/webrtcFakes.js +289 -0
  113. package/tests/audit.test.js +4158 -4158
  114. package/tests/cli.test.js +1136 -1103
  115. package/tests/compare.test.js +497 -486
  116. package/tests/component.test.js +3969 -3938
  117. package/tests/core.test.js +1910 -1910
  118. package/tests/dev-server.test.js +489 -489
  119. package/tests/diff.test.js +1416 -1416
  120. package/tests/docs.test.js +1664 -1650
  121. package/tests/electron-features.test.js +864 -864
  122. package/tests/errors.test.js +619 -619
  123. package/tests/expression.test.js +1056 -1056
  124. package/tests/http.test.js +648 -648
  125. package/tests/reactive.test.js +819 -819
  126. package/tests/router.test.js +2327 -2327
  127. package/tests/ssr.test.js +870 -870
  128. package/tests/store.test.js +830 -830
  129. package/tests/test-minifier.js +153 -153
  130. package/tests/test-ssr.js +27 -27
  131. package/tests/utils.test.js +1377 -1377
  132. package/tests/webrtc/e2ee.test.js +283 -0
  133. package/tests/webrtc/ice.test.js +202 -0
  134. package/tests/webrtc/joinToken.test.js +89 -0
  135. package/tests/webrtc/observe.test.js +111 -0
  136. package/tests/webrtc/peer.test.js +373 -0
  137. package/tests/webrtc/reactive.test.js +235 -0
  138. package/tests/webrtc/room.test.js +406 -0
  139. package/tests/webrtc/sdp.test.js +151 -0
  140. package/tests/webrtc/sfu-livekit.test.js +119 -0
  141. package/tests/webrtc/sfu.test.js +160 -0
  142. package/tests/webrtc/signaling.test.js +251 -0
  143. package/tests/webrtc/turn.test.js +256 -0
  144. package/types/collection.d.ts +383 -383
  145. package/types/component.d.ts +186 -186
  146. package/types/errors.d.ts +135 -135
  147. package/types/http.d.ts +92 -92
  148. package/types/misc.d.ts +201 -201
  149. package/types/reactive.d.ts +98 -98
  150. package/types/router.d.ts +190 -190
  151. package/types/ssr.d.ts +102 -102
  152. package/types/store.d.ts +146 -146
  153. package/types/utils.d.ts +245 -245
  154. package/types/webrtc.d.ts +653 -0
@@ -1,1910 +1,1910 @@
1
- import { describe, it, expect, beforeEach, vi } from 'vitest';
2
- import { query, queryAll, ZQueryCollection } from '../src/core.js';
3
-
4
-
5
- // ---------------------------------------------------------------------------
6
- // Setup
7
- // ---------------------------------------------------------------------------
8
- beforeEach(() => {
9
- document.body.innerHTML = `
10
- <div id="main">
11
- <p class="text first-p">Hello</p>
12
- <p class="text second-p">World</p>
13
- <span class="other">Span</span>
14
- <p class="text third-p">Extra</p>
15
- </div>
16
- <div id="sidebar">
17
- <ul id="nav">
18
- <li class="nav-item active">Home</li>
19
- <li class="nav-item">About</li>
20
- <li class="nav-item">Contact</li>
21
- </ul>
22
- </div>
23
- `;
24
- });
25
-
26
-
27
- // ---------------------------------------------------------------------------
28
- // query() - single selector
29
- // ---------------------------------------------------------------------------
30
-
31
- describe('query()', () => {
32
- it('returns ZQueryCollection by CSS selector', () => {
33
- const col = query('#main');
34
- expect(col).toBeInstanceOf(ZQueryCollection);
35
- expect(col.first()).toBe(document.getElementById('main'));
36
- });
37
-
38
- it('returns empty collection for non-matching selector', () => {
39
- const col = query('#nonexistent');
40
- expect(col).toBeInstanceOf(ZQueryCollection);
41
- expect(col.length).toBe(0);
42
- });
43
-
44
- it('returns empty collection for null/undefined', () => {
45
- expect(query(null).length).toBe(0);
46
- expect(query(undefined).length).toBe(0);
47
- });
48
-
49
- it('wraps DOM element in collection', () => {
50
- const div = document.createElement('div');
51
- const col = query(div);
52
- expect(col).toBeInstanceOf(ZQueryCollection);
53
- expect(col.first()).toBe(div);
54
- });
55
-
56
- it('creates elements from HTML string as collection', () => {
57
- const col = query('<span>hello</span>');
58
- expect(col).toBeInstanceOf(ZQueryCollection);
59
- expect(col.first().tagName).toBe('SPAN');
60
- expect(col.first().textContent).toBe('hello');
61
- });
62
-
63
- it('returns all matching elements', () => {
64
- const col = query('.text');
65
- expect(col.length).toBe(3);
66
- });
67
-
68
- it('uses context parameter', () => {
69
- const col = query('.text', '#main');
70
- expect(col.length).toBe(3);
71
- expect(col.first().textContent).toBe('Hello');
72
- });
73
-
74
- it('supports chaining on result', () => {
75
- const col = query('#main').addClass('chained');
76
- expect(col.first().classList.contains('chained')).toBe(true);
77
- });
78
- });
79
-
80
-
81
- // ---------------------------------------------------------------------------
82
- // queryAll() - collection selector
83
- // ---------------------------------------------------------------------------
84
-
85
- describe('queryAll()', () => {
86
- it('returns ZQueryCollection for CSS selector', () => {
87
- const col = queryAll('.text');
88
- expect(col).toBeInstanceOf(ZQueryCollection);
89
- expect(col.length).toBe(3);
90
- });
91
-
92
- it('returns empty collection for non-matching', () => {
93
- const col = queryAll('.nonexistent');
94
- expect(col.length).toBe(0);
95
- });
96
-
97
- it('wraps single element', () => {
98
- const div = document.createElement('div');
99
- const col = queryAll(div);
100
- expect(col.length).toBe(1);
101
- expect(col.first()).toBe(div);
102
- });
103
-
104
- it('creates elements from HTML', () => {
105
- const col = queryAll('<li>a</li><li>b</li>');
106
- expect(col.length).toBe(2);
107
- });
108
-
109
- it('returns empty for null', () => {
110
- expect(queryAll(null).length).toBe(0);
111
- });
112
- });
113
-
114
-
115
- // ---------------------------------------------------------------------------
116
- // ZQueryCollection methods
117
- // ---------------------------------------------------------------------------
118
-
119
- describe('ZQueryCollection', () => {
120
- describe('iteration', () => {
121
- it('each() iterates elements', () => {
122
- const col = queryAll('.text');
123
- const tags = [];
124
- col.each((_, el) => tags.push(el.textContent));
125
- expect(tags).toEqual(['Hello', 'World', 'Extra']);
126
- });
127
-
128
- it('map() maps elements', () => {
129
- const texts = queryAll('.text').map((_, el) => el.textContent);
130
- expect(texts).toEqual(['Hello', 'World', 'Extra']);
131
- });
132
-
133
- it('first() and last()', () => {
134
- const col = queryAll('.text');
135
- expect(col.first().textContent).toBe('Hello');
136
- expect(col.last().textContent).toBe('Extra');
137
- });
138
-
139
- it('eq() returns sub-collection', () => {
140
- const col = queryAll('.text');
141
- expect(col.eq(1).first().textContent).toBe('World');
142
- expect(col.eq(5).length).toBe(0);
143
- });
144
-
145
- it('toArray() returns plain array', () => {
146
- const arr = queryAll('.text').toArray();
147
- expect(Array.isArray(arr)).toBe(true);
148
- expect(arr.length).toBe(3);
149
- });
150
-
151
- it('is iterable', () => {
152
- const col = queryAll('.text');
153
- const items = [...col];
154
- expect(items.length).toBe(3);
155
- });
156
- });
157
-
158
-
159
- describe('traversal', () => {
160
- it('find() searches descendants', () => {
161
- const main = queryAll('#main');
162
- const ps = main.find('.text');
163
- expect(ps.length).toBe(3);
164
- });
165
-
166
- it('parent() returns parents', () => {
167
- const p = queryAll('.text').parent();
168
- expect(p.first().id).toBe('main');
169
- });
170
-
171
- it('children() returns direct children', () => {
172
- const main = queryAll('#main');
173
- expect(main.children().length).toBe(4); // 3 p + 1 span
174
- });
175
-
176
- it('filter() with string selector', () => {
177
- const col = queryAll('#main').children();
178
- const ps = col.filter('p');
179
- expect(ps.length).toBe(3);
180
- });
181
-
182
- it('filter() with function', () => {
183
- const col = queryAll('#main').children();
184
- const ps = col.filter(el => el.tagName === 'P');
185
- expect(ps.length).toBe(3);
186
- });
187
-
188
- it('not() excludes elements', () => {
189
- const col = queryAll('#main').children();
190
- const nonP = col.not('p');
191
- expect(nonP.length).toBe(1);
192
- expect(nonP.first().tagName).toBe('SPAN');
193
- });
194
-
195
- it('next() returns next sibling', () => {
196
- const col = queryAll('.first-p');
197
- expect(col.next().first().classList.contains('second-p')).toBe(true);
198
- });
199
-
200
- it('next(selector) filters by selector', () => {
201
- const col = queryAll('.first-p');
202
- expect(col.next('.second-p').length).toBe(1);
203
- expect(col.next('.third-p').length).toBe(0);
204
- });
205
-
206
- it('prev() returns previous sibling', () => {
207
- const col = queryAll('.second-p');
208
- expect(col.prev().first().classList.contains('first-p')).toBe(true);
209
- });
210
-
211
- it('prev(selector) filters by selector', () => {
212
- const col = queryAll('.second-p');
213
- expect(col.prev('.first-p').length).toBe(1);
214
- expect(col.prev('.other').length).toBe(0);
215
- });
216
-
217
- it('nextAll() returns all following siblings', () => {
218
- const col = queryAll('.first-p');
219
- expect(col.nextAll().length).toBe(3); // second-p, other, third-p
220
- });
221
-
222
- it('nextAll(selector) filters', () => {
223
- const col = queryAll('.first-p');
224
- expect(col.nextAll('.text').length).toBe(2);
225
- });
226
-
227
- it('nextUntil() stops at selector', () => {
228
- const col = queryAll('.first-p');
229
- const result = col.nextUntil('.third-p');
230
- expect(result.length).toBe(2); // second-p, other
231
- });
232
-
233
- it('prevAll() returns all preceding siblings', () => {
234
- const col = queryAll('.third-p');
235
- expect(col.prevAll().length).toBe(3);
236
- });
237
-
238
- it('prevAll(selector) filters', () => {
239
- const col = queryAll('.third-p');
240
- expect(col.prevAll('.text').length).toBe(2);
241
- });
242
-
243
- it('prevUntil() stops at selector', () => {
244
- const col = queryAll('.third-p');
245
- const result = col.prevUntil('.first-p');
246
- expect(result.length).toBe(2); // other, second-p
247
- });
248
-
249
- it('parents() returns all ancestors', () => {
250
- const col = queryAll('.first-p');
251
- const parents = col.parents();
252
- expect(parents.length).toBeGreaterThanOrEqual(2); // #main, body, html
253
- });
254
-
255
- it('parents(selector) filters', () => {
256
- const col = queryAll('.first-p');
257
- expect(col.parents('#main').length).toBe(1);
258
- });
259
-
260
- it('parentsUntil() stops at selector', () => {
261
- const col = queryAll('.first-p');
262
- const result = col.parentsUntil('body');
263
- expect(result.length).toBe(1); // just #main
264
- });
265
-
266
- it('contents() includes text nodes', () => {
267
- document.body.innerHTML = '<div id="ct">text<span>child</span></div>';
268
- const col = queryAll('#ct');
269
- expect(col.contents().length).toBe(2); // text node + span
270
- });
271
-
272
- it('siblings() returns all siblings excluding self', () => {
273
- const col = queryAll('.other');
274
- const sibs = col.siblings();
275
- expect(sibs.length).toBe(3); // three .text p's
276
- });
277
-
278
- it('closest() finds ancestor', () => {
279
- const col = queryAll('.nav-item').eq(0);
280
- expect(col.closest('#nav').length).toBe(1);
281
- });
282
- });
283
-
284
-
285
- describe('classes', () => {
286
- it('addClass / hasClass / removeClass', () => {
287
- const col = queryAll('#main');
288
- col.addClass('active');
289
- expect(col.hasClass('active')).toBe(true);
290
- col.removeClass('active');
291
- expect(col.hasClass('active')).toBe(false);
292
- });
293
-
294
- it('toggleClass', () => {
295
- const col = queryAll('#main');
296
- col.toggleClass('toggled');
297
- expect(col.hasClass('toggled')).toBe(true);
298
- col.toggleClass('toggled');
299
- expect(col.hasClass('toggled')).toBe(false);
300
- });
301
-
302
- it('toggleClass with multiple classes', () => {
303
- const col = queryAll('#main');
304
- col.toggleClass('a', 'b');
305
- expect(col[0].classList.contains('a')).toBe(true);
306
- expect(col[0].classList.contains('b')).toBe(true);
307
- col.toggleClass('a', 'b');
308
- expect(col[0].classList.contains('a')).toBe(false);
309
- expect(col[0].classList.contains('b')).toBe(false);
310
- });
311
-
312
- it('toggleClass with force boolean', () => {
313
- const col = queryAll('#main');
314
- col.toggleClass('forced', true);
315
- expect(col.hasClass('forced')).toBe(true);
316
- col.toggleClass('forced', true);
317
- expect(col.hasClass('forced')).toBe(true);
318
- col.toggleClass('forced', false);
319
- expect(col.hasClass('forced')).toBe(false);
320
- });
321
- });
322
-
323
-
324
- describe('attributes', () => {
325
- it('attr get/set', () => {
326
- const col = queryAll('#main');
327
- col.attr('data-test', 'value');
328
- expect(col.attr('data-test')).toBe('value');
329
- });
330
-
331
- it('removeAttr', () => {
332
- const col = queryAll('#main');
333
- col.attr('data-x', 'y');
334
- col.removeAttr('data-x');
335
- expect(col.attr('data-x')).toBeNull();
336
- });
337
-
338
- it('data get/set', () => {
339
- const col = queryAll('#main');
340
- col.data('count', 42);
341
- expect(col.data('count')).toBe(42);
342
- });
343
-
344
- it('data handles objects via JSON', () => {
345
- const col = queryAll('#main');
346
- col.data('info', { a: 1 });
347
- expect(col.data('info')).toEqual({ a: 1 });
348
- });
349
- });
350
-
351
-
352
- describe('content', () => {
353
- it('html get/set', () => {
354
- const col = queryAll('#main');
355
- col.html('<b>bold</b>');
356
- expect(col.html()).toBe('<b>bold</b>');
357
- });
358
-
359
- it('html() auto-morphs when element has existing children', () => {
360
- const main = document.querySelector('#main');
361
- main.innerHTML = '<p id="preserved">old text</p>';
362
- const ref = main.children[0]; // grab DOM reference
363
- const col = queryAll('#main');
364
- col.html('<p id="preserved">new text</p>');
365
- // Same DOM node preserved - morph, not innerHTML replace
366
- expect(main.children[0]).toBe(ref);
367
- expect(main.children[0].textContent).toBe('new text');
368
- });
369
-
370
- it('html() uses innerHTML for empty elements (fast first-paint)', () => {
371
- const main = document.querySelector('#main');
372
- main.innerHTML = ''; // make it empty
373
- const col = queryAll('#main');
374
- col.html('<p id="fresh">hello</p>');
375
- expect(main.innerHTML).toBe('<p id="fresh">hello</p>');
376
- });
377
-
378
- it('empty().html() forces raw innerHTML (opt-out of morph)', () => {
379
- const main = document.querySelector('#main');
380
- main.innerHTML = '<p id="old">will be destroyed</p>';
381
- const ref = main.children[0];
382
- const col = queryAll('#main');
383
- col.empty().html('<p id="old">replaced</p>');
384
- // NOT the same node - empty() cleared children, so html() used innerHTML
385
- expect(main.children[0]).not.toBe(ref);
386
- expect(main.children[0].textContent).toBe('replaced');
387
- });
388
-
389
- it('text get/set', () => {
390
- const col = queryAll('.text').eq(0);
391
- col.text('Changed');
392
- expect(col.text()).toBe('Changed');
393
- });
394
-
395
- it('morph() diffs content instead of replacing', () => {
396
- const main = document.querySelector('#main');
397
- main.innerHTML = '<p id="keep">old</p>';
398
- const ref = main.children[0];
399
- const col = queryAll('#main');
400
- col.morph('<p id="keep">new</p>');
401
- // Same DOM node preserved (morph, not innerHTML)
402
- expect(main.children[0]).toBe(ref);
403
- expect(main.children[0].textContent).toBe('new');
404
- });
405
-
406
- it('morph() is chainable', () => {
407
- const col = queryAll('#main');
408
- const ret = col.morph('<p>m</p>');
409
- expect(ret).toBe(col);
410
- });
411
- });
412
-
413
-
414
- describe('DOM manipulation', () => {
415
- it('append() adds content', () => {
416
- const col = queryAll('#main');
417
- col.append('<div class="appended">new</div>');
418
- expect(document.querySelector('.appended').textContent).toBe('new');
419
- });
420
-
421
- it('prepend() adds at start', () => {
422
- const col = queryAll('#main');
423
- col.prepend('<div class="first">start</div>');
424
- expect(col.first().firstElementChild.className).toBe('first');
425
- });
426
-
427
- it('remove() removes elements', () => {
428
- queryAll('.other').remove();
429
- expect(document.querySelector('.other')).toBeNull();
430
- });
431
-
432
- it('empty() clears content', () => {
433
- queryAll('#main').empty();
434
- expect(document.querySelector('#main').children.length).toBe(0);
435
- });
436
-
437
- it('clone() creates deep copy', () => {
438
- const col = queryAll('.text').eq(0);
439
- const clone = col.clone();
440
- expect(clone.first().textContent).toBe('Hello');
441
- expect(clone.first()).not.toBe(col.first());
442
- });
443
- });
444
-
445
-
446
- describe('visibility', () => {
447
- it('hide() sets display none', () => {
448
- queryAll('#main').hide();
449
- expect(document.getElementById('main').style.display).toBe('none');
450
- });
451
-
452
- it('show() clears display', () => {
453
- const main = document.getElementById('main');
454
- main.style.display = 'none';
455
- queryAll('#main').show();
456
- expect(main.style.display).toBe('');
457
- });
458
- });
459
-
460
-
461
- describe('events', () => {
462
- it('on() and trigger()', () => {
463
- let clicked = false;
464
- const col = queryAll('#main');
465
- col.on('click', () => { clicked = true; });
466
- col.trigger('click');
467
- expect(clicked).toBe(true);
468
- });
469
-
470
- it('off() removes handler', () => {
471
- let count = 0;
472
- const handler = () => { count++; };
473
- const col = queryAll('#main');
474
- col.on('click', handler);
475
- col.trigger('click');
476
- col.off('click', handler);
477
- col.trigger('click');
478
- expect(count).toBe(1);
479
- });
480
-
481
- it('on() works on non-Element targets like window', () => {
482
- let fired = false;
483
- const col = new ZQueryCollection([window]);
484
- col.on('custom-win-evt', () => { fired = true; });
485
- window.dispatchEvent(new Event('custom-win-evt'));
486
- expect(fired).toBe(true);
487
- });
488
-
489
- it('$.on() with EventTarget binds directly to that target', () => {
490
- let fired = false;
491
- const target = new EventTarget();
492
- query.on('test-evt', target, () => { fired = true; });
493
- target.dispatchEvent(new Event('test-evt'));
494
- expect(fired).toBe(true);
495
- });
496
- });
497
- });
498
-
499
-
500
- // ---------------------------------------------------------------------------
501
- // Quick ref helpers
502
- // ---------------------------------------------------------------------------
503
-
504
- describe('query quick refs', () => {
505
- it('$.id() returns element by id', () => {
506
- expect(query.id('main')).toBe(document.getElementById('main'));
507
- });
508
-
509
- it('$.class() returns first element by class', () => {
510
- expect(query.class('text').textContent).toBe('Hello');
511
- });
512
-
513
- it('$.classes() returns ZQueryCollection', () => {
514
- const col = query.classes('text');
515
- expect(col).toBeInstanceOf(ZQueryCollection);
516
- expect(col.length).toBe(3);
517
- });
518
-
519
- it('$.children() returns ZQueryCollection', () => {
520
- const col = query.children('main');
521
- expect(col).toBeInstanceOf(ZQueryCollection);
522
- expect(col.length).toBe(4);
523
- });
524
-
525
- it('$.tag() returns ZQueryCollection', () => {
526
- const col = query.tag('p');
527
- expect(col).toBeInstanceOf(ZQueryCollection);
528
- expect(col.length).toBe(3);
529
- });
530
-
531
- describe('collection forEach() works like Array.forEach()', () => {
532
- it('iterates with el, index, array args', () => {
533
- const results = [];
534
- query.classes('text').forEach((el, i) => results.push({ i, text: el.textContent }));
535
- expect(results).toEqual([{ i: 0, text: 'Hello' }, { i: 1, text: 'World' }, { i: 2, text: 'Extra' }]);
536
- });
537
- });
538
-
539
- it('$.create() creates element with attributes', () => {
540
- const col = query.create('div', { class: 'new', id: 'created' }, 'text');
541
- expect(col).toBeInstanceOf(ZQueryCollection);
542
- expect(col.length).toBe(1);
543
- const el = col[0];
544
- expect(el.tagName).toBe('DIV');
545
- expect(el.className).toBe('new');
546
- expect(el.id).toBe('created');
547
- expect(el.textContent).toBe('text');
548
- });
549
-
550
- // qs / qsa - raw DOM shortcuts
551
- it('$.qs() returns raw element by CSS selector', () => {
552
- const el = query.qs('#main');
553
- expect(el).toBe(document.getElementById('main'));
554
- expect(el).toBeInstanceOf(HTMLElement);
555
- });
556
-
557
- it('$.qs() returns null for non-matching selector', () => {
558
- expect(query.qs('#nonexistent')).toBeNull();
559
- });
560
-
561
- it('$.qs() scopes to context element', () => {
562
- const sidebar = document.getElementById('sidebar');
563
- const el = query.qs('.nav-item', sidebar);
564
- expect(el.textContent).toBe('Home');
565
- });
566
-
567
- it('$.qsa() returns array of raw elements', () => {
568
- const els = query.qsa('.text');
569
- expect(Array.isArray(els)).toBe(true);
570
- expect(els.length).toBe(3);
571
- expect(els[0]).toBeInstanceOf(HTMLElement);
572
- });
573
-
574
- it('$.qsa() returns empty array for non-matching selector', () => {
575
- const els = query.qsa('.nonexistent');
576
- expect(els).toEqual([]);
577
- });
578
-
579
- it('$.qsa() scopes to context element', () => {
580
- const nav = document.getElementById('nav');
581
- const els = query.qsa('.nav-item', nav);
582
- expect(els.length).toBe(3);
583
- expect(els[0].textContent).toBe('Home');
584
- });
585
-
586
- it('$.qsa() result supports Array methods', () => {
587
- const names = query.qsa('.nav-item').map(el => el.textContent);
588
- expect(names).toEqual(['Home', 'About', 'Contact']);
589
- });
590
- });
591
-
592
-
593
- // ---------------------------------------------------------------------------
594
- // New filtering / collection methods
595
- // ---------------------------------------------------------------------------
596
-
597
- describe('filtering & collection', () => {
598
- it('is() checks if any element matches selector', () => {
599
- expect(queryAll('#main').children().is('p')).toBe(true);
600
- expect(queryAll('#main').children().is('table')).toBe(false);
601
- });
602
-
603
- it('is() with function', () => {
604
- const result = queryAll('.text').is(function(i) { return this.textContent === 'World'; });
605
- expect(result).toBe(true);
606
- });
607
-
608
- it('has() keeps elements containing matching descendant', () => {
609
- const col = queryAll('#sidebar').has('.nav-item');
610
- expect(col.length).toBe(1);
611
- });
612
-
613
- it('slice() returns subset', () => {
614
- const col = queryAll('.text').slice(0, 2);
615
- expect(col.length).toBe(2);
616
- expect(col.first().textContent).toBe('Hello');
617
- });
618
-
619
- it('slice() with negative index', () => {
620
- const col = queryAll('.text').slice(-1);
621
- expect(col.length).toBe(1);
622
- expect(col.first().textContent).toBe('Extra');
623
- });
624
-
625
- it('add() merges collections', () => {
626
- const col = queryAll('.first-p').add('.other');
627
- expect(col.length).toBe(2);
628
- });
629
-
630
- it('add() with element', () => {
631
- const el = document.getElementById('main');
632
- const col = queryAll('.first-p').add(el);
633
- expect(col.length).toBe(2);
634
- });
635
-
636
- it('add() with ZQueryCollection', () => {
637
- const col = queryAll('.first-p').add(queryAll('.other'));
638
- expect(col.length).toBe(2);
639
- });
640
-
641
- it('get() with no args returns array', () => {
642
- const arr = queryAll('.text').get();
643
- expect(Array.isArray(arr)).toBe(true);
644
- expect(arr.length).toBe(3);
645
- });
646
-
647
- it('get(index) returns element', () => {
648
- const el = queryAll('.text').get(1);
649
- expect(el.textContent).toBe('World');
650
- });
651
-
652
- it('get(negative) returns from end', () => {
653
- const el = queryAll('.text').get(-1);
654
- expect(el.textContent).toBe('Extra');
655
- });
656
-
657
- it('index() returns position among siblings', () => {
658
- const col = queryAll('.other');
659
- expect(col.index()).toBe(2); // 3rd child (0-indexed)
660
- });
661
-
662
- it('index(element) returns position in collection', () => {
663
- const other = document.querySelector('.other');
664
- const col = queryAll('#main').children();
665
- expect(col.index(other)).toBe(2);
666
- });
667
- });
668
-
669
-
670
- // ---------------------------------------------------------------------------
671
- // Inverse DOM manipulation (appendTo, prependTo, insertAfter, insertBefore, etc.)
672
- // ---------------------------------------------------------------------------
673
-
674
- describe('inverse DOM manipulation', () => {
675
- it('appendTo() moves elements into target', () => {
676
- queryAll('<div class="new-item">new</div>').appendTo('#nav');
677
- const nav = document.getElementById('nav');
678
- expect(nav.lastElementChild.className).toBe('new-item');
679
- });
680
-
681
- it('prependTo() inserts at start of target', () => {
682
- queryAll('<li class="first-item">first</li>').prependTo('#nav');
683
- const nav = document.getElementById('nav');
684
- expect(nav.firstElementChild.className).toBe('first-item');
685
- });
686
-
687
- it('insertAfter() inserts after target', () => {
688
- queryAll('<span class="inserted">!</span>').insertAfter('.first-p');
689
- const firstP = document.querySelector('.first-p');
690
- expect(firstP.nextElementSibling.className).toBe('inserted');
691
- });
692
-
693
- it('insertBefore() inserts before target', () => {
694
- queryAll('<span class="inserted">!</span>').insertBefore('.second-p');
695
- const secondP = document.querySelector('.second-p');
696
- expect(secondP.previousElementSibling.className).toBe('inserted');
697
- });
698
-
699
- it('replaceAll() replaces target elements', () => {
700
- queryAll('<em>replaced</em>').replaceAll('.other');
701
- expect(document.querySelector('.other')).toBeNull();
702
- expect(document.querySelector('em').textContent).toBe('replaced');
703
- });
704
-
705
- it('unwrap() removes parent wrapper', () => {
706
- // wrap nav-items in a div first
707
- const items = queryAll('.nav-item');
708
- const count = items.length;
709
- items.eq(0).wrap('<div class="wrapper"></div>');
710
- expect(document.querySelector('.wrapper')).not.toBeNull();
711
- queryAll('.nav-item').eq(0).unwrap('.wrapper');
712
- expect(document.querySelector('.wrapper')).toBeNull();
713
- });
714
-
715
- it('wrapAll() wraps all elements in one wrapper', () => {
716
- queryAll('.nav-item').wrapAll('<div class="all-wrap"></div>');
717
- const wrap = document.querySelector('.all-wrap');
718
- expect(wrap).not.toBeNull();
719
- expect(wrap.children.length).toBe(3);
720
- });
721
-
722
- it('wrapInner() wraps inner contents', () => {
723
- queryAll('.first-p').wrapInner('<strong></strong>');
724
- const strong = document.querySelector('.first-p > strong');
725
- expect(strong).not.toBeNull();
726
- expect(strong.textContent).toBe('Hello');
727
- });
728
-
729
- it('detach() removes elements (alias for remove)', () => {
730
- queryAll('.other').detach();
731
- expect(document.querySelector('.other')).toBeNull();
732
- });
733
- });
734
-
735
-
736
- // ---------------------------------------------------------------------------
737
- // CSS dimension methods
738
- // ---------------------------------------------------------------------------
739
-
740
- describe('CSS dimension methods', () => {
741
- it('scrollTop() get returns a number', () => {
742
- const val = queryAll('#main').scrollTop();
743
- expect(typeof val).toBe('number');
744
- });
745
-
746
- it('scrollTop(value) sets scroll position', () => {
747
- const main = document.getElementById('main');
748
- main.style.overflow = 'auto';
749
- main.style.height = '10px';
750
- main.innerHTML = '<div style="height:1000px">tall</div>';
751
- queryAll('#main').scrollTop(50);
752
- expect(main.scrollTop).toBe(50);
753
- });
754
-
755
- it('scrollLeft() get returns a number', () => {
756
- const val = queryAll('#main').scrollLeft();
757
- expect(typeof val).toBe('number');
758
- });
759
-
760
- it('innerWidth() returns clientWidth', () => {
761
- const main = document.getElementById('main');
762
- // jsdom sets clientWidth to 0 but the method should still return a number
763
- const val = queryAll('#main').innerWidth();
764
- expect(typeof val).toBe('number');
765
- });
766
-
767
- it('innerHeight() returns clientHeight', () => {
768
- const val = queryAll('#main').innerHeight();
769
- expect(typeof val).toBe('number');
770
- });
771
-
772
- it('outerWidth() returns offsetWidth', () => {
773
- const val = queryAll('#main').outerWidth();
774
- expect(typeof val).toBe('number');
775
- });
776
-
777
- it('outerHeight() returns offsetHeight', () => {
778
- const val = queryAll('#main').outerHeight();
779
- expect(typeof val).toBe('number');
780
- });
781
-
782
- it('outerWidth(true) includes margin', () => {
783
- const main = document.getElementById('main');
784
- main.style.margin = '10px';
785
- const val = queryAll('#main').outerWidth(true);
786
- expect(typeof val).toBe('number');
787
- });
788
- });
789
-
790
-
791
- // ---------------------------------------------------------------------------
792
- // prop() method
793
- // ---------------------------------------------------------------------------
794
-
795
- describe('ZQueryCollection - prop()', () => {
796
- it('gets a DOM property', () => {
797
- document.body.innerHTML = '<input type="checkbox" checked>';
798
- const col = queryAll('input');
799
- expect(col.prop('checked')).toBe(true);
800
- });
801
-
802
- it('sets a DOM property', () => {
803
- document.body.innerHTML = '<input type="checkbox">';
804
- const col = queryAll('input');
805
- col.prop('checked', true);
806
- expect(col[0].checked).toBe(true);
807
- });
808
-
809
- it('sets multiple properties via sequential calls', () => {
810
- document.body.innerHTML = '<input type="text">';
811
- const col = queryAll('input');
812
- col.prop('disabled', true);
813
- col.prop('value', 'hello');
814
- expect(col[0].disabled).toBe(true);
815
- expect(col[0].value).toBe('hello');
816
- });
817
- });
818
-
819
-
820
- // ---------------------------------------------------------------------------
821
- // css() method
822
- // ---------------------------------------------------------------------------
823
-
824
- describe('ZQueryCollection - css()', () => {
825
- beforeEach(() => {
826
- document.body.innerHTML = '<div id="styled">test</div>';
827
- });
828
-
829
- it('sets style properties from object', () => {
830
- queryAll('#styled').css({ color: 'red', 'font-size': '16px' });
831
- const el = document.getElementById('styled');
832
- expect(el.style.color).toBe('red');
833
- });
834
-
835
- it('returns collection for chaining', () => {
836
- const col = queryAll('#styled').css({ color: 'blue' });
837
- expect(col).toBeInstanceOf(ZQueryCollection);
838
- });
839
- });
840
-
841
-
842
- // ---------------------------------------------------------------------------
843
- // val() method
844
- // ---------------------------------------------------------------------------
845
-
846
- describe('ZQueryCollection - val()', () => {
847
- it('gets input value', () => {
848
- document.body.innerHTML = '<input value="test">';
849
- expect(queryAll('input').val()).toBe('test');
850
- });
851
-
852
- it('sets input value', () => {
853
- document.body.innerHTML = '<input value="">';
854
- queryAll('input').val('new value');
855
- expect(document.querySelector('input').value).toBe('new value');
856
- });
857
-
858
- it('gets select value', () => {
859
- document.body.innerHTML = '<select><option value="a" selected>A</option><option value="b">B</option></select>';
860
- expect(queryAll('select').val()).toBe('a');
861
- });
862
-
863
- it('gets textarea value', () => {
864
- document.body.innerHTML = '<textarea>hello</textarea>';
865
- expect(queryAll('textarea').val()).toBe('hello');
866
- });
867
- });
868
-
869
-
870
- // ---------------------------------------------------------------------------
871
- // after(), before() methods
872
- // ---------------------------------------------------------------------------
873
-
874
- describe('ZQueryCollection - after() / before()', () => {
875
- beforeEach(() => {
876
- document.body.innerHTML = '<div id="container"><p id="target">target</p></div>';
877
- });
878
-
879
- it('after() inserts content after element', () => {
880
- queryAll('#target').after('<span class="after">after</span>');
881
- const next = document.getElementById('target').nextElementSibling;
882
- expect(next.className).toBe('after');
883
- });
884
-
885
- it('before() inserts content before element', () => {
886
- queryAll('#target').before('<span class="before">before</span>');
887
- const prev = document.getElementById('target').previousElementSibling;
888
- expect(prev.className).toBe('before');
889
- });
890
- });
891
-
892
-
893
- // ---------------------------------------------------------------------------
894
- // wrap() method
895
- // ---------------------------------------------------------------------------
896
-
897
- describe('ZQueryCollection - wrap()', () => {
898
- it('wraps element in new parent', () => {
899
- document.body.innerHTML = '<div id="container"><p id="target">text</p></div>';
900
- queryAll('#target').wrap('<div class="wrapper"></div>');
901
- const wrapper = document.querySelector('.wrapper');
902
- expect(wrapper).not.toBeNull();
903
- expect(wrapper.querySelector('#target')).not.toBeNull();
904
- });
905
- });
906
-
907
-
908
- // ---------------------------------------------------------------------------
909
- // replaceWith() method
910
- // ---------------------------------------------------------------------------
911
-
912
- describe('ZQueryCollection - replaceWith()', () => {
913
- it('replaces element with new content', () => {
914
- document.body.innerHTML = '<div id="container"><p id="old">old</p></div>';
915
- queryAll('#old').replaceWith('<span id="new">new</span>');
916
- expect(document.querySelector('#old')).toBeNull();
917
- expect(document.querySelector('#new')).not.toBeNull();
918
- });
919
-
920
- it('auto-morphs when tag name matches (preserves identity)', () => {
921
- document.body.innerHTML = '<div id="container"><p id="target" class="old">old text</p></div>';
922
- const target = document.querySelector('#target');
923
- queryAll('#target').replaceWith('<p id="target" class="new">new text</p>');
924
- // Same DOM node - morphed, not replaced
925
- expect(document.querySelector('#target')).toBe(target);
926
- expect(target.className).toBe('new');
927
- expect(target.textContent).toBe('new text');
928
- });
929
-
930
- it('replaces when tag name differs', () => {
931
- document.body.innerHTML = '<div id="container"><p id="old">old</p></div>';
932
- const oldRef = document.querySelector('#old');
933
- queryAll('#old').replaceWith('<section id="replaced">new</section>');
934
- expect(document.querySelector('#replaced')).not.toBe(oldRef);
935
- expect(document.querySelector('#replaced').tagName).toBe('SECTION');
936
- });
937
- });
938
-
939
-
940
- // ---------------------------------------------------------------------------
941
- // offset() and position()
942
- // ---------------------------------------------------------------------------
943
-
944
- describe('ZQueryCollection - offset() / position()', () => {
945
- it('offset() returns object with top and left', () => {
946
- document.body.innerHTML = '<div id="box">box</div>';
947
- const off = queryAll('#box').offset();
948
- expect(off).toHaveProperty('top');
949
- expect(off).toHaveProperty('left');
950
- expect(typeof off.top).toBe('number');
951
- });
952
-
953
- it('position() returns object with top and left', () => {
954
- document.body.innerHTML = '<div id="box">box</div>';
955
- const pos = queryAll('#box').position();
956
- expect(pos).toHaveProperty('top');
957
- expect(pos).toHaveProperty('left');
958
- });
959
- });
960
-
961
-
962
- // ---------------------------------------------------------------------------
963
- // width() and height()
964
- // ---------------------------------------------------------------------------
965
-
966
- describe('ZQueryCollection - width() / height()', () => {
967
- it('width() returns a number', () => {
968
- document.body.innerHTML = '<div id="box" style="width:100px">box</div>';
969
- const val = queryAll('#box').width();
970
- expect(typeof val).toBe('number');
971
- });
972
-
973
- it('height() returns a number', () => {
974
- document.body.innerHTML = '<div id="box" style="height:50px">box</div>';
975
- const val = queryAll('#box').height();
976
- expect(typeof val).toBe('number');
977
- });
978
- });
979
-
980
-
981
- // ---------------------------------------------------------------------------
982
- // animate()
983
- // ---------------------------------------------------------------------------
984
-
985
- describe('ZQueryCollection - animate()', () => {
986
- it('returns a promise', () => {
987
- document.body.innerHTML = '<div id="box">box</div>';
988
- const result = queryAll('#box').animate({ opacity: 0 }, 100);
989
- // animate() returns a Promise
990
- expect(result).toBeInstanceOf(Promise);
991
- });
992
- });
993
-
994
-
995
- // ---------------------------------------------------------------------------
996
- // hover() convenience
997
- // ---------------------------------------------------------------------------
998
-
999
- describe('hover()', () => {
1000
- it('binds mouseenter and mouseleave', () => {
1001
- let entered = false, left = false;
1002
- const col = queryAll('#main');
1003
- col.hover(() => { entered = true; }, () => { left = true; });
1004
- col.first().dispatchEvent(new Event('mouseenter'));
1005
- col.first().dispatchEvent(new Event('mouseleave'));
1006
- expect(entered).toBe(true);
1007
- expect(left).toBe(true);
1008
- });
1009
-
1010
- it('uses same fn for both if only one provided', () => {
1011
- let count = 0;
1012
- const col = queryAll('#main');
1013
- col.hover(() => { count++; });
1014
- col.first().dispatchEvent(new Event('mouseenter'));
1015
- col.first().dispatchEvent(new Event('mouseleave'));
1016
- expect(count).toBe(2);
1017
- });
1018
- });
1019
-
1020
-
1021
- // ---------------------------------------------------------------------------
1022
- // ZQueryCollection - empty collection safety
1023
- // ---------------------------------------------------------------------------
1024
-
1025
- describe('ZQueryCollection - empty collection operations', () => {
1026
- it('first() returns null on empty', () => {
1027
- expect(queryAll('.nonexistent').first()).toBeNull();
1028
- });
1029
-
1030
- it('last() returns null on empty', () => {
1031
- expect(queryAll('.nonexistent').last()).toBeNull();
1032
- });
1033
-
1034
- it('each() on empty collection does not call callback', () => {
1035
- const fn = vi.fn();
1036
- queryAll('.nonexistent').each(fn);
1037
- expect(fn).not.toHaveBeenCalled();
1038
- });
1039
-
1040
- it('map() on empty returns empty array', () => {
1041
- expect(queryAll('.nonexistent').map(el => el)).toEqual([]);
1042
- });
1043
-
1044
- it('html() get on empty returns undefined', () => {
1045
- const result = queryAll('.nonexistent').html();
1046
- expect(result).toBeUndefined();
1047
- });
1048
-
1049
- it('text() get on empty returns undefined', () => {
1050
- const result = queryAll('.nonexistent').text();
1051
- expect(result).toBeUndefined();
1052
- });
1053
-
1054
- it('val() get on empty returns undefined', () => {
1055
- expect(queryAll('.nonexistent').val()).toBeUndefined();
1056
- });
1057
-
1058
- it('addClass on empty does not throw', () => {
1059
- expect(() => queryAll('.nonexistent').addClass('test')).not.toThrow();
1060
- });
1061
-
1062
- it('attr() get on empty returns undefined', () => {
1063
- expect(queryAll('.nonexistent').attr('id')).toBeUndefined();
1064
- });
1065
-
1066
- it('chaining on empty collection', () => {
1067
- const col = queryAll('.nonexistent');
1068
- const result = col.addClass('x').removeClass('x').toggleClass('y');
1069
- expect(result).toBeInstanceOf(ZQueryCollection);
1070
- expect(result.length).toBe(0);
1071
- });
1072
- });
1073
-
1074
-
1075
- // ---------------------------------------------------------------------------
1076
- // Collection wrapping edge cases
1077
- // ---------------------------------------------------------------------------
1078
-
1079
- describe('query - wrapping edge cases', () => {
1080
- it('wraps an HTMLCollection', () => {
1081
- const col = queryAll(document.getElementsByClassName('text'));
1082
- expect(col).toBeInstanceOf(ZQueryCollection);
1083
- expect(col.length).toBe(3);
1084
- });
1085
-
1086
- it('wraps a NodeList', () => {
1087
- const col = queryAll(document.querySelectorAll('.text'));
1088
- expect(col).toBeInstanceOf(ZQueryCollection);
1089
- expect(col.length).toBe(3);
1090
- });
1091
-
1092
- it('wraps an Array of elements', () => {
1093
- const arr = [document.getElementById('main'), document.getElementById('sidebar')];
1094
- const col = queryAll(arr);
1095
- expect(col.length).toBe(2);
1096
- expect(col.first().id).toBe('main');
1097
- });
1098
-
1099
- it('query() wraps ZQueryCollection (returns as-is)', () => {
1100
- const original = queryAll('.text');
1101
- const wrapped = query(original);
1102
- expect(wrapped).toBe(original);
1103
- });
1104
-
1105
- it('creates multiple elements from HTML', () => {
1106
- const col = queryAll('<p>a</p><p>b</p><p>c</p>');
1107
- expect(col.length).toBe(3);
1108
- expect(col.first().textContent).toBe('a');
1109
- expect(col.last().textContent).toBe('c');
1110
- });
1111
- });
1112
-
1113
-
1114
- // ---------------------------------------------------------------------------
1115
- // html() morphing advanced
1116
- // ---------------------------------------------------------------------------
1117
-
1118
- describe('ZQueryCollection - html() morphing advanced', () => {
1119
- it('morphs complex nested structure', () => {
1120
- document.body.innerHTML = '<div id="m"><ul><li id="i1">old1</li><li id="i2">old2</li></ul></div>';
1121
- const li1 = document.getElementById('i1');
1122
- queryAll('#m').html('<ul><li id="i1">new1</li><li id="i2">new2</li><li id="i3">new3</li></ul>');
1123
- // li1 should be preserved (same id → morph)
1124
- expect(document.getElementById('i1')).toBe(li1);
1125
- expect(li1.textContent).toBe('new1');
1126
- expect(document.querySelectorAll('#m li').length).toBe(3);
1127
- });
1128
-
1129
- it('morph() handles tag change at root', () => {
1130
- document.body.innerHTML = '<div id="m"><p>old</p></div>';
1131
- queryAll('#m').morph('<span>new</span>');
1132
- expect(document.querySelector('#m span')).not.toBeNull();
1133
- expect(document.querySelector('#m p')).toBeNull();
1134
- });
1135
- });
1136
-
1137
-
1138
- // ---------------------------------------------------------------------------
1139
- // Event delegation
1140
- // ---------------------------------------------------------------------------
1141
-
1142
- describe('ZQueryCollection - event delegation', () => {
1143
- it('on() with selector delegates to matching children', () => {
1144
- let clicked = null;
1145
- queryAll('#nav').on('click', '.nav-item', function() { clicked = this.textContent; });
1146
- document.querySelector('.nav-item.active').click();
1147
- expect(clicked).toBe('Home');
1148
- });
1149
-
1150
- it('delegated event does not fire for non-matching elements', () => {
1151
- let fired = false;
1152
- queryAll('#main').on('click', '.nonexistent', () => { fired = true; });
1153
- document.querySelector('.text').click();
1154
- expect(fired).toBe(false);
1155
- });
1156
- });
1157
-
1158
-
1159
- // ---------------------------------------------------------------------------
1160
- // Multiple class operations
1161
- // ---------------------------------------------------------------------------
1162
-
1163
- describe('ZQueryCollection - multiple class operations', () => {
1164
- it('addClass with space-separated classes', () => {
1165
- const col = queryAll('#main');
1166
- col.addClass('a', 'b', 'c');
1167
- expect(col.first().classList.contains('a')).toBe(true);
1168
- expect(col.first().classList.contains('b')).toBe(true);
1169
- expect(col.first().classList.contains('c')).toBe(true);
1170
- });
1171
-
1172
- it('removeClass with multiple classes', () => {
1173
- const col = queryAll('#main');
1174
- col.addClass('x', 'y', 'z');
1175
- col.removeClass('x', 'z');
1176
- expect(col.first().classList.contains('x')).toBe(false);
1177
- expect(col.first().classList.contains('y')).toBe(true);
1178
- expect(col.first().classList.contains('z')).toBe(false);
1179
- });
1180
- });
1181
-
1182
-
1183
- // ---------------------------------------------------------------------------
1184
- // Traversal edge cases
1185
- // ---------------------------------------------------------------------------
1186
-
1187
- describe('ZQueryCollection - traversal edge cases', () => {
1188
- it('find() returns empty when no descendants match', () => {
1189
- expect(queryAll('#main').find('.nonexistent').length).toBe(0);
1190
- });
1191
-
1192
- it('parent() on body returns html', () => {
1193
- const parents = queryAll('body').parent();
1194
- expect(parents.first().tagName).toBe('HTML');
1195
- });
1196
-
1197
- it('children() with selector filters', () => {
1198
- const col = queryAll('#main').children('.text');
1199
- expect(col.length).toBe(3);
1200
- });
1201
-
1202
- it('closest() returns self if it matches', () => {
1203
- const col = queryAll('#main');
1204
- expect(col.closest('#main').first()).toBe(document.getElementById('main'));
1205
- });
1206
-
1207
- it('closest() returns empty when no match', () => {
1208
- expect(queryAll('.text').closest('.nonexistent').length).toBe(0);
1209
- });
1210
-
1211
- it('siblings() returns all siblings', () => {
1212
- const sibs = queryAll('.first-p').siblings();
1213
- // siblings() returns all sibling elements except self
1214
- expect(sibs.length).toBeGreaterThanOrEqual(2);
1215
- });
1216
-
1217
- it('next() at end returns empty', () => {
1218
- const col = queryAll('.third-p');
1219
- expect(col.next().length).toBe(0);
1220
- });
1221
-
1222
- it('prev() at start returns empty', () => {
1223
- const col = queryAll('.first-p');
1224
- expect(col.prev().length).toBe(0);
1225
- });
1226
- });
1227
-
1228
-
1229
- // ---------------------------------------------------------------------------
1230
- // DOM manipulation edge cases
1231
- // ---------------------------------------------------------------------------
1232
-
1233
- describe('ZQueryCollection - DOM manipulation edge cases', () => {
1234
- it('append with element node', () => {
1235
- const newEl = document.createElement('div');
1236
- newEl.id = 'appended-el';
1237
- queryAll('#main').append(newEl);
1238
- expect(document.getElementById('appended-el')).not.toBeNull();
1239
- expect(document.getElementById('appended-el').parentElement.id).toBe('main');
1240
- });
1241
-
1242
- it('prepend with element node', () => {
1243
- const newEl = document.createElement('div');
1244
- newEl.id = 'prepended-el';
1245
- queryAll('#main').prepend(newEl);
1246
- expect(document.getElementById('main').firstElementChild.id).toBe('prepended-el');
1247
- });
1248
-
1249
- it('remove on already-removed element does not throw', () => {
1250
- const col = queryAll('.text').eq(0);
1251
- col.remove();
1252
- expect(() => col.remove()).not.toThrow();
1253
- });
1254
-
1255
- it('clone produces independent copy', () => {
1256
- const original = queryAll('.first-p');
1257
- const cloned = original.clone();
1258
- cloned.addClass('cloned-class');
1259
- expect(original.hasClass('cloned-class')).toBe(false);
1260
- expect(cloned.hasClass('cloned-class')).toBe(true);
1261
- });
1262
-
1263
- it('empty() on already empty element', () => {
1264
- document.body.innerHTML = '<div id="empty"></div>';
1265
- expect(() => queryAll('#empty').empty()).not.toThrow();
1266
- expect(document.getElementById('empty').children.length).toBe(0);
1267
- });
1268
- });
1269
-
1270
-
1271
- // ---------------------------------------------------------------------------
1272
- // Attribute edge cases
1273
- // ---------------------------------------------------------------------------
1274
-
1275
- describe('ZQueryCollection - attribute edge cases', () => {
1276
- it('attr() set with sequential calls sets multiple attributes', () => {
1277
- document.body.innerHTML = '<div id="a"></div>';
1278
- queryAll('#a').attr('data-x', '1').attr('data-y', '2').attr('title', 'test');
1279
- const el = document.getElementById('a');
1280
- expect(el.getAttribute('data-x')).toBe('1');
1281
- expect(el.getAttribute('data-y')).toBe('2');
1282
- expect(el.getAttribute('title')).toBe('test');
1283
- });
1284
-
1285
- it('data() returns undefined for missing key', () => {
1286
- expect(queryAll('#main').data('nonexistent')).toBeUndefined();
1287
- });
1288
-
1289
- it('removeAttr on nonexistent attribute does not throw', () => {
1290
- expect(() => queryAll('#main').removeAttr('data-nope')).not.toThrow();
1291
- });
1292
- });
1293
-
1294
-
1295
- // ---------------------------------------------------------------------------
1296
- // css() advanced
1297
- // ---------------------------------------------------------------------------
1298
-
1299
- describe('ZQueryCollection - css() advanced', () => {
1300
- it('sets a single style property via object', () => {
1301
- document.body.innerHTML = '<div id="s">test</div>';
1302
- queryAll('#s').css({ color: 'green' });
1303
- expect(document.getElementById('s').style.color).toBe('green');
1304
- });
1305
-
1306
- it('sets multiple CSS properties', () => {
1307
- document.body.innerHTML = '<div id="s2">test</div>';
1308
- queryAll('#s2').css({ color: 'red', 'font-weight': 'bold', display: 'flex' });
1309
- const el = document.getElementById('s2');
1310
- expect(el.style.color).toBe('red');
1311
- expect(el.style.display).toBe('flex');
1312
- });
1313
- });
1314
-
1315
-
1316
- // ---------------------------------------------------------------------------
1317
- // $.create advanced
1318
- // ---------------------------------------------------------------------------
1319
-
1320
- describe('query.create - advanced', () => {
1321
- it('creates element with no attributes', () => {
1322
- const col = query.create('span');
1323
- expect(col.length).toBe(1);
1324
- expect(col[0].tagName).toBe('SPAN');
1325
- });
1326
-
1327
- it('creates element with multiple children', () => {
1328
- const child1 = document.createElement('span');
1329
- child1.textContent = 'span child';
1330
- const col = query.create('div', {}, 'text', child1);
1331
- expect(col[0].childNodes.length).toBe(2);
1332
- expect(col[0].childNodes[0].textContent).toBe('text');
1333
- expect(col[0].querySelector('span').textContent).toBe('span child');
1334
- });
1335
-
1336
- it('creates element with boolean attributes', () => {
1337
- const col = query.create('input', { type: 'text', disabled: '' });
1338
- expect(col[0].tagName).toBe('INPUT');
1339
- expect(col[0].getAttribute('type')).toBe('text');
1340
- });
1341
- });
1342
-
1343
-
1344
- // ---------------------------------------------------------------------------
1345
- // Prop edge cases
1346
- // ---------------------------------------------------------------------------
1347
-
1348
- describe('ZQueryCollection - prop() edge cases', () => {
1349
- it('gets defaultValue property', () => {
1350
- document.body.innerHTML = '<input value="initial">';
1351
- const col = queryAll('input');
1352
- expect(col.prop('defaultValue')).toBe('initial');
1353
- });
1354
-
1355
- it('gets tagName property', () => {
1356
- const col = queryAll('#main');
1357
- expect(col.prop('tagName')).toBe('DIV');
1358
- });
1359
-
1360
- it('prop on empty collection returns undefined', () => {
1361
- expect(queryAll('.nonexistent').prop('checked')).toBeUndefined();
1362
- });
1363
- });
1364
-
1365
-
1366
- // ---------------------------------------------------------------------------
1367
- // BUG FIX: siblings() with selector filtering + null parent guard
1368
- // ---------------------------------------------------------------------------
1369
-
1370
- describe('ZQueryCollection - siblings() fixes', () => {
1371
- it('filters siblings by selector', () => {
1372
- document.body.innerHTML = '<div><p class="a">1</p><p class="b">2</p><p class="a">3</p></div>';
1373
- const sibs = queryAll('.b').siblings('.a');
1374
- expect(sibs.length).toBe(2);
1375
- });
1376
-
1377
- it('returns all siblings when no selector given', () => {
1378
- document.body.innerHTML = '<div><p>1</p><p id="mid">2</p><p>3</p></div>';
1379
- const sibs = queryAll('#mid').siblings();
1380
- expect(sibs.length).toBe(2);
1381
- });
1382
-
1383
- it('does not crash on detached element (no parentElement)', () => {
1384
- const detached = document.createElement('div');
1385
- const col = new ZQueryCollection([detached]);
1386
- expect(() => col.siblings()).not.toThrow();
1387
- expect(col.siblings().length).toBe(0);
1388
- });
1389
- });
1390
-
1391
-
1392
- // ---------------------------------------------------------------------------
1393
- // BUG FIX: ZQueryCollection constructor null safety
1394
- // ---------------------------------------------------------------------------
1395
-
1396
- describe('ZQueryCollection - constructor null/undefined safety', () => {
1397
- it('creates empty collection from null', () => {
1398
- const col = new ZQueryCollection(null);
1399
- expect(col.length).toBe(0);
1400
- });
1401
-
1402
- it('creates empty collection from undefined', () => {
1403
- const col = new ZQueryCollection(undefined);
1404
- expect(col.length).toBe(0);
1405
- });
1406
-
1407
- it('wraps a single element', () => {
1408
- const el = document.createElement('div');
1409
- const col = new ZQueryCollection(el);
1410
- expect(col.length).toBe(1);
1411
- expect(col[0]).toBe(el);
1412
- });
1413
- });
1414
-
1415
-
1416
- // ---------------------------------------------------------------------------
1417
- // BUG FIX: attr() with object syntax
1418
- // ---------------------------------------------------------------------------
1419
-
1420
- describe('ZQueryCollection - attr() object set', () => {
1421
- it('sets multiple attributes with object', () => {
1422
- document.body.innerHTML = '<div id="at"></div>';
1423
- queryAll('#at').attr({ 'data-x': '1', 'data-y': '2', title: 'hello' });
1424
- const el = document.getElementById('at');
1425
- expect(el.getAttribute('data-x')).toBe('1');
1426
- expect(el.getAttribute('data-y')).toBe('2');
1427
- expect(el.getAttribute('title')).toBe('hello');
1428
- });
1429
- });
1430
-
1431
-
1432
- // ---------------------------------------------------------------------------
1433
- // BUG FIX: css() two-argument setter
1434
- // ---------------------------------------------------------------------------
1435
-
1436
- describe('ZQueryCollection - css() two-argument setter', () => {
1437
- it('sets a CSS property with key-value arguments', () => {
1438
- document.body.innerHTML = '<div id="cs">text</div>';
1439
- queryAll('#cs').css('color', 'green');
1440
- expect(document.getElementById('cs').style.color).toBe('green');
1441
- });
1442
-
1443
- it('still works as getter with single string arg', () => {
1444
- document.body.innerHTML = '<div id="cs2" style="color: red;">text</div>';
1445
- const val = queryAll('#cs2').css('color');
1446
- expect(val).toBeDefined();
1447
- });
1448
-
1449
- it('still works as setter with object arg', () => {
1450
- document.body.innerHTML = '<div id="cs3">text</div>';
1451
- queryAll('#cs3').css({ color: 'blue', display: 'flex' });
1452
- const el = document.getElementById('cs3');
1453
- expect(el.style.color).toBe('blue');
1454
- expect(el.style.display).toBe('flex');
1455
- });
1456
- });
1457
-
1458
-
1459
- // ---------------------------------------------------------------------------
1460
- // BUG FIX: wrap() does not crash on empty/invalid wrapper
1461
- // ---------------------------------------------------------------------------
1462
-
1463
- describe('ZQueryCollection - wrap() safety', () => {
1464
- it('does not crash if wrapper string is empty', () => {
1465
- document.body.innerHTML = '<div id="w"><p>inside</p></div>';
1466
- expect(() => queryAll('#w p').wrap('')).not.toThrow();
1467
- });
1468
-
1469
- it('does not crash on detached element (no parentNode)', () => {
1470
- const detached = document.createElement('span');
1471
- const col = new ZQueryCollection([detached]);
1472
- expect(() => col.wrap('<div></div>')).not.toThrow();
1473
- });
1474
- });
1475
-
1476
-
1477
- // ---------------------------------------------------------------------------
1478
- // BUG FIX: index() does not crash on detached element
1479
- // ---------------------------------------------------------------------------
1480
-
1481
- describe('ZQueryCollection - index() null parent safety', () => {
1482
- it('returns -1 for detached element', () => {
1483
- const detached = document.createElement('div');
1484
- const col = new ZQueryCollection([detached]);
1485
- expect(col.index()).toBe(-1);
1486
- });
1487
- });
1488
-
1489
-
1490
- // ---------------------------------------------------------------------------
1491
- // BUG FIX: delegated on() / off() handler removal
1492
- // ---------------------------------------------------------------------------
1493
-
1494
- describe('ZQueryCollection - delegated on/off', () => {
1495
- it('off() removes delegated event handlers', () => {
1496
- document.body.innerHTML = '<div id="parent"><button class="btn">click</button></div>';
1497
- const parent = new ZQueryCollection([document.getElementById('parent')]);
1498
- const handler = vi.fn();
1499
-
1500
- parent.on('click', '.btn', handler);
1501
- document.querySelector('.btn').click();
1502
- expect(handler).toHaveBeenCalledTimes(1);
1503
-
1504
- parent.off('click', handler);
1505
- document.querySelector('.btn').click();
1506
- // Should not fire again after off()
1507
- expect(handler).toHaveBeenCalledTimes(1);
1508
- });
1509
- });
1510
-
1511
-
1512
- // ---------------------------------------------------------------------------
1513
- // BUG FIX: animate() resolves immediately on empty collection
1514
- // ---------------------------------------------------------------------------
1515
-
1516
- describe('ZQueryCollection - animate() empty collection', () => {
1517
- it('resolves immediately when collection is empty', async () => {
1518
- const col = new ZQueryCollection([]);
1519
- const result = await col.animate({ opacity: '0' }, 50);
1520
- expect(result).toBe(col);
1521
- });
1522
- });
1523
-
1524
-
1525
- // ===========================================================================
1526
- // one() - single-fire event listener
1527
- // ===========================================================================
1528
-
1529
- describe('ZQueryCollection - one()', () => {
1530
- it('fires handler only once', () => {
1531
- const handler = vi.fn();
1532
- document.body.innerHTML = '<button id="one-btn">click</button>';
1533
- const col = query('#one-btn');
1534
- col.one('click', handler);
1535
- document.querySelector('#one-btn').click();
1536
- document.querySelector('#one-btn').click();
1537
- expect(handler).toHaveBeenCalledTimes(1);
1538
- });
1539
- });
1540
-
1541
-
1542
- // ===========================================================================
1543
- // toggle() - show/hide toggle
1544
- // ===========================================================================
1545
-
1546
- describe('ZQueryCollection - toggle()', () => {
1547
- it('hides a visible element', () => {
1548
- const el = document.querySelector('#main');
1549
- el.style.display = '';
1550
- const col = query('#main');
1551
- col.toggle();
1552
- expect(el.style.display).toBe('none');
1553
- });
1554
-
1555
- it('shows a hidden element', () => {
1556
- const el = document.querySelector('#main');
1557
- el.style.display = 'none';
1558
- const col = query('#main');
1559
- col.toggle();
1560
- expect(el.style.display).toBe('');
1561
- });
1562
-
1563
- it('uses custom display value when showing', () => {
1564
- const el = document.querySelector('#main');
1565
- el.style.display = 'none';
1566
- const col = query('#main');
1567
- col.toggle('flex');
1568
- expect(el.style.display).toBe('flex');
1569
- });
1570
- });
1571
-
1572
-
1573
- // ===========================================================================
1574
- // serialize() and serializeObject()
1575
- // ===========================================================================
1576
-
1577
- describe('ZQueryCollection - serialize()', () => {
1578
- it('serializes form inputs to URL-encoded string', () => {
1579
- document.body.innerHTML = '<form id="f"><input name="user" value="Alice"><input name="age" value="30"></form>';
1580
- const result = query('#f').serialize();
1581
- expect(result).toContain('user=Alice');
1582
- expect(result).toContain('age=30');
1583
- });
1584
-
1585
- it('returns empty string for non-form element', () => {
1586
- expect(query('#main').serialize()).toBe('');
1587
- });
1588
- });
1589
-
1590
- describe('ZQueryCollection - serializeObject()', () => {
1591
- it('builds an object from form fields', () => {
1592
- document.body.innerHTML = '<form id="f"><input name="user" value="Alice"><input name="age" value="30"></form>';
1593
- expect(query('#f').serializeObject()).toEqual({ user: 'Alice', age: '30' });
1594
- });
1595
-
1596
- it('groups duplicate keys into arrays', () => {
1597
- document.body.innerHTML = `<form id="f">
1598
- <input name="tags" value="a">
1599
- <input name="tags" value="b">
1600
- <input name="tags" value="c">
1601
- </form>`;
1602
- expect(query('#f').serializeObject()).toEqual({ tags: ['a', 'b', 'c'] });
1603
- });
1604
-
1605
- it('returns empty object for non-form element', () => {
1606
- expect(query('#main').serializeObject()).toEqual({});
1607
- });
1608
- });
1609
-
1610
-
1611
- // ===========================================================================
1612
- // $.ready
1613
- // ===========================================================================
1614
-
1615
- describe('$.ready', () => {
1616
- it('calls function immediately when document is not loading', () => {
1617
- const fn = vi.fn();
1618
- query.ready(fn);
1619
- expect(fn).toHaveBeenCalledTimes(1);
1620
- });
1621
- });
1622
-
1623
-
1624
- // ===========================================================================
1625
- // $.name
1626
- // ===========================================================================
1627
-
1628
- describe('$.name', () => {
1629
- it('selects elements by name attribute', () => {
1630
- document.body.innerHTML = '<input name="email" value="a@b.com"><input name="email" value="x@y.com"><input name="other">';
1631
- const result = query.name('email');
1632
- expect(result.length).toBe(2);
1633
- });
1634
- });
1635
-
1636
-
1637
- // ===========================================================================
1638
- // $.create
1639
- // ===========================================================================
1640
-
1641
- describe('$.create', () => {
1642
- it('creates an element with attributes', () => {
1643
- const col = query.create('div', { id: 'test', class: 'box' }, 'hello');
1644
- const el = col.first();
1645
- expect(el.tagName).toBe('DIV');
1646
- expect(el.id).toBe('test');
1647
- expect(el.className).toBe('box');
1648
- expect(el.textContent).toBe('hello');
1649
- });
1650
-
1651
- it('applies style object', () => {
1652
- const col = query.create('span', { style: { color: 'red', fontSize: '20px' } });
1653
- const el = col.first();
1654
- expect(el.style.color).toBe('red');
1655
- expect(el.style.fontSize).toBe('20px');
1656
- });
1657
-
1658
- it('binds event handlers via on* attributes', () => {
1659
- const handler = vi.fn();
1660
- const col = query.create('button', { onclick: handler }, 'click me');
1661
- col.first().click();
1662
- expect(handler).toHaveBeenCalledTimes(1);
1663
- });
1664
-
1665
- it('sets data attributes from data object', () => {
1666
- const col = query.create('div', { data: { userId: '42', role: 'admin' } });
1667
- const el = col.first();
1668
- expect(el.dataset.userId).toBe('42');
1669
- expect(el.dataset.role).toBe('admin');
1670
- });
1671
-
1672
- it('appends child Node elements', () => {
1673
- const child = document.createElement('span');
1674
- child.textContent = 'child';
1675
- const col = query.create('div', {}, child);
1676
- const el = col.first();
1677
- expect(el.children.length).toBe(1);
1678
- expect(el.querySelector('span').textContent).toBe('child');
1679
- });
1680
- });
1681
-
1682
-
1683
- // ===========================================================================
1684
- // data() - no key returns full dataset
1685
- // ===========================================================================
1686
-
1687
- describe('ZQueryCollection - data() full dataset', () => {
1688
- it('returns the full dataset when no key is given', () => {
1689
- document.body.innerHTML = '<div id="d" data-x="1" data-y="2"></div>';
1690
- const ds = query('#d').data();
1691
- expect(ds.x).toBe('1');
1692
- expect(ds.y).toBe('2');
1693
- });
1694
- });
1695
-
1696
-
1697
- // ===========================================================================
1698
- // css() getter on empty collection
1699
- // ===========================================================================
1700
-
1701
- describe('ZQueryCollection - css() empty collection', () => {
1702
- it('returns undefined when collection is empty', () => {
1703
- const col = new ZQueryCollection([]);
1704
- expect(col.css('color')).toBeUndefined();
1705
- });
1706
- });
1707
-
1708
-
1709
- // ===========================================================================
1710
- // append/prepend/after/before with Node
1711
- // ===========================================================================
1712
-
1713
- describe('ZQueryCollection - append/prepend with Node', () => {
1714
- it('appends a Node element', () => {
1715
- document.body.innerHTML = '<div id="container"><p>existing</p></div>';
1716
- const newNode = document.createElement('span');
1717
- newNode.textContent = 'appended';
1718
- query('#container').append(newNode);
1719
- expect(document.querySelector('#container span').textContent).toBe('appended');
1720
- expect(document.querySelector('#container').lastElementChild.tagName).toBe('SPAN');
1721
- });
1722
-
1723
- it('prepends a Node element', () => {
1724
- document.body.innerHTML = '<div id="container"><p>existing</p></div>';
1725
- const newNode = document.createElement('span');
1726
- newNode.textContent = 'prepended';
1727
- query('#container').prepend(newNode);
1728
- expect(document.querySelector('#container').firstElementChild.tagName).toBe('SPAN');
1729
- });
1730
-
1731
- it('appends a ZQueryCollection', () => {
1732
- document.body.innerHTML = '<div id="container"></div><span class="source">item</span>';
1733
- const source = queryAll('.source');
1734
- query('#container').append(source);
1735
- expect(document.querySelector('#container span').textContent).toBe('item');
1736
- });
1737
- });
1738
-
1739
- describe('ZQueryCollection - after/before with Node', () => {
1740
- it('inserts Node after element', () => {
1741
- document.body.innerHTML = '<div id="anchor"></div>';
1742
- const newNode = document.createElement('span');
1743
- newNode.id = 'after';
1744
- query('#anchor').after(newNode);
1745
- expect(document.querySelector('#anchor').nextElementSibling.id).toBe('after');
1746
- });
1747
-
1748
- it('inserts Node before element', () => {
1749
- document.body.innerHTML = '<div id="anchor"></div>';
1750
- const newNode = document.createElement('span');
1751
- newNode.id = 'before';
1752
- query('#anchor').before(newNode);
1753
- expect(document.querySelector('#anchor').previousElementSibling.id).toBe('before');
1754
- });
1755
- });
1756
-
1757
-
1758
- // ===========================================================================
1759
- // replaceWith using Node
1760
- // ===========================================================================
1761
-
1762
- describe('ZQueryCollection - replaceWith(Node)', () => {
1763
- it('replaces element with a Node', () => {
1764
- document.body.innerHTML = '<div id="old">old</div>';
1765
- const newNode = document.createElement('span');
1766
- newNode.id = 'new';
1767
- newNode.textContent = 'replaced';
1768
- query('#old').replaceWith(newNode);
1769
- expect(document.querySelector('#old')).toBeNull();
1770
- expect(document.querySelector('#new').textContent).toBe('replaced');
1771
- });
1772
- });
1773
-
1774
-
1775
- // ===========================================================================
1776
- // nextUntil/prevUntil/parentsUntil with filter
1777
- // ===========================================================================
1778
-
1779
- describe('ZQueryCollection - nextUntil with filter', () => {
1780
- it('collects siblings until stop selector, applying filter', () => {
1781
- document.body.innerHTML = '<div id="start"></div><span class="a">A</span><p>P</p><span class="a">A2</span><div id="stop"></div>';
1782
- const result = query('#start').nextUntil('#stop', 'span');
1783
- expect(result.length).toBe(2); // only <span> siblings
1784
- });
1785
- });
1786
-
1787
- describe('ZQueryCollection - prevUntil with filter', () => {
1788
- it('collects previous siblings until stop selector, applying filter', () => {
1789
- document.body.innerHTML = '<div id="stop"></div><span>A</span><p>P</p><span>B</span><div id="end"></div>';
1790
- const result = query('#end').prevUntil('#stop', 'span');
1791
- expect(result.length).toBe(2);
1792
- });
1793
- });
1794
-
1795
- describe('ZQueryCollection - parentsUntil with filter', () => {
1796
- it('collects parent elements until stop selector, applying filter', () => {
1797
- document.body.innerHTML = '<section><article><div><span id="target"></span></div></article></section>';
1798
- const result = query('#target').parentsUntil('section', 'div');
1799
- expect(result.length).toBe(1);
1800
- expect(result.first().tagName).toBe('DIV');
1801
- });
1802
- });
1803
-
1804
-
1805
- // ===========================================================================
1806
- // delegated on() at document level
1807
- // ===========================================================================
1808
-
1809
- describe('ZQueryCollection - delegated on()', () => {
1810
- it('delegates event to matching child selector', () => {
1811
- document.body.innerHTML = '<div id="container"><button class="action">click</button></div>';
1812
- const handler = vi.fn();
1813
- query('#container').on('click', '.action', handler);
1814
- document.querySelector('.action').click();
1815
- expect(handler).toHaveBeenCalledTimes(1);
1816
- });
1817
-
1818
- it('does not fire for non-matching elements', () => {
1819
- document.body.innerHTML = '<div id="container"><span class="other">x</span></div>';
1820
- const handler = vi.fn();
1821
- query('#container').on('click', '.action', handler);
1822
- document.querySelector('.other').click();
1823
- expect(handler).not.toHaveBeenCalled();
1824
- });
1825
- });
1826
-
1827
-
1828
- // ===========================================================================
1829
- // Multi-event on/off
1830
- // ===========================================================================
1831
-
1832
- describe('ZQueryCollection - multi-event on()', () => {
1833
- it('binds handler to multiple space-separated events', () => {
1834
- document.body.innerHTML = '<input id="inp" type="text">';
1835
- const handler = vi.fn();
1836
- query('#inp').on('focus blur', handler);
1837
- document.querySelector('#inp').dispatchEvent(new Event('focus'));
1838
- document.querySelector('#inp').dispatchEvent(new Event('blur'));
1839
- expect(handler).toHaveBeenCalledTimes(2);
1840
- });
1841
- });
1842
-
1843
-
1844
- // ===========================================================================
1845
- // scrollTop/scrollLeft getters
1846
- // ===========================================================================
1847
-
1848
- describe('ZQueryCollection - scrollTop/scrollLeft', () => {
1849
- it('gets and sets scrollTop', () => {
1850
- document.body.innerHTML = '<div id="scr" style="overflow:auto; height: 50px;"><div style="height:200px;">x</div></div>';
1851
- const el = document.querySelector('#scr');
1852
- query('#scr').scrollTop(100);
1853
- expect(el.scrollTop).toBe(100);
1854
- });
1855
-
1856
- it('gets scrollTop value', () => {
1857
- document.body.innerHTML = '<div id="scr" style="overflow:auto; height: 50px;"><div style="height:200px;">x</div></div>';
1858
- document.querySelector('#scr').scrollTop = 50;
1859
- expect(query('#scr').scrollTop()).toBe(50);
1860
- });
1861
- });
1862
-
1863
-
1864
- // ===========================================================================
1865
- // slideDown/slideUp set styles
1866
- // ===========================================================================
1867
-
1868
- describe('ZQueryCollection - slideDown/slideUp', () => {
1869
- it('slideDown sets overflow hidden and maxHeight initially', () => {
1870
- vi.useFakeTimers();
1871
- document.body.innerHTML = '<div id="slide" style="display:none;">content</div>';
1872
- query('#slide').slideDown(100);
1873
- const el = document.querySelector('#slide');
1874
- expect(el.style.overflow).toBe('hidden');
1875
- // maxHeight could be '0' or '0px' depending on jsdom normalization
1876
- expect(el.style.maxHeight).toMatch(/^0(px)?$/);
1877
- vi.advanceTimersByTime(100);
1878
- vi.useRealTimers();
1879
- });
1880
-
1881
- it('slideUp hides element after duration', () => {
1882
- vi.useFakeTimers();
1883
- document.body.innerHTML = '<div id="slide">content</div>';
1884
- query('#slide').slideUp(100);
1885
- vi.advanceTimersByTime(100);
1886
- expect(document.querySelector('#slide').style.display).toBe('none');
1887
- vi.useRealTimers();
1888
- });
1889
- });
1890
-
1891
-
1892
- // ===========================================================================
1893
- // fadeIn/fadeOut set opacity
1894
- // ===========================================================================
1895
-
1896
- describe('ZQueryCollection - fadeIn/fadeOut', () => {
1897
- it('fadeIn sets initial opacity to 0', () => {
1898
- document.body.innerHTML = '<div id="fade" style="display:none;">content</div>';
1899
- query('#fade').fadeIn(100);
1900
- const el = document.querySelector('#fade');
1901
- expect(el.style.opacity).toBe('0');
1902
- });
1903
-
1904
- it('fadeTo animates to specified opacity', () => {
1905
- document.body.innerHTML = '<div id="fade">content</div>';
1906
- query('#fade').fadeTo(100, 0.5);
1907
- // Animation starts - just verify no throw
1908
- expect(document.querySelector('#fade')).not.toBeNull();
1909
- });
1910
- });
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { query, queryAll, ZQueryCollection } from '../src/core.js';
3
+
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Setup
7
+ // ---------------------------------------------------------------------------
8
+ beforeEach(() => {
9
+ document.body.innerHTML = `
10
+ <div id="main">
11
+ <p class="text first-p">Hello</p>
12
+ <p class="text second-p">World</p>
13
+ <span class="other">Span</span>
14
+ <p class="text third-p">Extra</p>
15
+ </div>
16
+ <div id="sidebar">
17
+ <ul id="nav">
18
+ <li class="nav-item active">Home</li>
19
+ <li class="nav-item">About</li>
20
+ <li class="nav-item">Contact</li>
21
+ </ul>
22
+ </div>
23
+ `;
24
+ });
25
+
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // query() - single selector
29
+ // ---------------------------------------------------------------------------
30
+
31
+ describe('query()', () => {
32
+ it('returns ZQueryCollection by CSS selector', () => {
33
+ const col = query('#main');
34
+ expect(col).toBeInstanceOf(ZQueryCollection);
35
+ expect(col.first()).toBe(document.getElementById('main'));
36
+ });
37
+
38
+ it('returns empty collection for non-matching selector', () => {
39
+ const col = query('#nonexistent');
40
+ expect(col).toBeInstanceOf(ZQueryCollection);
41
+ expect(col.length).toBe(0);
42
+ });
43
+
44
+ it('returns empty collection for null/undefined', () => {
45
+ expect(query(null).length).toBe(0);
46
+ expect(query(undefined).length).toBe(0);
47
+ });
48
+
49
+ it('wraps DOM element in collection', () => {
50
+ const div = document.createElement('div');
51
+ const col = query(div);
52
+ expect(col).toBeInstanceOf(ZQueryCollection);
53
+ expect(col.first()).toBe(div);
54
+ });
55
+
56
+ it('creates elements from HTML string as collection', () => {
57
+ const col = query('<span>hello</span>');
58
+ expect(col).toBeInstanceOf(ZQueryCollection);
59
+ expect(col.first().tagName).toBe('SPAN');
60
+ expect(col.first().textContent).toBe('hello');
61
+ });
62
+
63
+ it('returns all matching elements', () => {
64
+ const col = query('.text');
65
+ expect(col.length).toBe(3);
66
+ });
67
+
68
+ it('uses context parameter', () => {
69
+ const col = query('.text', '#main');
70
+ expect(col.length).toBe(3);
71
+ expect(col.first().textContent).toBe('Hello');
72
+ });
73
+
74
+ it('supports chaining on result', () => {
75
+ const col = query('#main').addClass('chained');
76
+ expect(col.first().classList.contains('chained')).toBe(true);
77
+ });
78
+ });
79
+
80
+
81
+ // ---------------------------------------------------------------------------
82
+ // queryAll() - collection selector
83
+ // ---------------------------------------------------------------------------
84
+
85
+ describe('queryAll()', () => {
86
+ it('returns ZQueryCollection for CSS selector', () => {
87
+ const col = queryAll('.text');
88
+ expect(col).toBeInstanceOf(ZQueryCollection);
89
+ expect(col.length).toBe(3);
90
+ });
91
+
92
+ it('returns empty collection for non-matching', () => {
93
+ const col = queryAll('.nonexistent');
94
+ expect(col.length).toBe(0);
95
+ });
96
+
97
+ it('wraps single element', () => {
98
+ const div = document.createElement('div');
99
+ const col = queryAll(div);
100
+ expect(col.length).toBe(1);
101
+ expect(col.first()).toBe(div);
102
+ });
103
+
104
+ it('creates elements from HTML', () => {
105
+ const col = queryAll('<li>a</li><li>b</li>');
106
+ expect(col.length).toBe(2);
107
+ });
108
+
109
+ it('returns empty for null', () => {
110
+ expect(queryAll(null).length).toBe(0);
111
+ });
112
+ });
113
+
114
+
115
+ // ---------------------------------------------------------------------------
116
+ // ZQueryCollection methods
117
+ // ---------------------------------------------------------------------------
118
+
119
+ describe('ZQueryCollection', () => {
120
+ describe('iteration', () => {
121
+ it('each() iterates elements', () => {
122
+ const col = queryAll('.text');
123
+ const tags = [];
124
+ col.each((_, el) => tags.push(el.textContent));
125
+ expect(tags).toEqual(['Hello', 'World', 'Extra']);
126
+ });
127
+
128
+ it('map() maps elements', () => {
129
+ const texts = queryAll('.text').map((_, el) => el.textContent);
130
+ expect(texts).toEqual(['Hello', 'World', 'Extra']);
131
+ });
132
+
133
+ it('first() and last()', () => {
134
+ const col = queryAll('.text');
135
+ expect(col.first().textContent).toBe('Hello');
136
+ expect(col.last().textContent).toBe('Extra');
137
+ });
138
+
139
+ it('eq() returns sub-collection', () => {
140
+ const col = queryAll('.text');
141
+ expect(col.eq(1).first().textContent).toBe('World');
142
+ expect(col.eq(5).length).toBe(0);
143
+ });
144
+
145
+ it('toArray() returns plain array', () => {
146
+ const arr = queryAll('.text').toArray();
147
+ expect(Array.isArray(arr)).toBe(true);
148
+ expect(arr.length).toBe(3);
149
+ });
150
+
151
+ it('is iterable', () => {
152
+ const col = queryAll('.text');
153
+ const items = [...col];
154
+ expect(items.length).toBe(3);
155
+ });
156
+ });
157
+
158
+
159
+ describe('traversal', () => {
160
+ it('find() searches descendants', () => {
161
+ const main = queryAll('#main');
162
+ const ps = main.find('.text');
163
+ expect(ps.length).toBe(3);
164
+ });
165
+
166
+ it('parent() returns parents', () => {
167
+ const p = queryAll('.text').parent();
168
+ expect(p.first().id).toBe('main');
169
+ });
170
+
171
+ it('children() returns direct children', () => {
172
+ const main = queryAll('#main');
173
+ expect(main.children().length).toBe(4); // 3 p + 1 span
174
+ });
175
+
176
+ it('filter() with string selector', () => {
177
+ const col = queryAll('#main').children();
178
+ const ps = col.filter('p');
179
+ expect(ps.length).toBe(3);
180
+ });
181
+
182
+ it('filter() with function', () => {
183
+ const col = queryAll('#main').children();
184
+ const ps = col.filter(el => el.tagName === 'P');
185
+ expect(ps.length).toBe(3);
186
+ });
187
+
188
+ it('not() excludes elements', () => {
189
+ const col = queryAll('#main').children();
190
+ const nonP = col.not('p');
191
+ expect(nonP.length).toBe(1);
192
+ expect(nonP.first().tagName).toBe('SPAN');
193
+ });
194
+
195
+ it('next() returns next sibling', () => {
196
+ const col = queryAll('.first-p');
197
+ expect(col.next().first().classList.contains('second-p')).toBe(true);
198
+ });
199
+
200
+ it('next(selector) filters by selector', () => {
201
+ const col = queryAll('.first-p');
202
+ expect(col.next('.second-p').length).toBe(1);
203
+ expect(col.next('.third-p').length).toBe(0);
204
+ });
205
+
206
+ it('prev() returns previous sibling', () => {
207
+ const col = queryAll('.second-p');
208
+ expect(col.prev().first().classList.contains('first-p')).toBe(true);
209
+ });
210
+
211
+ it('prev(selector) filters by selector', () => {
212
+ const col = queryAll('.second-p');
213
+ expect(col.prev('.first-p').length).toBe(1);
214
+ expect(col.prev('.other').length).toBe(0);
215
+ });
216
+
217
+ it('nextAll() returns all following siblings', () => {
218
+ const col = queryAll('.first-p');
219
+ expect(col.nextAll().length).toBe(3); // second-p, other, third-p
220
+ });
221
+
222
+ it('nextAll(selector) filters', () => {
223
+ const col = queryAll('.first-p');
224
+ expect(col.nextAll('.text').length).toBe(2);
225
+ });
226
+
227
+ it('nextUntil() stops at selector', () => {
228
+ const col = queryAll('.first-p');
229
+ const result = col.nextUntil('.third-p');
230
+ expect(result.length).toBe(2); // second-p, other
231
+ });
232
+
233
+ it('prevAll() returns all preceding siblings', () => {
234
+ const col = queryAll('.third-p');
235
+ expect(col.prevAll().length).toBe(3);
236
+ });
237
+
238
+ it('prevAll(selector) filters', () => {
239
+ const col = queryAll('.third-p');
240
+ expect(col.prevAll('.text').length).toBe(2);
241
+ });
242
+
243
+ it('prevUntil() stops at selector', () => {
244
+ const col = queryAll('.third-p');
245
+ const result = col.prevUntil('.first-p');
246
+ expect(result.length).toBe(2); // other, second-p
247
+ });
248
+
249
+ it('parents() returns all ancestors', () => {
250
+ const col = queryAll('.first-p');
251
+ const parents = col.parents();
252
+ expect(parents.length).toBeGreaterThanOrEqual(2); // #main, body, html
253
+ });
254
+
255
+ it('parents(selector) filters', () => {
256
+ const col = queryAll('.first-p');
257
+ expect(col.parents('#main').length).toBe(1);
258
+ });
259
+
260
+ it('parentsUntil() stops at selector', () => {
261
+ const col = queryAll('.first-p');
262
+ const result = col.parentsUntil('body');
263
+ expect(result.length).toBe(1); // just #main
264
+ });
265
+
266
+ it('contents() includes text nodes', () => {
267
+ document.body.innerHTML = '<div id="ct">text<span>child</span></div>';
268
+ const col = queryAll('#ct');
269
+ expect(col.contents().length).toBe(2); // text node + span
270
+ });
271
+
272
+ it('siblings() returns all siblings excluding self', () => {
273
+ const col = queryAll('.other');
274
+ const sibs = col.siblings();
275
+ expect(sibs.length).toBe(3); // three .text p's
276
+ });
277
+
278
+ it('closest() finds ancestor', () => {
279
+ const col = queryAll('.nav-item').eq(0);
280
+ expect(col.closest('#nav').length).toBe(1);
281
+ });
282
+ });
283
+
284
+
285
+ describe('classes', () => {
286
+ it('addClass / hasClass / removeClass', () => {
287
+ const col = queryAll('#main');
288
+ col.addClass('active');
289
+ expect(col.hasClass('active')).toBe(true);
290
+ col.removeClass('active');
291
+ expect(col.hasClass('active')).toBe(false);
292
+ });
293
+
294
+ it('toggleClass', () => {
295
+ const col = queryAll('#main');
296
+ col.toggleClass('toggled');
297
+ expect(col.hasClass('toggled')).toBe(true);
298
+ col.toggleClass('toggled');
299
+ expect(col.hasClass('toggled')).toBe(false);
300
+ });
301
+
302
+ it('toggleClass with multiple classes', () => {
303
+ const col = queryAll('#main');
304
+ col.toggleClass('a', 'b');
305
+ expect(col[0].classList.contains('a')).toBe(true);
306
+ expect(col[0].classList.contains('b')).toBe(true);
307
+ col.toggleClass('a', 'b');
308
+ expect(col[0].classList.contains('a')).toBe(false);
309
+ expect(col[0].classList.contains('b')).toBe(false);
310
+ });
311
+
312
+ it('toggleClass with force boolean', () => {
313
+ const col = queryAll('#main');
314
+ col.toggleClass('forced', true);
315
+ expect(col.hasClass('forced')).toBe(true);
316
+ col.toggleClass('forced', true);
317
+ expect(col.hasClass('forced')).toBe(true);
318
+ col.toggleClass('forced', false);
319
+ expect(col.hasClass('forced')).toBe(false);
320
+ });
321
+ });
322
+
323
+
324
+ describe('attributes', () => {
325
+ it('attr get/set', () => {
326
+ const col = queryAll('#main');
327
+ col.attr('data-test', 'value');
328
+ expect(col.attr('data-test')).toBe('value');
329
+ });
330
+
331
+ it('removeAttr', () => {
332
+ const col = queryAll('#main');
333
+ col.attr('data-x', 'y');
334
+ col.removeAttr('data-x');
335
+ expect(col.attr('data-x')).toBeNull();
336
+ });
337
+
338
+ it('data get/set', () => {
339
+ const col = queryAll('#main');
340
+ col.data('count', 42);
341
+ expect(col.data('count')).toBe(42);
342
+ });
343
+
344
+ it('data handles objects via JSON', () => {
345
+ const col = queryAll('#main');
346
+ col.data('info', { a: 1 });
347
+ expect(col.data('info')).toEqual({ a: 1 });
348
+ });
349
+ });
350
+
351
+
352
+ describe('content', () => {
353
+ it('html get/set', () => {
354
+ const col = queryAll('#main');
355
+ col.html('<b>bold</b>');
356
+ expect(col.html()).toBe('<b>bold</b>');
357
+ });
358
+
359
+ it('html() auto-morphs when element has existing children', () => {
360
+ const main = document.querySelector('#main');
361
+ main.innerHTML = '<p id="preserved">old text</p>';
362
+ const ref = main.children[0]; // grab DOM reference
363
+ const col = queryAll('#main');
364
+ col.html('<p id="preserved">new text</p>');
365
+ // Same DOM node preserved - morph, not innerHTML replace
366
+ expect(main.children[0]).toBe(ref);
367
+ expect(main.children[0].textContent).toBe('new text');
368
+ });
369
+
370
+ it('html() uses innerHTML for empty elements (fast first-paint)', () => {
371
+ const main = document.querySelector('#main');
372
+ main.innerHTML = ''; // make it empty
373
+ const col = queryAll('#main');
374
+ col.html('<p id="fresh">hello</p>');
375
+ expect(main.innerHTML).toBe('<p id="fresh">hello</p>');
376
+ });
377
+
378
+ it('empty().html() forces raw innerHTML (opt-out of morph)', () => {
379
+ const main = document.querySelector('#main');
380
+ main.innerHTML = '<p id="old">will be destroyed</p>';
381
+ const ref = main.children[0];
382
+ const col = queryAll('#main');
383
+ col.empty().html('<p id="old">replaced</p>');
384
+ // NOT the same node - empty() cleared children, so html() used innerHTML
385
+ expect(main.children[0]).not.toBe(ref);
386
+ expect(main.children[0].textContent).toBe('replaced');
387
+ });
388
+
389
+ it('text get/set', () => {
390
+ const col = queryAll('.text').eq(0);
391
+ col.text('Changed');
392
+ expect(col.text()).toBe('Changed');
393
+ });
394
+
395
+ it('morph() diffs content instead of replacing', () => {
396
+ const main = document.querySelector('#main');
397
+ main.innerHTML = '<p id="keep">old</p>';
398
+ const ref = main.children[0];
399
+ const col = queryAll('#main');
400
+ col.morph('<p id="keep">new</p>');
401
+ // Same DOM node preserved (morph, not innerHTML)
402
+ expect(main.children[0]).toBe(ref);
403
+ expect(main.children[0].textContent).toBe('new');
404
+ });
405
+
406
+ it('morph() is chainable', () => {
407
+ const col = queryAll('#main');
408
+ const ret = col.morph('<p>m</p>');
409
+ expect(ret).toBe(col);
410
+ });
411
+ });
412
+
413
+
414
+ describe('DOM manipulation', () => {
415
+ it('append() adds content', () => {
416
+ const col = queryAll('#main');
417
+ col.append('<div class="appended">new</div>');
418
+ expect(document.querySelector('.appended').textContent).toBe('new');
419
+ });
420
+
421
+ it('prepend() adds at start', () => {
422
+ const col = queryAll('#main');
423
+ col.prepend('<div class="first">start</div>');
424
+ expect(col.first().firstElementChild.className).toBe('first');
425
+ });
426
+
427
+ it('remove() removes elements', () => {
428
+ queryAll('.other').remove();
429
+ expect(document.querySelector('.other')).toBeNull();
430
+ });
431
+
432
+ it('empty() clears content', () => {
433
+ queryAll('#main').empty();
434
+ expect(document.querySelector('#main').children.length).toBe(0);
435
+ });
436
+
437
+ it('clone() creates deep copy', () => {
438
+ const col = queryAll('.text').eq(0);
439
+ const clone = col.clone();
440
+ expect(clone.first().textContent).toBe('Hello');
441
+ expect(clone.first()).not.toBe(col.first());
442
+ });
443
+ });
444
+
445
+
446
+ describe('visibility', () => {
447
+ it('hide() sets display none', () => {
448
+ queryAll('#main').hide();
449
+ expect(document.getElementById('main').style.display).toBe('none');
450
+ });
451
+
452
+ it('show() clears display', () => {
453
+ const main = document.getElementById('main');
454
+ main.style.display = 'none';
455
+ queryAll('#main').show();
456
+ expect(main.style.display).toBe('');
457
+ });
458
+ });
459
+
460
+
461
+ describe('events', () => {
462
+ it('on() and trigger()', () => {
463
+ let clicked = false;
464
+ const col = queryAll('#main');
465
+ col.on('click', () => { clicked = true; });
466
+ col.trigger('click');
467
+ expect(clicked).toBe(true);
468
+ });
469
+
470
+ it('off() removes handler', () => {
471
+ let count = 0;
472
+ const handler = () => { count++; };
473
+ const col = queryAll('#main');
474
+ col.on('click', handler);
475
+ col.trigger('click');
476
+ col.off('click', handler);
477
+ col.trigger('click');
478
+ expect(count).toBe(1);
479
+ });
480
+
481
+ it('on() works on non-Element targets like window', () => {
482
+ let fired = false;
483
+ const col = new ZQueryCollection([window]);
484
+ col.on('custom-win-evt', () => { fired = true; });
485
+ window.dispatchEvent(new Event('custom-win-evt'));
486
+ expect(fired).toBe(true);
487
+ });
488
+
489
+ it('$.on() with EventTarget binds directly to that target', () => {
490
+ let fired = false;
491
+ const target = new EventTarget();
492
+ query.on('test-evt', target, () => { fired = true; });
493
+ target.dispatchEvent(new Event('test-evt'));
494
+ expect(fired).toBe(true);
495
+ });
496
+ });
497
+ });
498
+
499
+
500
+ // ---------------------------------------------------------------------------
501
+ // Quick ref helpers
502
+ // ---------------------------------------------------------------------------
503
+
504
+ describe('query quick refs', () => {
505
+ it('$.id() returns element by id', () => {
506
+ expect(query.id('main')).toBe(document.getElementById('main'));
507
+ });
508
+
509
+ it('$.class() returns first element by class', () => {
510
+ expect(query.class('text').textContent).toBe('Hello');
511
+ });
512
+
513
+ it('$.classes() returns ZQueryCollection', () => {
514
+ const col = query.classes('text');
515
+ expect(col).toBeInstanceOf(ZQueryCollection);
516
+ expect(col.length).toBe(3);
517
+ });
518
+
519
+ it('$.children() returns ZQueryCollection', () => {
520
+ const col = query.children('main');
521
+ expect(col).toBeInstanceOf(ZQueryCollection);
522
+ expect(col.length).toBe(4);
523
+ });
524
+
525
+ it('$.tag() returns ZQueryCollection', () => {
526
+ const col = query.tag('p');
527
+ expect(col).toBeInstanceOf(ZQueryCollection);
528
+ expect(col.length).toBe(3);
529
+ });
530
+
531
+ describe('collection forEach() works like Array.forEach()', () => {
532
+ it('iterates with el, index, array args', () => {
533
+ const results = [];
534
+ query.classes('text').forEach((el, i) => results.push({ i, text: el.textContent }));
535
+ expect(results).toEqual([{ i: 0, text: 'Hello' }, { i: 1, text: 'World' }, { i: 2, text: 'Extra' }]);
536
+ });
537
+ });
538
+
539
+ it('$.create() creates element with attributes', () => {
540
+ const col = query.create('div', { class: 'new', id: 'created' }, 'text');
541
+ expect(col).toBeInstanceOf(ZQueryCollection);
542
+ expect(col.length).toBe(1);
543
+ const el = col[0];
544
+ expect(el.tagName).toBe('DIV');
545
+ expect(el.className).toBe('new');
546
+ expect(el.id).toBe('created');
547
+ expect(el.textContent).toBe('text');
548
+ });
549
+
550
+ // qs / qsa - raw DOM shortcuts
551
+ it('$.qs() returns raw element by CSS selector', () => {
552
+ const el = query.qs('#main');
553
+ expect(el).toBe(document.getElementById('main'));
554
+ expect(el).toBeInstanceOf(HTMLElement);
555
+ });
556
+
557
+ it('$.qs() returns null for non-matching selector', () => {
558
+ expect(query.qs('#nonexistent')).toBeNull();
559
+ });
560
+
561
+ it('$.qs() scopes to context element', () => {
562
+ const sidebar = document.getElementById('sidebar');
563
+ const el = query.qs('.nav-item', sidebar);
564
+ expect(el.textContent).toBe('Home');
565
+ });
566
+
567
+ it('$.qsa() returns array of raw elements', () => {
568
+ const els = query.qsa('.text');
569
+ expect(Array.isArray(els)).toBe(true);
570
+ expect(els.length).toBe(3);
571
+ expect(els[0]).toBeInstanceOf(HTMLElement);
572
+ });
573
+
574
+ it('$.qsa() returns empty array for non-matching selector', () => {
575
+ const els = query.qsa('.nonexistent');
576
+ expect(els).toEqual([]);
577
+ });
578
+
579
+ it('$.qsa() scopes to context element', () => {
580
+ const nav = document.getElementById('nav');
581
+ const els = query.qsa('.nav-item', nav);
582
+ expect(els.length).toBe(3);
583
+ expect(els[0].textContent).toBe('Home');
584
+ });
585
+
586
+ it('$.qsa() result supports Array methods', () => {
587
+ const names = query.qsa('.nav-item').map(el => el.textContent);
588
+ expect(names).toEqual(['Home', 'About', 'Contact']);
589
+ });
590
+ });
591
+
592
+
593
+ // ---------------------------------------------------------------------------
594
+ // New filtering / collection methods
595
+ // ---------------------------------------------------------------------------
596
+
597
+ describe('filtering & collection', () => {
598
+ it('is() checks if any element matches selector', () => {
599
+ expect(queryAll('#main').children().is('p')).toBe(true);
600
+ expect(queryAll('#main').children().is('table')).toBe(false);
601
+ });
602
+
603
+ it('is() with function', () => {
604
+ const result = queryAll('.text').is(function(i) { return this.textContent === 'World'; });
605
+ expect(result).toBe(true);
606
+ });
607
+
608
+ it('has() keeps elements containing matching descendant', () => {
609
+ const col = queryAll('#sidebar').has('.nav-item');
610
+ expect(col.length).toBe(1);
611
+ });
612
+
613
+ it('slice() returns subset', () => {
614
+ const col = queryAll('.text').slice(0, 2);
615
+ expect(col.length).toBe(2);
616
+ expect(col.first().textContent).toBe('Hello');
617
+ });
618
+
619
+ it('slice() with negative index', () => {
620
+ const col = queryAll('.text').slice(-1);
621
+ expect(col.length).toBe(1);
622
+ expect(col.first().textContent).toBe('Extra');
623
+ });
624
+
625
+ it('add() merges collections', () => {
626
+ const col = queryAll('.first-p').add('.other');
627
+ expect(col.length).toBe(2);
628
+ });
629
+
630
+ it('add() with element', () => {
631
+ const el = document.getElementById('main');
632
+ const col = queryAll('.first-p').add(el);
633
+ expect(col.length).toBe(2);
634
+ });
635
+
636
+ it('add() with ZQueryCollection', () => {
637
+ const col = queryAll('.first-p').add(queryAll('.other'));
638
+ expect(col.length).toBe(2);
639
+ });
640
+
641
+ it('get() with no args returns array', () => {
642
+ const arr = queryAll('.text').get();
643
+ expect(Array.isArray(arr)).toBe(true);
644
+ expect(arr.length).toBe(3);
645
+ });
646
+
647
+ it('get(index) returns element', () => {
648
+ const el = queryAll('.text').get(1);
649
+ expect(el.textContent).toBe('World');
650
+ });
651
+
652
+ it('get(negative) returns from end', () => {
653
+ const el = queryAll('.text').get(-1);
654
+ expect(el.textContent).toBe('Extra');
655
+ });
656
+
657
+ it('index() returns position among siblings', () => {
658
+ const col = queryAll('.other');
659
+ expect(col.index()).toBe(2); // 3rd child (0-indexed)
660
+ });
661
+
662
+ it('index(element) returns position in collection', () => {
663
+ const other = document.querySelector('.other');
664
+ const col = queryAll('#main').children();
665
+ expect(col.index(other)).toBe(2);
666
+ });
667
+ });
668
+
669
+
670
+ // ---------------------------------------------------------------------------
671
+ // Inverse DOM manipulation (appendTo, prependTo, insertAfter, insertBefore, etc.)
672
+ // ---------------------------------------------------------------------------
673
+
674
+ describe('inverse DOM manipulation', () => {
675
+ it('appendTo() moves elements into target', () => {
676
+ queryAll('<div class="new-item">new</div>').appendTo('#nav');
677
+ const nav = document.getElementById('nav');
678
+ expect(nav.lastElementChild.className).toBe('new-item');
679
+ });
680
+
681
+ it('prependTo() inserts at start of target', () => {
682
+ queryAll('<li class="first-item">first</li>').prependTo('#nav');
683
+ const nav = document.getElementById('nav');
684
+ expect(nav.firstElementChild.className).toBe('first-item');
685
+ });
686
+
687
+ it('insertAfter() inserts after target', () => {
688
+ queryAll('<span class="inserted">!</span>').insertAfter('.first-p');
689
+ const firstP = document.querySelector('.first-p');
690
+ expect(firstP.nextElementSibling.className).toBe('inserted');
691
+ });
692
+
693
+ it('insertBefore() inserts before target', () => {
694
+ queryAll('<span class="inserted">!</span>').insertBefore('.second-p');
695
+ const secondP = document.querySelector('.second-p');
696
+ expect(secondP.previousElementSibling.className).toBe('inserted');
697
+ });
698
+
699
+ it('replaceAll() replaces target elements', () => {
700
+ queryAll('<em>replaced</em>').replaceAll('.other');
701
+ expect(document.querySelector('.other')).toBeNull();
702
+ expect(document.querySelector('em').textContent).toBe('replaced');
703
+ });
704
+
705
+ it('unwrap() removes parent wrapper', () => {
706
+ // wrap nav-items in a div first
707
+ const items = queryAll('.nav-item');
708
+ const count = items.length;
709
+ items.eq(0).wrap('<div class="wrapper"></div>');
710
+ expect(document.querySelector('.wrapper')).not.toBeNull();
711
+ queryAll('.nav-item').eq(0).unwrap('.wrapper');
712
+ expect(document.querySelector('.wrapper')).toBeNull();
713
+ });
714
+
715
+ it('wrapAll() wraps all elements in one wrapper', () => {
716
+ queryAll('.nav-item').wrapAll('<div class="all-wrap"></div>');
717
+ const wrap = document.querySelector('.all-wrap');
718
+ expect(wrap).not.toBeNull();
719
+ expect(wrap.children.length).toBe(3);
720
+ });
721
+
722
+ it('wrapInner() wraps inner contents', () => {
723
+ queryAll('.first-p').wrapInner('<strong></strong>');
724
+ const strong = document.querySelector('.first-p > strong');
725
+ expect(strong).not.toBeNull();
726
+ expect(strong.textContent).toBe('Hello');
727
+ });
728
+
729
+ it('detach() removes elements (alias for remove)', () => {
730
+ queryAll('.other').detach();
731
+ expect(document.querySelector('.other')).toBeNull();
732
+ });
733
+ });
734
+
735
+
736
+ // ---------------------------------------------------------------------------
737
+ // CSS dimension methods
738
+ // ---------------------------------------------------------------------------
739
+
740
+ describe('CSS dimension methods', () => {
741
+ it('scrollTop() get returns a number', () => {
742
+ const val = queryAll('#main').scrollTop();
743
+ expect(typeof val).toBe('number');
744
+ });
745
+
746
+ it('scrollTop(value) sets scroll position', () => {
747
+ const main = document.getElementById('main');
748
+ main.style.overflow = 'auto';
749
+ main.style.height = '10px';
750
+ main.innerHTML = '<div style="height:1000px">tall</div>';
751
+ queryAll('#main').scrollTop(50);
752
+ expect(main.scrollTop).toBe(50);
753
+ });
754
+
755
+ it('scrollLeft() get returns a number', () => {
756
+ const val = queryAll('#main').scrollLeft();
757
+ expect(typeof val).toBe('number');
758
+ });
759
+
760
+ it('innerWidth() returns clientWidth', () => {
761
+ const main = document.getElementById('main');
762
+ // jsdom sets clientWidth to 0 but the method should still return a number
763
+ const val = queryAll('#main').innerWidth();
764
+ expect(typeof val).toBe('number');
765
+ });
766
+
767
+ it('innerHeight() returns clientHeight', () => {
768
+ const val = queryAll('#main').innerHeight();
769
+ expect(typeof val).toBe('number');
770
+ });
771
+
772
+ it('outerWidth() returns offsetWidth', () => {
773
+ const val = queryAll('#main').outerWidth();
774
+ expect(typeof val).toBe('number');
775
+ });
776
+
777
+ it('outerHeight() returns offsetHeight', () => {
778
+ const val = queryAll('#main').outerHeight();
779
+ expect(typeof val).toBe('number');
780
+ });
781
+
782
+ it('outerWidth(true) includes margin', () => {
783
+ const main = document.getElementById('main');
784
+ main.style.margin = '10px';
785
+ const val = queryAll('#main').outerWidth(true);
786
+ expect(typeof val).toBe('number');
787
+ });
788
+ });
789
+
790
+
791
+ // ---------------------------------------------------------------------------
792
+ // prop() method
793
+ // ---------------------------------------------------------------------------
794
+
795
+ describe('ZQueryCollection - prop()', () => {
796
+ it('gets a DOM property', () => {
797
+ document.body.innerHTML = '<input type="checkbox" checked>';
798
+ const col = queryAll('input');
799
+ expect(col.prop('checked')).toBe(true);
800
+ });
801
+
802
+ it('sets a DOM property', () => {
803
+ document.body.innerHTML = '<input type="checkbox">';
804
+ const col = queryAll('input');
805
+ col.prop('checked', true);
806
+ expect(col[0].checked).toBe(true);
807
+ });
808
+
809
+ it('sets multiple properties via sequential calls', () => {
810
+ document.body.innerHTML = '<input type="text">';
811
+ const col = queryAll('input');
812
+ col.prop('disabled', true);
813
+ col.prop('value', 'hello');
814
+ expect(col[0].disabled).toBe(true);
815
+ expect(col[0].value).toBe('hello');
816
+ });
817
+ });
818
+
819
+
820
+ // ---------------------------------------------------------------------------
821
+ // css() method
822
+ // ---------------------------------------------------------------------------
823
+
824
+ describe('ZQueryCollection - css()', () => {
825
+ beforeEach(() => {
826
+ document.body.innerHTML = '<div id="styled">test</div>';
827
+ });
828
+
829
+ it('sets style properties from object', () => {
830
+ queryAll('#styled').css({ color: 'red', 'font-size': '16px' });
831
+ const el = document.getElementById('styled');
832
+ expect(el.style.color).toBe('red');
833
+ });
834
+
835
+ it('returns collection for chaining', () => {
836
+ const col = queryAll('#styled').css({ color: 'blue' });
837
+ expect(col).toBeInstanceOf(ZQueryCollection);
838
+ });
839
+ });
840
+
841
+
842
+ // ---------------------------------------------------------------------------
843
+ // val() method
844
+ // ---------------------------------------------------------------------------
845
+
846
+ describe('ZQueryCollection - val()', () => {
847
+ it('gets input value', () => {
848
+ document.body.innerHTML = '<input value="test">';
849
+ expect(queryAll('input').val()).toBe('test');
850
+ });
851
+
852
+ it('sets input value', () => {
853
+ document.body.innerHTML = '<input value="">';
854
+ queryAll('input').val('new value');
855
+ expect(document.querySelector('input').value).toBe('new value');
856
+ });
857
+
858
+ it('gets select value', () => {
859
+ document.body.innerHTML = '<select><option value="a" selected>A</option><option value="b">B</option></select>';
860
+ expect(queryAll('select').val()).toBe('a');
861
+ });
862
+
863
+ it('gets textarea value', () => {
864
+ document.body.innerHTML = '<textarea>hello</textarea>';
865
+ expect(queryAll('textarea').val()).toBe('hello');
866
+ });
867
+ });
868
+
869
+
870
+ // ---------------------------------------------------------------------------
871
+ // after(), before() methods
872
+ // ---------------------------------------------------------------------------
873
+
874
+ describe('ZQueryCollection - after() / before()', () => {
875
+ beforeEach(() => {
876
+ document.body.innerHTML = '<div id="container"><p id="target">target</p></div>';
877
+ });
878
+
879
+ it('after() inserts content after element', () => {
880
+ queryAll('#target').after('<span class="after">after</span>');
881
+ const next = document.getElementById('target').nextElementSibling;
882
+ expect(next.className).toBe('after');
883
+ });
884
+
885
+ it('before() inserts content before element', () => {
886
+ queryAll('#target').before('<span class="before">before</span>');
887
+ const prev = document.getElementById('target').previousElementSibling;
888
+ expect(prev.className).toBe('before');
889
+ });
890
+ });
891
+
892
+
893
+ // ---------------------------------------------------------------------------
894
+ // wrap() method
895
+ // ---------------------------------------------------------------------------
896
+
897
+ describe('ZQueryCollection - wrap()', () => {
898
+ it('wraps element in new parent', () => {
899
+ document.body.innerHTML = '<div id="container"><p id="target">text</p></div>';
900
+ queryAll('#target').wrap('<div class="wrapper"></div>');
901
+ const wrapper = document.querySelector('.wrapper');
902
+ expect(wrapper).not.toBeNull();
903
+ expect(wrapper.querySelector('#target')).not.toBeNull();
904
+ });
905
+ });
906
+
907
+
908
+ // ---------------------------------------------------------------------------
909
+ // replaceWith() method
910
+ // ---------------------------------------------------------------------------
911
+
912
+ describe('ZQueryCollection - replaceWith()', () => {
913
+ it('replaces element with new content', () => {
914
+ document.body.innerHTML = '<div id="container"><p id="old">old</p></div>';
915
+ queryAll('#old').replaceWith('<span id="new">new</span>');
916
+ expect(document.querySelector('#old')).toBeNull();
917
+ expect(document.querySelector('#new')).not.toBeNull();
918
+ });
919
+
920
+ it('auto-morphs when tag name matches (preserves identity)', () => {
921
+ document.body.innerHTML = '<div id="container"><p id="target" class="old">old text</p></div>';
922
+ const target = document.querySelector('#target');
923
+ queryAll('#target').replaceWith('<p id="target" class="new">new text</p>');
924
+ // Same DOM node - morphed, not replaced
925
+ expect(document.querySelector('#target')).toBe(target);
926
+ expect(target.className).toBe('new');
927
+ expect(target.textContent).toBe('new text');
928
+ });
929
+
930
+ it('replaces when tag name differs', () => {
931
+ document.body.innerHTML = '<div id="container"><p id="old">old</p></div>';
932
+ const oldRef = document.querySelector('#old');
933
+ queryAll('#old').replaceWith('<section id="replaced">new</section>');
934
+ expect(document.querySelector('#replaced')).not.toBe(oldRef);
935
+ expect(document.querySelector('#replaced').tagName).toBe('SECTION');
936
+ });
937
+ });
938
+
939
+
940
+ // ---------------------------------------------------------------------------
941
+ // offset() and position()
942
+ // ---------------------------------------------------------------------------
943
+
944
+ describe('ZQueryCollection - offset() / position()', () => {
945
+ it('offset() returns object with top and left', () => {
946
+ document.body.innerHTML = '<div id="box">box</div>';
947
+ const off = queryAll('#box').offset();
948
+ expect(off).toHaveProperty('top');
949
+ expect(off).toHaveProperty('left');
950
+ expect(typeof off.top).toBe('number');
951
+ });
952
+
953
+ it('position() returns object with top and left', () => {
954
+ document.body.innerHTML = '<div id="box">box</div>';
955
+ const pos = queryAll('#box').position();
956
+ expect(pos).toHaveProperty('top');
957
+ expect(pos).toHaveProperty('left');
958
+ });
959
+ });
960
+
961
+
962
+ // ---------------------------------------------------------------------------
963
+ // width() and height()
964
+ // ---------------------------------------------------------------------------
965
+
966
+ describe('ZQueryCollection - width() / height()', () => {
967
+ it('width() returns a number', () => {
968
+ document.body.innerHTML = '<div id="box" style="width:100px">box</div>';
969
+ const val = queryAll('#box').width();
970
+ expect(typeof val).toBe('number');
971
+ });
972
+
973
+ it('height() returns a number', () => {
974
+ document.body.innerHTML = '<div id="box" style="height:50px">box</div>';
975
+ const val = queryAll('#box').height();
976
+ expect(typeof val).toBe('number');
977
+ });
978
+ });
979
+
980
+
981
+ // ---------------------------------------------------------------------------
982
+ // animate()
983
+ // ---------------------------------------------------------------------------
984
+
985
+ describe('ZQueryCollection - animate()', () => {
986
+ it('returns a promise', () => {
987
+ document.body.innerHTML = '<div id="box">box</div>';
988
+ const result = queryAll('#box').animate({ opacity: 0 }, 100);
989
+ // animate() returns a Promise
990
+ expect(result).toBeInstanceOf(Promise);
991
+ });
992
+ });
993
+
994
+
995
+ // ---------------------------------------------------------------------------
996
+ // hover() convenience
997
+ // ---------------------------------------------------------------------------
998
+
999
+ describe('hover()', () => {
1000
+ it('binds mouseenter and mouseleave', () => {
1001
+ let entered = false, left = false;
1002
+ const col = queryAll('#main');
1003
+ col.hover(() => { entered = true; }, () => { left = true; });
1004
+ col.first().dispatchEvent(new Event('mouseenter'));
1005
+ col.first().dispatchEvent(new Event('mouseleave'));
1006
+ expect(entered).toBe(true);
1007
+ expect(left).toBe(true);
1008
+ });
1009
+
1010
+ it('uses same fn for both if only one provided', () => {
1011
+ let count = 0;
1012
+ const col = queryAll('#main');
1013
+ col.hover(() => { count++; });
1014
+ col.first().dispatchEvent(new Event('mouseenter'));
1015
+ col.first().dispatchEvent(new Event('mouseleave'));
1016
+ expect(count).toBe(2);
1017
+ });
1018
+ });
1019
+
1020
+
1021
+ // ---------------------------------------------------------------------------
1022
+ // ZQueryCollection - empty collection safety
1023
+ // ---------------------------------------------------------------------------
1024
+
1025
+ describe('ZQueryCollection - empty collection operations', () => {
1026
+ it('first() returns null on empty', () => {
1027
+ expect(queryAll('.nonexistent').first()).toBeNull();
1028
+ });
1029
+
1030
+ it('last() returns null on empty', () => {
1031
+ expect(queryAll('.nonexistent').last()).toBeNull();
1032
+ });
1033
+
1034
+ it('each() on empty collection does not call callback', () => {
1035
+ const fn = vi.fn();
1036
+ queryAll('.nonexistent').each(fn);
1037
+ expect(fn).not.toHaveBeenCalled();
1038
+ });
1039
+
1040
+ it('map() on empty returns empty array', () => {
1041
+ expect(queryAll('.nonexistent').map(el => el)).toEqual([]);
1042
+ });
1043
+
1044
+ it('html() get on empty returns undefined', () => {
1045
+ const result = queryAll('.nonexistent').html();
1046
+ expect(result).toBeUndefined();
1047
+ });
1048
+
1049
+ it('text() get on empty returns undefined', () => {
1050
+ const result = queryAll('.nonexistent').text();
1051
+ expect(result).toBeUndefined();
1052
+ });
1053
+
1054
+ it('val() get on empty returns undefined', () => {
1055
+ expect(queryAll('.nonexistent').val()).toBeUndefined();
1056
+ });
1057
+
1058
+ it('addClass on empty does not throw', () => {
1059
+ expect(() => queryAll('.nonexistent').addClass('test')).not.toThrow();
1060
+ });
1061
+
1062
+ it('attr() get on empty returns undefined', () => {
1063
+ expect(queryAll('.nonexistent').attr('id')).toBeUndefined();
1064
+ });
1065
+
1066
+ it('chaining on empty collection', () => {
1067
+ const col = queryAll('.nonexistent');
1068
+ const result = col.addClass('x').removeClass('x').toggleClass('y');
1069
+ expect(result).toBeInstanceOf(ZQueryCollection);
1070
+ expect(result.length).toBe(0);
1071
+ });
1072
+ });
1073
+
1074
+
1075
+ // ---------------------------------------------------------------------------
1076
+ // Collection wrapping edge cases
1077
+ // ---------------------------------------------------------------------------
1078
+
1079
+ describe('query - wrapping edge cases', () => {
1080
+ it('wraps an HTMLCollection', () => {
1081
+ const col = queryAll(document.getElementsByClassName('text'));
1082
+ expect(col).toBeInstanceOf(ZQueryCollection);
1083
+ expect(col.length).toBe(3);
1084
+ });
1085
+
1086
+ it('wraps a NodeList', () => {
1087
+ const col = queryAll(document.querySelectorAll('.text'));
1088
+ expect(col).toBeInstanceOf(ZQueryCollection);
1089
+ expect(col.length).toBe(3);
1090
+ });
1091
+
1092
+ it('wraps an Array of elements', () => {
1093
+ const arr = [document.getElementById('main'), document.getElementById('sidebar')];
1094
+ const col = queryAll(arr);
1095
+ expect(col.length).toBe(2);
1096
+ expect(col.first().id).toBe('main');
1097
+ });
1098
+
1099
+ it('query() wraps ZQueryCollection (returns as-is)', () => {
1100
+ const original = queryAll('.text');
1101
+ const wrapped = query(original);
1102
+ expect(wrapped).toBe(original);
1103
+ });
1104
+
1105
+ it('creates multiple elements from HTML', () => {
1106
+ const col = queryAll('<p>a</p><p>b</p><p>c</p>');
1107
+ expect(col.length).toBe(3);
1108
+ expect(col.first().textContent).toBe('a');
1109
+ expect(col.last().textContent).toBe('c');
1110
+ });
1111
+ });
1112
+
1113
+
1114
+ // ---------------------------------------------------------------------------
1115
+ // html() morphing advanced
1116
+ // ---------------------------------------------------------------------------
1117
+
1118
+ describe('ZQueryCollection - html() morphing advanced', () => {
1119
+ it('morphs complex nested structure', () => {
1120
+ document.body.innerHTML = '<div id="m"><ul><li id="i1">old1</li><li id="i2">old2</li></ul></div>';
1121
+ const li1 = document.getElementById('i1');
1122
+ queryAll('#m').html('<ul><li id="i1">new1</li><li id="i2">new2</li><li id="i3">new3</li></ul>');
1123
+ // li1 should be preserved (same id → morph)
1124
+ expect(document.getElementById('i1')).toBe(li1);
1125
+ expect(li1.textContent).toBe('new1');
1126
+ expect(document.querySelectorAll('#m li').length).toBe(3);
1127
+ });
1128
+
1129
+ it('morph() handles tag change at root', () => {
1130
+ document.body.innerHTML = '<div id="m"><p>old</p></div>';
1131
+ queryAll('#m').morph('<span>new</span>');
1132
+ expect(document.querySelector('#m span')).not.toBeNull();
1133
+ expect(document.querySelector('#m p')).toBeNull();
1134
+ });
1135
+ });
1136
+
1137
+
1138
+ // ---------------------------------------------------------------------------
1139
+ // Event delegation
1140
+ // ---------------------------------------------------------------------------
1141
+
1142
+ describe('ZQueryCollection - event delegation', () => {
1143
+ it('on() with selector delegates to matching children', () => {
1144
+ let clicked = null;
1145
+ queryAll('#nav').on('click', '.nav-item', function() { clicked = this.textContent; });
1146
+ document.querySelector('.nav-item.active').click();
1147
+ expect(clicked).toBe('Home');
1148
+ });
1149
+
1150
+ it('delegated event does not fire for non-matching elements', () => {
1151
+ let fired = false;
1152
+ queryAll('#main').on('click', '.nonexistent', () => { fired = true; });
1153
+ document.querySelector('.text').click();
1154
+ expect(fired).toBe(false);
1155
+ });
1156
+ });
1157
+
1158
+
1159
+ // ---------------------------------------------------------------------------
1160
+ // Multiple class operations
1161
+ // ---------------------------------------------------------------------------
1162
+
1163
+ describe('ZQueryCollection - multiple class operations', () => {
1164
+ it('addClass with space-separated classes', () => {
1165
+ const col = queryAll('#main');
1166
+ col.addClass('a', 'b', 'c');
1167
+ expect(col.first().classList.contains('a')).toBe(true);
1168
+ expect(col.first().classList.contains('b')).toBe(true);
1169
+ expect(col.first().classList.contains('c')).toBe(true);
1170
+ });
1171
+
1172
+ it('removeClass with multiple classes', () => {
1173
+ const col = queryAll('#main');
1174
+ col.addClass('x', 'y', 'z');
1175
+ col.removeClass('x', 'z');
1176
+ expect(col.first().classList.contains('x')).toBe(false);
1177
+ expect(col.first().classList.contains('y')).toBe(true);
1178
+ expect(col.first().classList.contains('z')).toBe(false);
1179
+ });
1180
+ });
1181
+
1182
+
1183
+ // ---------------------------------------------------------------------------
1184
+ // Traversal edge cases
1185
+ // ---------------------------------------------------------------------------
1186
+
1187
+ describe('ZQueryCollection - traversal edge cases', () => {
1188
+ it('find() returns empty when no descendants match', () => {
1189
+ expect(queryAll('#main').find('.nonexistent').length).toBe(0);
1190
+ });
1191
+
1192
+ it('parent() on body returns html', () => {
1193
+ const parents = queryAll('body').parent();
1194
+ expect(parents.first().tagName).toBe('HTML');
1195
+ });
1196
+
1197
+ it('children() with selector filters', () => {
1198
+ const col = queryAll('#main').children('.text');
1199
+ expect(col.length).toBe(3);
1200
+ });
1201
+
1202
+ it('closest() returns self if it matches', () => {
1203
+ const col = queryAll('#main');
1204
+ expect(col.closest('#main').first()).toBe(document.getElementById('main'));
1205
+ });
1206
+
1207
+ it('closest() returns empty when no match', () => {
1208
+ expect(queryAll('.text').closest('.nonexistent').length).toBe(0);
1209
+ });
1210
+
1211
+ it('siblings() returns all siblings', () => {
1212
+ const sibs = queryAll('.first-p').siblings();
1213
+ // siblings() returns all sibling elements except self
1214
+ expect(sibs.length).toBeGreaterThanOrEqual(2);
1215
+ });
1216
+
1217
+ it('next() at end returns empty', () => {
1218
+ const col = queryAll('.third-p');
1219
+ expect(col.next().length).toBe(0);
1220
+ });
1221
+
1222
+ it('prev() at start returns empty', () => {
1223
+ const col = queryAll('.first-p');
1224
+ expect(col.prev().length).toBe(0);
1225
+ });
1226
+ });
1227
+
1228
+
1229
+ // ---------------------------------------------------------------------------
1230
+ // DOM manipulation edge cases
1231
+ // ---------------------------------------------------------------------------
1232
+
1233
+ describe('ZQueryCollection - DOM manipulation edge cases', () => {
1234
+ it('append with element node', () => {
1235
+ const newEl = document.createElement('div');
1236
+ newEl.id = 'appended-el';
1237
+ queryAll('#main').append(newEl);
1238
+ expect(document.getElementById('appended-el')).not.toBeNull();
1239
+ expect(document.getElementById('appended-el').parentElement.id).toBe('main');
1240
+ });
1241
+
1242
+ it('prepend with element node', () => {
1243
+ const newEl = document.createElement('div');
1244
+ newEl.id = 'prepended-el';
1245
+ queryAll('#main').prepend(newEl);
1246
+ expect(document.getElementById('main').firstElementChild.id).toBe('prepended-el');
1247
+ });
1248
+
1249
+ it('remove on already-removed element does not throw', () => {
1250
+ const col = queryAll('.text').eq(0);
1251
+ col.remove();
1252
+ expect(() => col.remove()).not.toThrow();
1253
+ });
1254
+
1255
+ it('clone produces independent copy', () => {
1256
+ const original = queryAll('.first-p');
1257
+ const cloned = original.clone();
1258
+ cloned.addClass('cloned-class');
1259
+ expect(original.hasClass('cloned-class')).toBe(false);
1260
+ expect(cloned.hasClass('cloned-class')).toBe(true);
1261
+ });
1262
+
1263
+ it('empty() on already empty element', () => {
1264
+ document.body.innerHTML = '<div id="empty"></div>';
1265
+ expect(() => queryAll('#empty').empty()).not.toThrow();
1266
+ expect(document.getElementById('empty').children.length).toBe(0);
1267
+ });
1268
+ });
1269
+
1270
+
1271
+ // ---------------------------------------------------------------------------
1272
+ // Attribute edge cases
1273
+ // ---------------------------------------------------------------------------
1274
+
1275
+ describe('ZQueryCollection - attribute edge cases', () => {
1276
+ it('attr() set with sequential calls sets multiple attributes', () => {
1277
+ document.body.innerHTML = '<div id="a"></div>';
1278
+ queryAll('#a').attr('data-x', '1').attr('data-y', '2').attr('title', 'test');
1279
+ const el = document.getElementById('a');
1280
+ expect(el.getAttribute('data-x')).toBe('1');
1281
+ expect(el.getAttribute('data-y')).toBe('2');
1282
+ expect(el.getAttribute('title')).toBe('test');
1283
+ });
1284
+
1285
+ it('data() returns undefined for missing key', () => {
1286
+ expect(queryAll('#main').data('nonexistent')).toBeUndefined();
1287
+ });
1288
+
1289
+ it('removeAttr on nonexistent attribute does not throw', () => {
1290
+ expect(() => queryAll('#main').removeAttr('data-nope')).not.toThrow();
1291
+ });
1292
+ });
1293
+
1294
+
1295
+ // ---------------------------------------------------------------------------
1296
+ // css() advanced
1297
+ // ---------------------------------------------------------------------------
1298
+
1299
+ describe('ZQueryCollection - css() advanced', () => {
1300
+ it('sets a single style property via object', () => {
1301
+ document.body.innerHTML = '<div id="s">test</div>';
1302
+ queryAll('#s').css({ color: 'green' });
1303
+ expect(document.getElementById('s').style.color).toBe('green');
1304
+ });
1305
+
1306
+ it('sets multiple CSS properties', () => {
1307
+ document.body.innerHTML = '<div id="s2">test</div>';
1308
+ queryAll('#s2').css({ color: 'red', 'font-weight': 'bold', display: 'flex' });
1309
+ const el = document.getElementById('s2');
1310
+ expect(el.style.color).toBe('red');
1311
+ expect(el.style.display).toBe('flex');
1312
+ });
1313
+ });
1314
+
1315
+
1316
+ // ---------------------------------------------------------------------------
1317
+ // $.create advanced
1318
+ // ---------------------------------------------------------------------------
1319
+
1320
+ describe('query.create - advanced', () => {
1321
+ it('creates element with no attributes', () => {
1322
+ const col = query.create('span');
1323
+ expect(col.length).toBe(1);
1324
+ expect(col[0].tagName).toBe('SPAN');
1325
+ });
1326
+
1327
+ it('creates element with multiple children', () => {
1328
+ const child1 = document.createElement('span');
1329
+ child1.textContent = 'span child';
1330
+ const col = query.create('div', {}, 'text', child1);
1331
+ expect(col[0].childNodes.length).toBe(2);
1332
+ expect(col[0].childNodes[0].textContent).toBe('text');
1333
+ expect(col[0].querySelector('span').textContent).toBe('span child');
1334
+ });
1335
+
1336
+ it('creates element with boolean attributes', () => {
1337
+ const col = query.create('input', { type: 'text', disabled: '' });
1338
+ expect(col[0].tagName).toBe('INPUT');
1339
+ expect(col[0].getAttribute('type')).toBe('text');
1340
+ });
1341
+ });
1342
+
1343
+
1344
+ // ---------------------------------------------------------------------------
1345
+ // Prop edge cases
1346
+ // ---------------------------------------------------------------------------
1347
+
1348
+ describe('ZQueryCollection - prop() edge cases', () => {
1349
+ it('gets defaultValue property', () => {
1350
+ document.body.innerHTML = '<input value="initial">';
1351
+ const col = queryAll('input');
1352
+ expect(col.prop('defaultValue')).toBe('initial');
1353
+ });
1354
+
1355
+ it('gets tagName property', () => {
1356
+ const col = queryAll('#main');
1357
+ expect(col.prop('tagName')).toBe('DIV');
1358
+ });
1359
+
1360
+ it('prop on empty collection returns undefined', () => {
1361
+ expect(queryAll('.nonexistent').prop('checked')).toBeUndefined();
1362
+ });
1363
+ });
1364
+
1365
+
1366
+ // ---------------------------------------------------------------------------
1367
+ // BUG FIX: siblings() with selector filtering + null parent guard
1368
+ // ---------------------------------------------------------------------------
1369
+
1370
+ describe('ZQueryCollection - siblings() fixes', () => {
1371
+ it('filters siblings by selector', () => {
1372
+ document.body.innerHTML = '<div><p class="a">1</p><p class="b">2</p><p class="a">3</p></div>';
1373
+ const sibs = queryAll('.b').siblings('.a');
1374
+ expect(sibs.length).toBe(2);
1375
+ });
1376
+
1377
+ it('returns all siblings when no selector given', () => {
1378
+ document.body.innerHTML = '<div><p>1</p><p id="mid">2</p><p>3</p></div>';
1379
+ const sibs = queryAll('#mid').siblings();
1380
+ expect(sibs.length).toBe(2);
1381
+ });
1382
+
1383
+ it('does not crash on detached element (no parentElement)', () => {
1384
+ const detached = document.createElement('div');
1385
+ const col = new ZQueryCollection([detached]);
1386
+ expect(() => col.siblings()).not.toThrow();
1387
+ expect(col.siblings().length).toBe(0);
1388
+ });
1389
+ });
1390
+
1391
+
1392
+ // ---------------------------------------------------------------------------
1393
+ // BUG FIX: ZQueryCollection constructor null safety
1394
+ // ---------------------------------------------------------------------------
1395
+
1396
+ describe('ZQueryCollection - constructor null/undefined safety', () => {
1397
+ it('creates empty collection from null', () => {
1398
+ const col = new ZQueryCollection(null);
1399
+ expect(col.length).toBe(0);
1400
+ });
1401
+
1402
+ it('creates empty collection from undefined', () => {
1403
+ const col = new ZQueryCollection(undefined);
1404
+ expect(col.length).toBe(0);
1405
+ });
1406
+
1407
+ it('wraps a single element', () => {
1408
+ const el = document.createElement('div');
1409
+ const col = new ZQueryCollection(el);
1410
+ expect(col.length).toBe(1);
1411
+ expect(col[0]).toBe(el);
1412
+ });
1413
+ });
1414
+
1415
+
1416
+ // ---------------------------------------------------------------------------
1417
+ // BUG FIX: attr() with object syntax
1418
+ // ---------------------------------------------------------------------------
1419
+
1420
+ describe('ZQueryCollection - attr() object set', () => {
1421
+ it('sets multiple attributes with object', () => {
1422
+ document.body.innerHTML = '<div id="at"></div>';
1423
+ queryAll('#at').attr({ 'data-x': '1', 'data-y': '2', title: 'hello' });
1424
+ const el = document.getElementById('at');
1425
+ expect(el.getAttribute('data-x')).toBe('1');
1426
+ expect(el.getAttribute('data-y')).toBe('2');
1427
+ expect(el.getAttribute('title')).toBe('hello');
1428
+ });
1429
+ });
1430
+
1431
+
1432
+ // ---------------------------------------------------------------------------
1433
+ // BUG FIX: css() two-argument setter
1434
+ // ---------------------------------------------------------------------------
1435
+
1436
+ describe('ZQueryCollection - css() two-argument setter', () => {
1437
+ it('sets a CSS property with key-value arguments', () => {
1438
+ document.body.innerHTML = '<div id="cs">text</div>';
1439
+ queryAll('#cs').css('color', 'green');
1440
+ expect(document.getElementById('cs').style.color).toBe('green');
1441
+ });
1442
+
1443
+ it('still works as getter with single string arg', () => {
1444
+ document.body.innerHTML = '<div id="cs2" style="color: red;">text</div>';
1445
+ const val = queryAll('#cs2').css('color');
1446
+ expect(val).toBeDefined();
1447
+ });
1448
+
1449
+ it('still works as setter with object arg', () => {
1450
+ document.body.innerHTML = '<div id="cs3">text</div>';
1451
+ queryAll('#cs3').css({ color: 'blue', display: 'flex' });
1452
+ const el = document.getElementById('cs3');
1453
+ expect(el.style.color).toBe('blue');
1454
+ expect(el.style.display).toBe('flex');
1455
+ });
1456
+ });
1457
+
1458
+
1459
+ // ---------------------------------------------------------------------------
1460
+ // BUG FIX: wrap() does not crash on empty/invalid wrapper
1461
+ // ---------------------------------------------------------------------------
1462
+
1463
+ describe('ZQueryCollection - wrap() safety', () => {
1464
+ it('does not crash if wrapper string is empty', () => {
1465
+ document.body.innerHTML = '<div id="w"><p>inside</p></div>';
1466
+ expect(() => queryAll('#w p').wrap('')).not.toThrow();
1467
+ });
1468
+
1469
+ it('does not crash on detached element (no parentNode)', () => {
1470
+ const detached = document.createElement('span');
1471
+ const col = new ZQueryCollection([detached]);
1472
+ expect(() => col.wrap('<div></div>')).not.toThrow();
1473
+ });
1474
+ });
1475
+
1476
+
1477
+ // ---------------------------------------------------------------------------
1478
+ // BUG FIX: index() does not crash on detached element
1479
+ // ---------------------------------------------------------------------------
1480
+
1481
+ describe('ZQueryCollection - index() null parent safety', () => {
1482
+ it('returns -1 for detached element', () => {
1483
+ const detached = document.createElement('div');
1484
+ const col = new ZQueryCollection([detached]);
1485
+ expect(col.index()).toBe(-1);
1486
+ });
1487
+ });
1488
+
1489
+
1490
+ // ---------------------------------------------------------------------------
1491
+ // BUG FIX: delegated on() / off() handler removal
1492
+ // ---------------------------------------------------------------------------
1493
+
1494
+ describe('ZQueryCollection - delegated on/off', () => {
1495
+ it('off() removes delegated event handlers', () => {
1496
+ document.body.innerHTML = '<div id="parent"><button class="btn">click</button></div>';
1497
+ const parent = new ZQueryCollection([document.getElementById('parent')]);
1498
+ const handler = vi.fn();
1499
+
1500
+ parent.on('click', '.btn', handler);
1501
+ document.querySelector('.btn').click();
1502
+ expect(handler).toHaveBeenCalledTimes(1);
1503
+
1504
+ parent.off('click', handler);
1505
+ document.querySelector('.btn').click();
1506
+ // Should not fire again after off()
1507
+ expect(handler).toHaveBeenCalledTimes(1);
1508
+ });
1509
+ });
1510
+
1511
+
1512
+ // ---------------------------------------------------------------------------
1513
+ // BUG FIX: animate() resolves immediately on empty collection
1514
+ // ---------------------------------------------------------------------------
1515
+
1516
+ describe('ZQueryCollection - animate() empty collection', () => {
1517
+ it('resolves immediately when collection is empty', async () => {
1518
+ const col = new ZQueryCollection([]);
1519
+ const result = await col.animate({ opacity: '0' }, 50);
1520
+ expect(result).toBe(col);
1521
+ });
1522
+ });
1523
+
1524
+
1525
+ // ===========================================================================
1526
+ // one() - single-fire event listener
1527
+ // ===========================================================================
1528
+
1529
+ describe('ZQueryCollection - one()', () => {
1530
+ it('fires handler only once', () => {
1531
+ const handler = vi.fn();
1532
+ document.body.innerHTML = '<button id="one-btn">click</button>';
1533
+ const col = query('#one-btn');
1534
+ col.one('click', handler);
1535
+ document.querySelector('#one-btn').click();
1536
+ document.querySelector('#one-btn').click();
1537
+ expect(handler).toHaveBeenCalledTimes(1);
1538
+ });
1539
+ });
1540
+
1541
+
1542
+ // ===========================================================================
1543
+ // toggle() - show/hide toggle
1544
+ // ===========================================================================
1545
+
1546
+ describe('ZQueryCollection - toggle()', () => {
1547
+ it('hides a visible element', () => {
1548
+ const el = document.querySelector('#main');
1549
+ el.style.display = '';
1550
+ const col = query('#main');
1551
+ col.toggle();
1552
+ expect(el.style.display).toBe('none');
1553
+ });
1554
+
1555
+ it('shows a hidden element', () => {
1556
+ const el = document.querySelector('#main');
1557
+ el.style.display = 'none';
1558
+ const col = query('#main');
1559
+ col.toggle();
1560
+ expect(el.style.display).toBe('');
1561
+ });
1562
+
1563
+ it('uses custom display value when showing', () => {
1564
+ const el = document.querySelector('#main');
1565
+ el.style.display = 'none';
1566
+ const col = query('#main');
1567
+ col.toggle('flex');
1568
+ expect(el.style.display).toBe('flex');
1569
+ });
1570
+ });
1571
+
1572
+
1573
+ // ===========================================================================
1574
+ // serialize() and serializeObject()
1575
+ // ===========================================================================
1576
+
1577
+ describe('ZQueryCollection - serialize()', () => {
1578
+ it('serializes form inputs to URL-encoded string', () => {
1579
+ document.body.innerHTML = '<form id="f"><input name="user" value="Alice"><input name="age" value="30"></form>';
1580
+ const result = query('#f').serialize();
1581
+ expect(result).toContain('user=Alice');
1582
+ expect(result).toContain('age=30');
1583
+ });
1584
+
1585
+ it('returns empty string for non-form element', () => {
1586
+ expect(query('#main').serialize()).toBe('');
1587
+ });
1588
+ });
1589
+
1590
+ describe('ZQueryCollection - serializeObject()', () => {
1591
+ it('builds an object from form fields', () => {
1592
+ document.body.innerHTML = '<form id="f"><input name="user" value="Alice"><input name="age" value="30"></form>';
1593
+ expect(query('#f').serializeObject()).toEqual({ user: 'Alice', age: '30' });
1594
+ });
1595
+
1596
+ it('groups duplicate keys into arrays', () => {
1597
+ document.body.innerHTML = `<form id="f">
1598
+ <input name="tags" value="a">
1599
+ <input name="tags" value="b">
1600
+ <input name="tags" value="c">
1601
+ </form>`;
1602
+ expect(query('#f').serializeObject()).toEqual({ tags: ['a', 'b', 'c'] });
1603
+ });
1604
+
1605
+ it('returns empty object for non-form element', () => {
1606
+ expect(query('#main').serializeObject()).toEqual({});
1607
+ });
1608
+ });
1609
+
1610
+
1611
+ // ===========================================================================
1612
+ // $.ready
1613
+ // ===========================================================================
1614
+
1615
+ describe('$.ready', () => {
1616
+ it('calls function immediately when document is not loading', () => {
1617
+ const fn = vi.fn();
1618
+ query.ready(fn);
1619
+ expect(fn).toHaveBeenCalledTimes(1);
1620
+ });
1621
+ });
1622
+
1623
+
1624
+ // ===========================================================================
1625
+ // $.name
1626
+ // ===========================================================================
1627
+
1628
+ describe('$.name', () => {
1629
+ it('selects elements by name attribute', () => {
1630
+ document.body.innerHTML = '<input name="email" value="a@b.com"><input name="email" value="x@y.com"><input name="other">';
1631
+ const result = query.name('email');
1632
+ expect(result.length).toBe(2);
1633
+ });
1634
+ });
1635
+
1636
+
1637
+ // ===========================================================================
1638
+ // $.create
1639
+ // ===========================================================================
1640
+
1641
+ describe('$.create', () => {
1642
+ it('creates an element with attributes', () => {
1643
+ const col = query.create('div', { id: 'test', class: 'box' }, 'hello');
1644
+ const el = col.first();
1645
+ expect(el.tagName).toBe('DIV');
1646
+ expect(el.id).toBe('test');
1647
+ expect(el.className).toBe('box');
1648
+ expect(el.textContent).toBe('hello');
1649
+ });
1650
+
1651
+ it('applies style object', () => {
1652
+ const col = query.create('span', { style: { color: 'red', fontSize: '20px' } });
1653
+ const el = col.first();
1654
+ expect(el.style.color).toBe('red');
1655
+ expect(el.style.fontSize).toBe('20px');
1656
+ });
1657
+
1658
+ it('binds event handlers via on* attributes', () => {
1659
+ const handler = vi.fn();
1660
+ const col = query.create('button', { onclick: handler }, 'click me');
1661
+ col.first().click();
1662
+ expect(handler).toHaveBeenCalledTimes(1);
1663
+ });
1664
+
1665
+ it('sets data attributes from data object', () => {
1666
+ const col = query.create('div', { data: { userId: '42', role: 'admin' } });
1667
+ const el = col.first();
1668
+ expect(el.dataset.userId).toBe('42');
1669
+ expect(el.dataset.role).toBe('admin');
1670
+ });
1671
+
1672
+ it('appends child Node elements', () => {
1673
+ const child = document.createElement('span');
1674
+ child.textContent = 'child';
1675
+ const col = query.create('div', {}, child);
1676
+ const el = col.first();
1677
+ expect(el.children.length).toBe(1);
1678
+ expect(el.querySelector('span').textContent).toBe('child');
1679
+ });
1680
+ });
1681
+
1682
+
1683
+ // ===========================================================================
1684
+ // data() - no key returns full dataset
1685
+ // ===========================================================================
1686
+
1687
+ describe('ZQueryCollection - data() full dataset', () => {
1688
+ it('returns the full dataset when no key is given', () => {
1689
+ document.body.innerHTML = '<div id="d" data-x="1" data-y="2"></div>';
1690
+ const ds = query('#d').data();
1691
+ expect(ds.x).toBe('1');
1692
+ expect(ds.y).toBe('2');
1693
+ });
1694
+ });
1695
+
1696
+
1697
+ // ===========================================================================
1698
+ // css() getter on empty collection
1699
+ // ===========================================================================
1700
+
1701
+ describe('ZQueryCollection - css() empty collection', () => {
1702
+ it('returns undefined when collection is empty', () => {
1703
+ const col = new ZQueryCollection([]);
1704
+ expect(col.css('color')).toBeUndefined();
1705
+ });
1706
+ });
1707
+
1708
+
1709
+ // ===========================================================================
1710
+ // append/prepend/after/before with Node
1711
+ // ===========================================================================
1712
+
1713
+ describe('ZQueryCollection - append/prepend with Node', () => {
1714
+ it('appends a Node element', () => {
1715
+ document.body.innerHTML = '<div id="container"><p>existing</p></div>';
1716
+ const newNode = document.createElement('span');
1717
+ newNode.textContent = 'appended';
1718
+ query('#container').append(newNode);
1719
+ expect(document.querySelector('#container span').textContent).toBe('appended');
1720
+ expect(document.querySelector('#container').lastElementChild.tagName).toBe('SPAN');
1721
+ });
1722
+
1723
+ it('prepends a Node element', () => {
1724
+ document.body.innerHTML = '<div id="container"><p>existing</p></div>';
1725
+ const newNode = document.createElement('span');
1726
+ newNode.textContent = 'prepended';
1727
+ query('#container').prepend(newNode);
1728
+ expect(document.querySelector('#container').firstElementChild.tagName).toBe('SPAN');
1729
+ });
1730
+
1731
+ it('appends a ZQueryCollection', () => {
1732
+ document.body.innerHTML = '<div id="container"></div><span class="source">item</span>';
1733
+ const source = queryAll('.source');
1734
+ query('#container').append(source);
1735
+ expect(document.querySelector('#container span').textContent).toBe('item');
1736
+ });
1737
+ });
1738
+
1739
+ describe('ZQueryCollection - after/before with Node', () => {
1740
+ it('inserts Node after element', () => {
1741
+ document.body.innerHTML = '<div id="anchor"></div>';
1742
+ const newNode = document.createElement('span');
1743
+ newNode.id = 'after';
1744
+ query('#anchor').after(newNode);
1745
+ expect(document.querySelector('#anchor').nextElementSibling.id).toBe('after');
1746
+ });
1747
+
1748
+ it('inserts Node before element', () => {
1749
+ document.body.innerHTML = '<div id="anchor"></div>';
1750
+ const newNode = document.createElement('span');
1751
+ newNode.id = 'before';
1752
+ query('#anchor').before(newNode);
1753
+ expect(document.querySelector('#anchor').previousElementSibling.id).toBe('before');
1754
+ });
1755
+ });
1756
+
1757
+
1758
+ // ===========================================================================
1759
+ // replaceWith using Node
1760
+ // ===========================================================================
1761
+
1762
+ describe('ZQueryCollection - replaceWith(Node)', () => {
1763
+ it('replaces element with a Node', () => {
1764
+ document.body.innerHTML = '<div id="old">old</div>';
1765
+ const newNode = document.createElement('span');
1766
+ newNode.id = 'new';
1767
+ newNode.textContent = 'replaced';
1768
+ query('#old').replaceWith(newNode);
1769
+ expect(document.querySelector('#old')).toBeNull();
1770
+ expect(document.querySelector('#new').textContent).toBe('replaced');
1771
+ });
1772
+ });
1773
+
1774
+
1775
+ // ===========================================================================
1776
+ // nextUntil/prevUntil/parentsUntil with filter
1777
+ // ===========================================================================
1778
+
1779
+ describe('ZQueryCollection - nextUntil with filter', () => {
1780
+ it('collects siblings until stop selector, applying filter', () => {
1781
+ document.body.innerHTML = '<div id="start"></div><span class="a">A</span><p>P</p><span class="a">A2</span><div id="stop"></div>';
1782
+ const result = query('#start').nextUntil('#stop', 'span');
1783
+ expect(result.length).toBe(2); // only <span> siblings
1784
+ });
1785
+ });
1786
+
1787
+ describe('ZQueryCollection - prevUntil with filter', () => {
1788
+ it('collects previous siblings until stop selector, applying filter', () => {
1789
+ document.body.innerHTML = '<div id="stop"></div><span>A</span><p>P</p><span>B</span><div id="end"></div>';
1790
+ const result = query('#end').prevUntil('#stop', 'span');
1791
+ expect(result.length).toBe(2);
1792
+ });
1793
+ });
1794
+
1795
+ describe('ZQueryCollection - parentsUntil with filter', () => {
1796
+ it('collects parent elements until stop selector, applying filter', () => {
1797
+ document.body.innerHTML = '<section><article><div><span id="target"></span></div></article></section>';
1798
+ const result = query('#target').parentsUntil('section', 'div');
1799
+ expect(result.length).toBe(1);
1800
+ expect(result.first().tagName).toBe('DIV');
1801
+ });
1802
+ });
1803
+
1804
+
1805
+ // ===========================================================================
1806
+ // delegated on() at document level
1807
+ // ===========================================================================
1808
+
1809
+ describe('ZQueryCollection - delegated on()', () => {
1810
+ it('delegates event to matching child selector', () => {
1811
+ document.body.innerHTML = '<div id="container"><button class="action">click</button></div>';
1812
+ const handler = vi.fn();
1813
+ query('#container').on('click', '.action', handler);
1814
+ document.querySelector('.action').click();
1815
+ expect(handler).toHaveBeenCalledTimes(1);
1816
+ });
1817
+
1818
+ it('does not fire for non-matching elements', () => {
1819
+ document.body.innerHTML = '<div id="container"><span class="other">x</span></div>';
1820
+ const handler = vi.fn();
1821
+ query('#container').on('click', '.action', handler);
1822
+ document.querySelector('.other').click();
1823
+ expect(handler).not.toHaveBeenCalled();
1824
+ });
1825
+ });
1826
+
1827
+
1828
+ // ===========================================================================
1829
+ // Multi-event on/off
1830
+ // ===========================================================================
1831
+
1832
+ describe('ZQueryCollection - multi-event on()', () => {
1833
+ it('binds handler to multiple space-separated events', () => {
1834
+ document.body.innerHTML = '<input id="inp" type="text">';
1835
+ const handler = vi.fn();
1836
+ query('#inp').on('focus blur', handler);
1837
+ document.querySelector('#inp').dispatchEvent(new Event('focus'));
1838
+ document.querySelector('#inp').dispatchEvent(new Event('blur'));
1839
+ expect(handler).toHaveBeenCalledTimes(2);
1840
+ });
1841
+ });
1842
+
1843
+
1844
+ // ===========================================================================
1845
+ // scrollTop/scrollLeft getters
1846
+ // ===========================================================================
1847
+
1848
+ describe('ZQueryCollection - scrollTop/scrollLeft', () => {
1849
+ it('gets and sets scrollTop', () => {
1850
+ document.body.innerHTML = '<div id="scr" style="overflow:auto; height: 50px;"><div style="height:200px;">x</div></div>';
1851
+ const el = document.querySelector('#scr');
1852
+ query('#scr').scrollTop(100);
1853
+ expect(el.scrollTop).toBe(100);
1854
+ });
1855
+
1856
+ it('gets scrollTop value', () => {
1857
+ document.body.innerHTML = '<div id="scr" style="overflow:auto; height: 50px;"><div style="height:200px;">x</div></div>';
1858
+ document.querySelector('#scr').scrollTop = 50;
1859
+ expect(query('#scr').scrollTop()).toBe(50);
1860
+ });
1861
+ });
1862
+
1863
+
1864
+ // ===========================================================================
1865
+ // slideDown/slideUp set styles
1866
+ // ===========================================================================
1867
+
1868
+ describe('ZQueryCollection - slideDown/slideUp', () => {
1869
+ it('slideDown sets overflow hidden and maxHeight initially', () => {
1870
+ vi.useFakeTimers();
1871
+ document.body.innerHTML = '<div id="slide" style="display:none;">content</div>';
1872
+ query('#slide').slideDown(100);
1873
+ const el = document.querySelector('#slide');
1874
+ expect(el.style.overflow).toBe('hidden');
1875
+ // maxHeight could be '0' or '0px' depending on jsdom normalization
1876
+ expect(el.style.maxHeight).toMatch(/^0(px)?$/);
1877
+ vi.advanceTimersByTime(100);
1878
+ vi.useRealTimers();
1879
+ });
1880
+
1881
+ it('slideUp hides element after duration', () => {
1882
+ vi.useFakeTimers();
1883
+ document.body.innerHTML = '<div id="slide">content</div>';
1884
+ query('#slide').slideUp(100);
1885
+ vi.advanceTimersByTime(100);
1886
+ expect(document.querySelector('#slide').style.display).toBe('none');
1887
+ vi.useRealTimers();
1888
+ });
1889
+ });
1890
+
1891
+
1892
+ // ===========================================================================
1893
+ // fadeIn/fadeOut set opacity
1894
+ // ===========================================================================
1895
+
1896
+ describe('ZQueryCollection - fadeIn/fadeOut', () => {
1897
+ it('fadeIn sets initial opacity to 0', () => {
1898
+ document.body.innerHTML = '<div id="fade" style="display:none;">content</div>';
1899
+ query('#fade').fadeIn(100);
1900
+ const el = document.querySelector('#fade');
1901
+ expect(el.style.opacity).toBe('0');
1902
+ });
1903
+
1904
+ it('fadeTo animates to specified opacity', () => {
1905
+ document.body.innerHTML = '<div id="fade">content</div>';
1906
+ query('#fade').fadeTo(100, 0.5);
1907
+ // Animation starts - just verify no throw
1908
+ expect(document.querySelector('#fade')).not.toBeNull();
1909
+ });
1910
+ });