zero-query 0.6.3 → 0.8.6

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 (72) hide show
  1. package/README.md +39 -29
  2. package/cli/commands/build.js +113 -4
  3. package/cli/commands/bundle.js +392 -29
  4. package/cli/commands/create.js +1 -1
  5. package/cli/commands/dev/devtools/index.js +56 -0
  6. package/cli/commands/dev/devtools/js/components.js +49 -0
  7. package/cli/commands/dev/devtools/js/core.js +409 -0
  8. package/cli/commands/dev/devtools/js/elements.js +413 -0
  9. package/cli/commands/dev/devtools/js/network.js +166 -0
  10. package/cli/commands/dev/devtools/js/performance.js +73 -0
  11. package/cli/commands/dev/devtools/js/router.js +105 -0
  12. package/cli/commands/dev/devtools/js/source.js +132 -0
  13. package/cli/commands/dev/devtools/js/stats.js +35 -0
  14. package/cli/commands/dev/devtools/js/tabs.js +79 -0
  15. package/cli/commands/dev/devtools/panel.html +95 -0
  16. package/cli/commands/dev/devtools/styles.css +244 -0
  17. package/cli/commands/dev/index.js +29 -4
  18. package/cli/commands/dev/logger.js +6 -1
  19. package/cli/commands/dev/overlay.js +428 -2
  20. package/cli/commands/dev/server.js +42 -5
  21. package/cli/commands/dev/watcher.js +59 -1
  22. package/cli/help.js +8 -5
  23. package/cli/scaffold/{scripts → app}/app.js +16 -23
  24. package/cli/scaffold/{scripts → app}/components/about.js +4 -4
  25. package/cli/scaffold/{scripts → app}/components/api-demo.js +1 -1
  26. package/cli/scaffold/{scripts → app}/components/contacts/contacts.css +0 -7
  27. package/cli/scaffold/{scripts → app}/components/contacts/contacts.html +3 -3
  28. package/cli/scaffold/app/components/home.js +137 -0
  29. package/cli/scaffold/{scripts → app}/routes.js +1 -1
  30. package/cli/scaffold/{scripts → app}/store.js +6 -6
  31. package/cli/scaffold/assets/.gitkeep +0 -0
  32. package/cli/scaffold/{styles/styles.css → global.css} +4 -2
  33. package/cli/scaffold/index.html +12 -11
  34. package/cli/utils.js +111 -6
  35. package/dist/zquery.dist.zip +0 -0
  36. package/dist/zquery.js +1122 -158
  37. package/dist/zquery.min.js +3 -16
  38. package/index.d.ts +129 -1290
  39. package/index.js +15 -10
  40. package/package.json +7 -6
  41. package/src/component.js +172 -49
  42. package/src/core.js +359 -18
  43. package/src/diff.js +256 -58
  44. package/src/expression.js +33 -3
  45. package/src/reactive.js +37 -5
  46. package/src/router.js +243 -7
  47. package/tests/component.test.js +886 -0
  48. package/tests/core.test.js +977 -0
  49. package/tests/diff.test.js +525 -0
  50. package/tests/errors.test.js +162 -0
  51. package/tests/expression.test.js +482 -0
  52. package/tests/http.test.js +289 -0
  53. package/tests/reactive.test.js +339 -0
  54. package/tests/router.test.js +649 -0
  55. package/tests/store.test.js +379 -0
  56. package/tests/utils.test.js +512 -0
  57. package/types/collection.d.ts +383 -0
  58. package/types/component.d.ts +217 -0
  59. package/types/errors.d.ts +103 -0
  60. package/types/http.d.ts +81 -0
  61. package/types/misc.d.ts +179 -0
  62. package/types/reactive.d.ts +76 -0
  63. package/types/router.d.ts +161 -0
  64. package/types/ssr.d.ts +49 -0
  65. package/types/store.d.ts +107 -0
  66. package/types/utils.d.ts +142 -0
  67. package/cli/commands/dev.old.js +0 -520
  68. package/cli/scaffold/scripts/components/home.js +0 -137
  69. /package/cli/scaffold/{scripts → app}/components/contacts/contacts.js +0 -0
  70. /package/cli/scaffold/{scripts → app}/components/counter.js +0 -0
  71. /package/cli/scaffold/{scripts → app}/components/not-found.js +0 -0
  72. /package/cli/scaffold/{scripts → app}/components/todos.js +0 -0
@@ -0,0 +1,977 @@
1
+ import { describe, it, expect, beforeEach } 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
+
551
+
552
+ // ---------------------------------------------------------------------------
553
+ // New filtering / collection methods
554
+ // ---------------------------------------------------------------------------
555
+
556
+ describe('filtering & collection', () => {
557
+ it('is() checks if any element matches selector', () => {
558
+ expect(queryAll('#main').children().is('p')).toBe(true);
559
+ expect(queryAll('#main').children().is('table')).toBe(false);
560
+ });
561
+
562
+ it('is() with function', () => {
563
+ const result = queryAll('.text').is(function(i) { return this.textContent === 'World'; });
564
+ expect(result).toBe(true);
565
+ });
566
+
567
+ it('has() keeps elements containing matching descendant', () => {
568
+ const col = queryAll('#sidebar').has('.nav-item');
569
+ expect(col.length).toBe(1);
570
+ });
571
+
572
+ it('slice() returns subset', () => {
573
+ const col = queryAll('.text').slice(0, 2);
574
+ expect(col.length).toBe(2);
575
+ expect(col.first().textContent).toBe('Hello');
576
+ });
577
+
578
+ it('slice() with negative index', () => {
579
+ const col = queryAll('.text').slice(-1);
580
+ expect(col.length).toBe(1);
581
+ expect(col.first().textContent).toBe('Extra');
582
+ });
583
+
584
+ it('add() merges collections', () => {
585
+ const col = queryAll('.first-p').add('.other');
586
+ expect(col.length).toBe(2);
587
+ });
588
+
589
+ it('add() with element', () => {
590
+ const el = document.getElementById('main');
591
+ const col = queryAll('.first-p').add(el);
592
+ expect(col.length).toBe(2);
593
+ });
594
+
595
+ it('add() with ZQueryCollection', () => {
596
+ const col = queryAll('.first-p').add(queryAll('.other'));
597
+ expect(col.length).toBe(2);
598
+ });
599
+
600
+ it('get() with no args returns array', () => {
601
+ const arr = queryAll('.text').get();
602
+ expect(Array.isArray(arr)).toBe(true);
603
+ expect(arr.length).toBe(3);
604
+ });
605
+
606
+ it('get(index) returns element', () => {
607
+ const el = queryAll('.text').get(1);
608
+ expect(el.textContent).toBe('World');
609
+ });
610
+
611
+ it('get(negative) returns from end', () => {
612
+ const el = queryAll('.text').get(-1);
613
+ expect(el.textContent).toBe('Extra');
614
+ });
615
+
616
+ it('index() returns position among siblings', () => {
617
+ const col = queryAll('.other');
618
+ expect(col.index()).toBe(2); // 3rd child (0-indexed)
619
+ });
620
+
621
+ it('index(element) returns position in collection', () => {
622
+ const other = document.querySelector('.other');
623
+ const col = queryAll('#main').children();
624
+ expect(col.index(other)).toBe(2);
625
+ });
626
+ });
627
+
628
+
629
+ // ---------------------------------------------------------------------------
630
+ // Inverse DOM manipulation (appendTo, prependTo, insertAfter, insertBefore, etc.)
631
+ // ---------------------------------------------------------------------------
632
+
633
+ describe('inverse DOM manipulation', () => {
634
+ it('appendTo() moves elements into target', () => {
635
+ queryAll('<div class="new-item">new</div>').appendTo('#nav');
636
+ const nav = document.getElementById('nav');
637
+ expect(nav.lastElementChild.className).toBe('new-item');
638
+ });
639
+
640
+ it('prependTo() inserts at start of target', () => {
641
+ queryAll('<li class="first-item">first</li>').prependTo('#nav');
642
+ const nav = document.getElementById('nav');
643
+ expect(nav.firstElementChild.className).toBe('first-item');
644
+ });
645
+
646
+ it('insertAfter() inserts after target', () => {
647
+ queryAll('<span class="inserted">!</span>').insertAfter('.first-p');
648
+ const firstP = document.querySelector('.first-p');
649
+ expect(firstP.nextElementSibling.className).toBe('inserted');
650
+ });
651
+
652
+ it('insertBefore() inserts before target', () => {
653
+ queryAll('<span class="inserted">!</span>').insertBefore('.second-p');
654
+ const secondP = document.querySelector('.second-p');
655
+ expect(secondP.previousElementSibling.className).toBe('inserted');
656
+ });
657
+
658
+ it('replaceAll() replaces target elements', () => {
659
+ queryAll('<em>replaced</em>').replaceAll('.other');
660
+ expect(document.querySelector('.other')).toBeNull();
661
+ expect(document.querySelector('em').textContent).toBe('replaced');
662
+ });
663
+
664
+ it('unwrap() removes parent wrapper', () => {
665
+ // wrap nav-items in a div first
666
+ const items = queryAll('.nav-item');
667
+ const count = items.length;
668
+ items.eq(0).wrap('<div class="wrapper"></div>');
669
+ expect(document.querySelector('.wrapper')).not.toBeNull();
670
+ queryAll('.nav-item').eq(0).unwrap('.wrapper');
671
+ expect(document.querySelector('.wrapper')).toBeNull();
672
+ });
673
+
674
+ it('wrapAll() wraps all elements in one wrapper', () => {
675
+ queryAll('.nav-item').wrapAll('<div class="all-wrap"></div>');
676
+ const wrap = document.querySelector('.all-wrap');
677
+ expect(wrap).not.toBeNull();
678
+ expect(wrap.children.length).toBe(3);
679
+ });
680
+
681
+ it('wrapInner() wraps inner contents', () => {
682
+ queryAll('.first-p').wrapInner('<strong></strong>');
683
+ const strong = document.querySelector('.first-p > strong');
684
+ expect(strong).not.toBeNull();
685
+ expect(strong.textContent).toBe('Hello');
686
+ });
687
+
688
+ it('detach() removes elements (alias for remove)', () => {
689
+ queryAll('.other').detach();
690
+ expect(document.querySelector('.other')).toBeNull();
691
+ });
692
+ });
693
+
694
+
695
+ // ---------------------------------------------------------------------------
696
+ // CSS dimension methods
697
+ // ---------------------------------------------------------------------------
698
+
699
+ describe('CSS dimension methods', () => {
700
+ it('scrollTop() get returns a number', () => {
701
+ const val = queryAll('#main').scrollTop();
702
+ expect(typeof val).toBe('number');
703
+ });
704
+
705
+ it('scrollTop(value) sets scroll position', () => {
706
+ const main = document.getElementById('main');
707
+ main.style.overflow = 'auto';
708
+ main.style.height = '10px';
709
+ main.innerHTML = '<div style="height:1000px">tall</div>';
710
+ queryAll('#main').scrollTop(50);
711
+ expect(main.scrollTop).toBe(50);
712
+ });
713
+
714
+ it('scrollLeft() get returns a number', () => {
715
+ const val = queryAll('#main').scrollLeft();
716
+ expect(typeof val).toBe('number');
717
+ });
718
+
719
+ it('innerWidth() returns clientWidth', () => {
720
+ const main = document.getElementById('main');
721
+ // jsdom sets clientWidth to 0 but the method should still return a number
722
+ const val = queryAll('#main').innerWidth();
723
+ expect(typeof val).toBe('number');
724
+ });
725
+
726
+ it('innerHeight() returns clientHeight', () => {
727
+ const val = queryAll('#main').innerHeight();
728
+ expect(typeof val).toBe('number');
729
+ });
730
+
731
+ it('outerWidth() returns offsetWidth', () => {
732
+ const val = queryAll('#main').outerWidth();
733
+ expect(typeof val).toBe('number');
734
+ });
735
+
736
+ it('outerHeight() returns offsetHeight', () => {
737
+ const val = queryAll('#main').outerHeight();
738
+ expect(typeof val).toBe('number');
739
+ });
740
+
741
+ it('outerWidth(true) includes margin', () => {
742
+ const main = document.getElementById('main');
743
+ main.style.margin = '10px';
744
+ const val = queryAll('#main').outerWidth(true);
745
+ expect(typeof val).toBe('number');
746
+ });
747
+ });
748
+
749
+
750
+ // ---------------------------------------------------------------------------
751
+ // prop() method
752
+ // ---------------------------------------------------------------------------
753
+
754
+ describe('ZQueryCollection — prop()', () => {
755
+ it('gets a DOM property', () => {
756
+ document.body.innerHTML = '<input type="checkbox" checked>';
757
+ const col = queryAll('input');
758
+ expect(col.prop('checked')).toBe(true);
759
+ });
760
+
761
+ it('sets a DOM property', () => {
762
+ document.body.innerHTML = '<input type="checkbox">';
763
+ const col = queryAll('input');
764
+ col.prop('checked', true);
765
+ expect(col[0].checked).toBe(true);
766
+ });
767
+
768
+ it('sets multiple properties via sequential calls', () => {
769
+ document.body.innerHTML = '<input type="text">';
770
+ const col = queryAll('input');
771
+ col.prop('disabled', true);
772
+ col.prop('value', 'hello');
773
+ expect(col[0].disabled).toBe(true);
774
+ expect(col[0].value).toBe('hello');
775
+ });
776
+ });
777
+
778
+
779
+ // ---------------------------------------------------------------------------
780
+ // css() method
781
+ // ---------------------------------------------------------------------------
782
+
783
+ describe('ZQueryCollection — css()', () => {
784
+ beforeEach(() => {
785
+ document.body.innerHTML = '<div id="styled">test</div>';
786
+ });
787
+
788
+ it('sets style properties from object', () => {
789
+ queryAll('#styled').css({ color: 'red', 'font-size': '16px' });
790
+ const el = document.getElementById('styled');
791
+ expect(el.style.color).toBe('red');
792
+ });
793
+
794
+ it('returns collection for chaining', () => {
795
+ const col = queryAll('#styled').css({ color: 'blue' });
796
+ expect(col).toBeInstanceOf(ZQueryCollection);
797
+ });
798
+ });
799
+
800
+
801
+ // ---------------------------------------------------------------------------
802
+ // val() method
803
+ // ---------------------------------------------------------------------------
804
+
805
+ describe('ZQueryCollection — val()', () => {
806
+ it('gets input value', () => {
807
+ document.body.innerHTML = '<input value="test">';
808
+ expect(queryAll('input').val()).toBe('test');
809
+ });
810
+
811
+ it('sets input value', () => {
812
+ document.body.innerHTML = '<input value="">';
813
+ queryAll('input').val('new value');
814
+ expect(document.querySelector('input').value).toBe('new value');
815
+ });
816
+
817
+ it('gets select value', () => {
818
+ document.body.innerHTML = '<select><option value="a" selected>A</option><option value="b">B</option></select>';
819
+ expect(queryAll('select').val()).toBe('a');
820
+ });
821
+
822
+ it('gets textarea value', () => {
823
+ document.body.innerHTML = '<textarea>hello</textarea>';
824
+ expect(queryAll('textarea').val()).toBe('hello');
825
+ });
826
+ });
827
+
828
+
829
+ // ---------------------------------------------------------------------------
830
+ // after(), before() methods
831
+ // ---------------------------------------------------------------------------
832
+
833
+ describe('ZQueryCollection — after() / before()', () => {
834
+ beforeEach(() => {
835
+ document.body.innerHTML = '<div id="container"><p id="target">target</p></div>';
836
+ });
837
+
838
+ it('after() inserts content after element', () => {
839
+ queryAll('#target').after('<span class="after">after</span>');
840
+ const next = document.getElementById('target').nextElementSibling;
841
+ expect(next.className).toBe('after');
842
+ });
843
+
844
+ it('before() inserts content before element', () => {
845
+ queryAll('#target').before('<span class="before">before</span>');
846
+ const prev = document.getElementById('target').previousElementSibling;
847
+ expect(prev.className).toBe('before');
848
+ });
849
+ });
850
+
851
+
852
+ // ---------------------------------------------------------------------------
853
+ // wrap() method
854
+ // ---------------------------------------------------------------------------
855
+
856
+ describe('ZQueryCollection — wrap()', () => {
857
+ it('wraps element in new parent', () => {
858
+ document.body.innerHTML = '<div id="container"><p id="target">text</p></div>';
859
+ queryAll('#target').wrap('<div class="wrapper"></div>');
860
+ const wrapper = document.querySelector('.wrapper');
861
+ expect(wrapper).not.toBeNull();
862
+ expect(wrapper.querySelector('#target')).not.toBeNull();
863
+ });
864
+ });
865
+
866
+
867
+ // ---------------------------------------------------------------------------
868
+ // replaceWith() method
869
+ // ---------------------------------------------------------------------------
870
+
871
+ describe('ZQueryCollection — replaceWith()', () => {
872
+ it('replaces element with new content', () => {
873
+ document.body.innerHTML = '<div id="container"><p id="old">old</p></div>';
874
+ queryAll('#old').replaceWith('<span id="new">new</span>');
875
+ expect(document.querySelector('#old')).toBeNull();
876
+ expect(document.querySelector('#new')).not.toBeNull();
877
+ });
878
+
879
+ it('auto-morphs when tag name matches (preserves identity)', () => {
880
+ document.body.innerHTML = '<div id="container"><p id="target" class="old">old text</p></div>';
881
+ const target = document.querySelector('#target');
882
+ queryAll('#target').replaceWith('<p id="target" class="new">new text</p>');
883
+ // Same DOM node — morphed, not replaced
884
+ expect(document.querySelector('#target')).toBe(target);
885
+ expect(target.className).toBe('new');
886
+ expect(target.textContent).toBe('new text');
887
+ });
888
+
889
+ it('replaces when tag name differs', () => {
890
+ document.body.innerHTML = '<div id="container"><p id="old">old</p></div>';
891
+ const oldRef = document.querySelector('#old');
892
+ queryAll('#old').replaceWith('<section id="replaced">new</section>');
893
+ expect(document.querySelector('#replaced')).not.toBe(oldRef);
894
+ expect(document.querySelector('#replaced').tagName).toBe('SECTION');
895
+ });
896
+ });
897
+
898
+
899
+ // ---------------------------------------------------------------------------
900
+ // offset() and position()
901
+ // ---------------------------------------------------------------------------
902
+
903
+ describe('ZQueryCollection — offset() / position()', () => {
904
+ it('offset() returns object with top and left', () => {
905
+ document.body.innerHTML = '<div id="box">box</div>';
906
+ const off = queryAll('#box').offset();
907
+ expect(off).toHaveProperty('top');
908
+ expect(off).toHaveProperty('left');
909
+ expect(typeof off.top).toBe('number');
910
+ });
911
+
912
+ it('position() returns object with top and left', () => {
913
+ document.body.innerHTML = '<div id="box">box</div>';
914
+ const pos = queryAll('#box').position();
915
+ expect(pos).toHaveProperty('top');
916
+ expect(pos).toHaveProperty('left');
917
+ });
918
+ });
919
+
920
+
921
+ // ---------------------------------------------------------------------------
922
+ // width() and height()
923
+ // ---------------------------------------------------------------------------
924
+
925
+ describe('ZQueryCollection — width() / height()', () => {
926
+ it('width() returns a number', () => {
927
+ document.body.innerHTML = '<div id="box" style="width:100px">box</div>';
928
+ const val = queryAll('#box').width();
929
+ expect(typeof val).toBe('number');
930
+ });
931
+
932
+ it('height() returns a number', () => {
933
+ document.body.innerHTML = '<div id="box" style="height:50px">box</div>';
934
+ const val = queryAll('#box').height();
935
+ expect(typeof val).toBe('number');
936
+ });
937
+ });
938
+
939
+
940
+ // ---------------------------------------------------------------------------
941
+ // animate()
942
+ // ---------------------------------------------------------------------------
943
+
944
+ describe('ZQueryCollection — animate()', () => {
945
+ it('returns a promise', () => {
946
+ document.body.innerHTML = '<div id="box">box</div>';
947
+ const result = queryAll('#box').animate({ opacity: 0 }, 100);
948
+ // animate() returns a Promise
949
+ expect(result).toBeInstanceOf(Promise);
950
+ });
951
+ });
952
+
953
+
954
+ // ---------------------------------------------------------------------------
955
+ // hover() convenience
956
+ // ---------------------------------------------------------------------------
957
+
958
+ describe('hover()', () => {
959
+ it('binds mouseenter and mouseleave', () => {
960
+ let entered = false, left = false;
961
+ const col = queryAll('#main');
962
+ col.hover(() => { entered = true; }, () => { left = true; });
963
+ col.first().dispatchEvent(new Event('mouseenter'));
964
+ col.first().dispatchEvent(new Event('mouseleave'));
965
+ expect(entered).toBe(true);
966
+ expect(left).toBe(true);
967
+ });
968
+
969
+ it('uses same fn for both if only one provided', () => {
970
+ let count = 0;
971
+ const col = queryAll('#main');
972
+ col.hover(() => { count++; });
973
+ col.first().dispatchEvent(new Event('mouseenter'));
974
+ col.first().dispatchEvent(new Event('mouseleave'));
975
+ expect(count).toBe(2);
976
+ });
977
+ });