zero-query 0.8.9 → 0.9.1

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.
@@ -1,4 +1,4 @@
1
- import { describe, it, expect, beforeEach } from 'vitest';
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
2
  import { query, queryAll, ZQueryCollection } from '../src/core.js';
3
3
 
4
4
 
@@ -975,3 +975,895 @@ describe('hover()', () => {
975
975
  expect(count).toBe(2);
976
976
  });
977
977
  });
978
+
979
+
980
+ // ---------------------------------------------------------------------------
981
+ // ZQueryCollection — empty collection safety
982
+ // ---------------------------------------------------------------------------
983
+
984
+ describe('ZQueryCollection — empty collection operations', () => {
985
+ it('first() returns null on empty', () => {
986
+ expect(queryAll('.nonexistent').first()).toBeNull();
987
+ });
988
+
989
+ it('last() returns null on empty', () => {
990
+ expect(queryAll('.nonexistent').last()).toBeNull();
991
+ });
992
+
993
+ it('each() on empty collection does not call callback', () => {
994
+ const fn = vi.fn();
995
+ queryAll('.nonexistent').each(fn);
996
+ expect(fn).not.toHaveBeenCalled();
997
+ });
998
+
999
+ it('map() on empty returns empty array', () => {
1000
+ expect(queryAll('.nonexistent').map(el => el)).toEqual([]);
1001
+ });
1002
+
1003
+ it('html() get on empty returns undefined', () => {
1004
+ const result = queryAll('.nonexistent').html();
1005
+ expect(result).toBeUndefined();
1006
+ });
1007
+
1008
+ it('text() get on empty returns undefined', () => {
1009
+ const result = queryAll('.nonexistent').text();
1010
+ expect(result).toBeUndefined();
1011
+ });
1012
+
1013
+ it('val() get on empty returns undefined', () => {
1014
+ expect(queryAll('.nonexistent').val()).toBeUndefined();
1015
+ });
1016
+
1017
+ it('addClass on empty does not throw', () => {
1018
+ expect(() => queryAll('.nonexistent').addClass('test')).not.toThrow();
1019
+ });
1020
+
1021
+ it('attr() get on empty returns undefined', () => {
1022
+ expect(queryAll('.nonexistent').attr('id')).toBeUndefined();
1023
+ });
1024
+
1025
+ it('chaining on empty collection', () => {
1026
+ const col = queryAll('.nonexistent');
1027
+ const result = col.addClass('x').removeClass('x').toggleClass('y');
1028
+ expect(result).toBeInstanceOf(ZQueryCollection);
1029
+ expect(result.length).toBe(0);
1030
+ });
1031
+ });
1032
+
1033
+
1034
+ // ---------------------------------------------------------------------------
1035
+ // Collection wrapping edge cases
1036
+ // ---------------------------------------------------------------------------
1037
+
1038
+ describe('query — wrapping edge cases', () => {
1039
+ it('wraps an HTMLCollection', () => {
1040
+ const col = queryAll(document.getElementsByClassName('text'));
1041
+ expect(col).toBeInstanceOf(ZQueryCollection);
1042
+ expect(col.length).toBe(3);
1043
+ });
1044
+
1045
+ it('wraps a NodeList', () => {
1046
+ const col = queryAll(document.querySelectorAll('.text'));
1047
+ expect(col).toBeInstanceOf(ZQueryCollection);
1048
+ expect(col.length).toBe(3);
1049
+ });
1050
+
1051
+ it('wraps an Array of elements', () => {
1052
+ const arr = [document.getElementById('main'), document.getElementById('sidebar')];
1053
+ const col = queryAll(arr);
1054
+ expect(col.length).toBe(2);
1055
+ expect(col.first().id).toBe('main');
1056
+ });
1057
+
1058
+ it('query() wraps ZQueryCollection (returns as-is)', () => {
1059
+ const original = queryAll('.text');
1060
+ const wrapped = query(original);
1061
+ expect(wrapped).toBe(original);
1062
+ });
1063
+
1064
+ it('creates multiple elements from HTML', () => {
1065
+ const col = queryAll('<p>a</p><p>b</p><p>c</p>');
1066
+ expect(col.length).toBe(3);
1067
+ expect(col.first().textContent).toBe('a');
1068
+ expect(col.last().textContent).toBe('c');
1069
+ });
1070
+ });
1071
+
1072
+
1073
+ // ---------------------------------------------------------------------------
1074
+ // html() morphing advanced
1075
+ // ---------------------------------------------------------------------------
1076
+
1077
+ describe('ZQueryCollection — html() morphing advanced', () => {
1078
+ it('morphs complex nested structure', () => {
1079
+ document.body.innerHTML = '<div id="m"><ul><li id="i1">old1</li><li id="i2">old2</li></ul></div>';
1080
+ const li1 = document.getElementById('i1');
1081
+ queryAll('#m').html('<ul><li id="i1">new1</li><li id="i2">new2</li><li id="i3">new3</li></ul>');
1082
+ // li1 should be preserved (same id → morph)
1083
+ expect(document.getElementById('i1')).toBe(li1);
1084
+ expect(li1.textContent).toBe('new1');
1085
+ expect(document.querySelectorAll('#m li').length).toBe(3);
1086
+ });
1087
+
1088
+ it('morph() handles tag change at root', () => {
1089
+ document.body.innerHTML = '<div id="m"><p>old</p></div>';
1090
+ queryAll('#m').morph('<span>new</span>');
1091
+ expect(document.querySelector('#m span')).not.toBeNull();
1092
+ expect(document.querySelector('#m p')).toBeNull();
1093
+ });
1094
+ });
1095
+
1096
+
1097
+ // ---------------------------------------------------------------------------
1098
+ // Event delegation
1099
+ // ---------------------------------------------------------------------------
1100
+
1101
+ describe('ZQueryCollection — event delegation', () => {
1102
+ it('on() with selector delegates to matching children', () => {
1103
+ let clicked = null;
1104
+ queryAll('#nav').on('click', '.nav-item', function() { clicked = this.textContent; });
1105
+ document.querySelector('.nav-item.active').click();
1106
+ expect(clicked).toBe('Home');
1107
+ });
1108
+
1109
+ it('delegated event does not fire for non-matching elements', () => {
1110
+ let fired = false;
1111
+ queryAll('#main').on('click', '.nonexistent', () => { fired = true; });
1112
+ document.querySelector('.text').click();
1113
+ expect(fired).toBe(false);
1114
+ });
1115
+ });
1116
+
1117
+
1118
+ // ---------------------------------------------------------------------------
1119
+ // Multiple class operations
1120
+ // ---------------------------------------------------------------------------
1121
+
1122
+ describe('ZQueryCollection — multiple class operations', () => {
1123
+ it('addClass with space-separated classes', () => {
1124
+ const col = queryAll('#main');
1125
+ col.addClass('a', 'b', 'c');
1126
+ expect(col.first().classList.contains('a')).toBe(true);
1127
+ expect(col.first().classList.contains('b')).toBe(true);
1128
+ expect(col.first().classList.contains('c')).toBe(true);
1129
+ });
1130
+
1131
+ it('removeClass with multiple classes', () => {
1132
+ const col = queryAll('#main');
1133
+ col.addClass('x', 'y', 'z');
1134
+ col.removeClass('x', 'z');
1135
+ expect(col.first().classList.contains('x')).toBe(false);
1136
+ expect(col.first().classList.contains('y')).toBe(true);
1137
+ expect(col.first().classList.contains('z')).toBe(false);
1138
+ });
1139
+ });
1140
+
1141
+
1142
+ // ---------------------------------------------------------------------------
1143
+ // Traversal edge cases
1144
+ // ---------------------------------------------------------------------------
1145
+
1146
+ describe('ZQueryCollection — traversal edge cases', () => {
1147
+ it('find() returns empty when no descendants match', () => {
1148
+ expect(queryAll('#main').find('.nonexistent').length).toBe(0);
1149
+ });
1150
+
1151
+ it('parent() on body returns html', () => {
1152
+ const parents = queryAll('body').parent();
1153
+ expect(parents.first().tagName).toBe('HTML');
1154
+ });
1155
+
1156
+ it('children() with selector filters', () => {
1157
+ const col = queryAll('#main').children('.text');
1158
+ expect(col.length).toBe(3);
1159
+ });
1160
+
1161
+ it('closest() returns self if it matches', () => {
1162
+ const col = queryAll('#main');
1163
+ expect(col.closest('#main').first()).toBe(document.getElementById('main'));
1164
+ });
1165
+
1166
+ it('closest() returns empty when no match', () => {
1167
+ expect(queryAll('.text').closest('.nonexistent').length).toBe(0);
1168
+ });
1169
+
1170
+ it('siblings() returns all siblings', () => {
1171
+ const sibs = queryAll('.first-p').siblings();
1172
+ // siblings() returns all sibling elements except self
1173
+ expect(sibs.length).toBeGreaterThanOrEqual(2);
1174
+ });
1175
+
1176
+ it('next() at end returns empty', () => {
1177
+ const col = queryAll('.third-p');
1178
+ expect(col.next().length).toBe(0);
1179
+ });
1180
+
1181
+ it('prev() at start returns empty', () => {
1182
+ const col = queryAll('.first-p');
1183
+ expect(col.prev().length).toBe(0);
1184
+ });
1185
+ });
1186
+
1187
+
1188
+ // ---------------------------------------------------------------------------
1189
+ // DOM manipulation edge cases
1190
+ // ---------------------------------------------------------------------------
1191
+
1192
+ describe('ZQueryCollection — DOM manipulation edge cases', () => {
1193
+ it('append with element node', () => {
1194
+ const newEl = document.createElement('div');
1195
+ newEl.id = 'appended-el';
1196
+ queryAll('#main').append(newEl);
1197
+ expect(document.getElementById('appended-el')).not.toBeNull();
1198
+ expect(document.getElementById('appended-el').parentElement.id).toBe('main');
1199
+ });
1200
+
1201
+ it('prepend with element node', () => {
1202
+ const newEl = document.createElement('div');
1203
+ newEl.id = 'prepended-el';
1204
+ queryAll('#main').prepend(newEl);
1205
+ expect(document.getElementById('main').firstElementChild.id).toBe('prepended-el');
1206
+ });
1207
+
1208
+ it('remove on already-removed element does not throw', () => {
1209
+ const col = queryAll('.text').eq(0);
1210
+ col.remove();
1211
+ expect(() => col.remove()).not.toThrow();
1212
+ });
1213
+
1214
+ it('clone produces independent copy', () => {
1215
+ const original = queryAll('.first-p');
1216
+ const cloned = original.clone();
1217
+ cloned.addClass('cloned-class');
1218
+ expect(original.hasClass('cloned-class')).toBe(false);
1219
+ expect(cloned.hasClass('cloned-class')).toBe(true);
1220
+ });
1221
+
1222
+ it('empty() on already empty element', () => {
1223
+ document.body.innerHTML = '<div id="empty"></div>';
1224
+ expect(() => queryAll('#empty').empty()).not.toThrow();
1225
+ expect(document.getElementById('empty').children.length).toBe(0);
1226
+ });
1227
+ });
1228
+
1229
+
1230
+ // ---------------------------------------------------------------------------
1231
+ // Attribute edge cases
1232
+ // ---------------------------------------------------------------------------
1233
+
1234
+ describe('ZQueryCollection — attribute edge cases', () => {
1235
+ it('attr() set with sequential calls sets multiple attributes', () => {
1236
+ document.body.innerHTML = '<div id="a"></div>';
1237
+ queryAll('#a').attr('data-x', '1').attr('data-y', '2').attr('title', 'test');
1238
+ const el = document.getElementById('a');
1239
+ expect(el.getAttribute('data-x')).toBe('1');
1240
+ expect(el.getAttribute('data-y')).toBe('2');
1241
+ expect(el.getAttribute('title')).toBe('test');
1242
+ });
1243
+
1244
+ it('data() returns undefined for missing key', () => {
1245
+ expect(queryAll('#main').data('nonexistent')).toBeUndefined();
1246
+ });
1247
+
1248
+ it('removeAttr on nonexistent attribute does not throw', () => {
1249
+ expect(() => queryAll('#main').removeAttr('data-nope')).not.toThrow();
1250
+ });
1251
+ });
1252
+
1253
+
1254
+ // ---------------------------------------------------------------------------
1255
+ // css() advanced
1256
+ // ---------------------------------------------------------------------------
1257
+
1258
+ describe('ZQueryCollection — css() advanced', () => {
1259
+ it('sets a single style property via object', () => {
1260
+ document.body.innerHTML = '<div id="s">test</div>';
1261
+ queryAll('#s').css({ color: 'green' });
1262
+ expect(document.getElementById('s').style.color).toBe('green');
1263
+ });
1264
+
1265
+ it('sets multiple CSS properties', () => {
1266
+ document.body.innerHTML = '<div id="s2">test</div>';
1267
+ queryAll('#s2').css({ color: 'red', 'font-weight': 'bold', display: 'flex' });
1268
+ const el = document.getElementById('s2');
1269
+ expect(el.style.color).toBe('red');
1270
+ expect(el.style.display).toBe('flex');
1271
+ });
1272
+ });
1273
+
1274
+
1275
+ // ---------------------------------------------------------------------------
1276
+ // $.create advanced
1277
+ // ---------------------------------------------------------------------------
1278
+
1279
+ describe('query.create — advanced', () => {
1280
+ it('creates element with no attributes', () => {
1281
+ const col = query.create('span');
1282
+ expect(col.length).toBe(1);
1283
+ expect(col[0].tagName).toBe('SPAN');
1284
+ });
1285
+
1286
+ it('creates element with multiple children', () => {
1287
+ const child1 = document.createElement('span');
1288
+ child1.textContent = 'span child';
1289
+ const col = query.create('div', {}, 'text', child1);
1290
+ expect(col[0].childNodes.length).toBe(2);
1291
+ expect(col[0].childNodes[0].textContent).toBe('text');
1292
+ expect(col[0].querySelector('span').textContent).toBe('span child');
1293
+ });
1294
+
1295
+ it('creates element with boolean attributes', () => {
1296
+ const col = query.create('input', { type: 'text', disabled: '' });
1297
+ expect(col[0].tagName).toBe('INPUT');
1298
+ expect(col[0].getAttribute('type')).toBe('text');
1299
+ });
1300
+ });
1301
+
1302
+
1303
+ // ---------------------------------------------------------------------------
1304
+ // Prop edge cases
1305
+ // ---------------------------------------------------------------------------
1306
+
1307
+ describe('ZQueryCollection — prop() edge cases', () => {
1308
+ it('gets defaultValue property', () => {
1309
+ document.body.innerHTML = '<input value="initial">';
1310
+ const col = queryAll('input');
1311
+ expect(col.prop('defaultValue')).toBe('initial');
1312
+ });
1313
+
1314
+ it('gets tagName property', () => {
1315
+ const col = queryAll('#main');
1316
+ expect(col.prop('tagName')).toBe('DIV');
1317
+ });
1318
+
1319
+ it('prop on empty collection returns undefined', () => {
1320
+ expect(queryAll('.nonexistent').prop('checked')).toBeUndefined();
1321
+ });
1322
+ });
1323
+
1324
+
1325
+ // ---------------------------------------------------------------------------
1326
+ // BUG FIX: siblings() with selector filtering + null parent guard
1327
+ // ---------------------------------------------------------------------------
1328
+
1329
+ describe('ZQueryCollection — siblings() fixes', () => {
1330
+ it('filters siblings by selector', () => {
1331
+ document.body.innerHTML = '<div><p class="a">1</p><p class="b">2</p><p class="a">3</p></div>';
1332
+ const sibs = queryAll('.b').siblings('.a');
1333
+ expect(sibs.length).toBe(2);
1334
+ });
1335
+
1336
+ it('returns all siblings when no selector given', () => {
1337
+ document.body.innerHTML = '<div><p>1</p><p id="mid">2</p><p>3</p></div>';
1338
+ const sibs = queryAll('#mid').siblings();
1339
+ expect(sibs.length).toBe(2);
1340
+ });
1341
+
1342
+ it('does not crash on detached element (no parentElement)', () => {
1343
+ const detached = document.createElement('div');
1344
+ const col = new ZQueryCollection([detached]);
1345
+ expect(() => col.siblings()).not.toThrow();
1346
+ expect(col.siblings().length).toBe(0);
1347
+ });
1348
+ });
1349
+
1350
+
1351
+ // ---------------------------------------------------------------------------
1352
+ // BUG FIX: ZQueryCollection constructor null safety
1353
+ // ---------------------------------------------------------------------------
1354
+
1355
+ describe('ZQueryCollection — constructor null/undefined safety', () => {
1356
+ it('creates empty collection from null', () => {
1357
+ const col = new ZQueryCollection(null);
1358
+ expect(col.length).toBe(0);
1359
+ });
1360
+
1361
+ it('creates empty collection from undefined', () => {
1362
+ const col = new ZQueryCollection(undefined);
1363
+ expect(col.length).toBe(0);
1364
+ });
1365
+
1366
+ it('wraps a single element', () => {
1367
+ const el = document.createElement('div');
1368
+ const col = new ZQueryCollection(el);
1369
+ expect(col.length).toBe(1);
1370
+ expect(col[0]).toBe(el);
1371
+ });
1372
+ });
1373
+
1374
+
1375
+ // ---------------------------------------------------------------------------
1376
+ // BUG FIX: attr() with object syntax
1377
+ // ---------------------------------------------------------------------------
1378
+
1379
+ describe('ZQueryCollection — attr() object set', () => {
1380
+ it('sets multiple attributes with object', () => {
1381
+ document.body.innerHTML = '<div id="at"></div>';
1382
+ queryAll('#at').attr({ 'data-x': '1', 'data-y': '2', title: 'hello' });
1383
+ const el = document.getElementById('at');
1384
+ expect(el.getAttribute('data-x')).toBe('1');
1385
+ expect(el.getAttribute('data-y')).toBe('2');
1386
+ expect(el.getAttribute('title')).toBe('hello');
1387
+ });
1388
+ });
1389
+
1390
+
1391
+ // ---------------------------------------------------------------------------
1392
+ // BUG FIX: css() two-argument setter
1393
+ // ---------------------------------------------------------------------------
1394
+
1395
+ describe('ZQueryCollection — css() two-argument setter', () => {
1396
+ it('sets a CSS property with key-value arguments', () => {
1397
+ document.body.innerHTML = '<div id="cs">text</div>';
1398
+ queryAll('#cs').css('color', 'green');
1399
+ expect(document.getElementById('cs').style.color).toBe('green');
1400
+ });
1401
+
1402
+ it('still works as getter with single string arg', () => {
1403
+ document.body.innerHTML = '<div id="cs2" style="color: red;">text</div>';
1404
+ const val = queryAll('#cs2').css('color');
1405
+ expect(val).toBeDefined();
1406
+ });
1407
+
1408
+ it('still works as setter with object arg', () => {
1409
+ document.body.innerHTML = '<div id="cs3">text</div>';
1410
+ queryAll('#cs3').css({ color: 'blue', display: 'flex' });
1411
+ const el = document.getElementById('cs3');
1412
+ expect(el.style.color).toBe('blue');
1413
+ expect(el.style.display).toBe('flex');
1414
+ });
1415
+ });
1416
+
1417
+
1418
+ // ---------------------------------------------------------------------------
1419
+ // BUG FIX: wrap() does not crash on empty/invalid wrapper
1420
+ // ---------------------------------------------------------------------------
1421
+
1422
+ describe('ZQueryCollection — wrap() safety', () => {
1423
+ it('does not crash if wrapper string is empty', () => {
1424
+ document.body.innerHTML = '<div id="w"><p>inside</p></div>';
1425
+ expect(() => queryAll('#w p').wrap('')).not.toThrow();
1426
+ });
1427
+
1428
+ it('does not crash on detached element (no parentNode)', () => {
1429
+ const detached = document.createElement('span');
1430
+ const col = new ZQueryCollection([detached]);
1431
+ expect(() => col.wrap('<div></div>')).not.toThrow();
1432
+ });
1433
+ });
1434
+
1435
+
1436
+ // ---------------------------------------------------------------------------
1437
+ // BUG FIX: index() does not crash on detached element
1438
+ // ---------------------------------------------------------------------------
1439
+
1440
+ describe('ZQueryCollection — index() null parent safety', () => {
1441
+ it('returns -1 for detached element', () => {
1442
+ const detached = document.createElement('div');
1443
+ const col = new ZQueryCollection([detached]);
1444
+ expect(col.index()).toBe(-1);
1445
+ });
1446
+ });
1447
+
1448
+
1449
+ // ---------------------------------------------------------------------------
1450
+ // BUG FIX: delegated on() / off() handler removal
1451
+ // ---------------------------------------------------------------------------
1452
+
1453
+ describe('ZQueryCollection — delegated on/off', () => {
1454
+ it('off() removes delegated event handlers', () => {
1455
+ document.body.innerHTML = '<div id="parent"><button class="btn">click</button></div>';
1456
+ const parent = new ZQueryCollection([document.getElementById('parent')]);
1457
+ const handler = vi.fn();
1458
+
1459
+ parent.on('click', '.btn', handler);
1460
+ document.querySelector('.btn').click();
1461
+ expect(handler).toHaveBeenCalledTimes(1);
1462
+
1463
+ parent.off('click', handler);
1464
+ document.querySelector('.btn').click();
1465
+ // Should not fire again after off()
1466
+ expect(handler).toHaveBeenCalledTimes(1);
1467
+ });
1468
+ });
1469
+
1470
+
1471
+ // ---------------------------------------------------------------------------
1472
+ // BUG FIX: animate() resolves immediately on empty collection
1473
+ // ---------------------------------------------------------------------------
1474
+
1475
+ describe('ZQueryCollection — animate() empty collection', () => {
1476
+ it('resolves immediately when collection is empty', async () => {
1477
+ const col = new ZQueryCollection([]);
1478
+ const result = await col.animate({ opacity: '0' }, 50);
1479
+ expect(result).toBe(col);
1480
+ });
1481
+ });
1482
+
1483
+
1484
+ // ===========================================================================
1485
+ // one() — single-fire event listener
1486
+ // ===========================================================================
1487
+
1488
+ describe('ZQueryCollection — one()', () => {
1489
+ it('fires handler only once', () => {
1490
+ const handler = vi.fn();
1491
+ document.body.innerHTML = '<button id="one-btn">click</button>';
1492
+ const col = query('#one-btn');
1493
+ col.one('click', handler);
1494
+ document.querySelector('#one-btn').click();
1495
+ document.querySelector('#one-btn').click();
1496
+ expect(handler).toHaveBeenCalledTimes(1);
1497
+ });
1498
+ });
1499
+
1500
+
1501
+ // ===========================================================================
1502
+ // toggle() — show/hide toggle
1503
+ // ===========================================================================
1504
+
1505
+ describe('ZQueryCollection — toggle()', () => {
1506
+ it('hides a visible element', () => {
1507
+ const el = document.querySelector('#main');
1508
+ el.style.display = '';
1509
+ const col = query('#main');
1510
+ col.toggle();
1511
+ expect(el.style.display).toBe('none');
1512
+ });
1513
+
1514
+ it('shows a hidden element', () => {
1515
+ const el = document.querySelector('#main');
1516
+ el.style.display = 'none';
1517
+ const col = query('#main');
1518
+ col.toggle();
1519
+ expect(el.style.display).toBe('');
1520
+ });
1521
+
1522
+ it('uses custom display value when showing', () => {
1523
+ const el = document.querySelector('#main');
1524
+ el.style.display = 'none';
1525
+ const col = query('#main');
1526
+ col.toggle('flex');
1527
+ expect(el.style.display).toBe('flex');
1528
+ });
1529
+ });
1530
+
1531
+
1532
+ // ===========================================================================
1533
+ // serialize() and serializeObject()
1534
+ // ===========================================================================
1535
+
1536
+ describe('ZQueryCollection — serialize()', () => {
1537
+ it('serializes form inputs to URL-encoded string', () => {
1538
+ document.body.innerHTML = '<form id="f"><input name="user" value="Alice"><input name="age" value="30"></form>';
1539
+ const result = query('#f').serialize();
1540
+ expect(result).toContain('user=Alice');
1541
+ expect(result).toContain('age=30');
1542
+ });
1543
+
1544
+ it('returns empty string for non-form element', () => {
1545
+ expect(query('#main').serialize()).toBe('');
1546
+ });
1547
+ });
1548
+
1549
+ describe('ZQueryCollection — serializeObject()', () => {
1550
+ it('builds an object from form fields', () => {
1551
+ document.body.innerHTML = '<form id="f"><input name="user" value="Alice"><input name="age" value="30"></form>';
1552
+ expect(query('#f').serializeObject()).toEqual({ user: 'Alice', age: '30' });
1553
+ });
1554
+
1555
+ it('groups duplicate keys into arrays', () => {
1556
+ document.body.innerHTML = `<form id="f">
1557
+ <input name="tags" value="a">
1558
+ <input name="tags" value="b">
1559
+ <input name="tags" value="c">
1560
+ </form>`;
1561
+ expect(query('#f').serializeObject()).toEqual({ tags: ['a', 'b', 'c'] });
1562
+ });
1563
+
1564
+ it('returns empty object for non-form element', () => {
1565
+ expect(query('#main').serializeObject()).toEqual({});
1566
+ });
1567
+ });
1568
+
1569
+
1570
+ // ===========================================================================
1571
+ // $.ready
1572
+ // ===========================================================================
1573
+
1574
+ describe('$.ready', () => {
1575
+ it('calls function immediately when document is not loading', () => {
1576
+ const fn = vi.fn();
1577
+ query.ready(fn);
1578
+ expect(fn).toHaveBeenCalledTimes(1);
1579
+ });
1580
+ });
1581
+
1582
+
1583
+ // ===========================================================================
1584
+ // $.name
1585
+ // ===========================================================================
1586
+
1587
+ describe('$.name', () => {
1588
+ it('selects elements by name attribute', () => {
1589
+ document.body.innerHTML = '<input name="email" value="a@b.com"><input name="email" value="x@y.com"><input name="other">';
1590
+ const result = query.name('email');
1591
+ expect(result.length).toBe(2);
1592
+ });
1593
+ });
1594
+
1595
+
1596
+ // ===========================================================================
1597
+ // $.create
1598
+ // ===========================================================================
1599
+
1600
+ describe('$.create', () => {
1601
+ it('creates an element with attributes', () => {
1602
+ const col = query.create('div', { id: 'test', class: 'box' }, 'hello');
1603
+ const el = col.first();
1604
+ expect(el.tagName).toBe('DIV');
1605
+ expect(el.id).toBe('test');
1606
+ expect(el.className).toBe('box');
1607
+ expect(el.textContent).toBe('hello');
1608
+ });
1609
+
1610
+ it('applies style object', () => {
1611
+ const col = query.create('span', { style: { color: 'red', fontSize: '20px' } });
1612
+ const el = col.first();
1613
+ expect(el.style.color).toBe('red');
1614
+ expect(el.style.fontSize).toBe('20px');
1615
+ });
1616
+
1617
+ it('binds event handlers via on* attributes', () => {
1618
+ const handler = vi.fn();
1619
+ const col = query.create('button', { onclick: handler }, 'click me');
1620
+ col.first().click();
1621
+ expect(handler).toHaveBeenCalledTimes(1);
1622
+ });
1623
+
1624
+ it('sets data attributes from data object', () => {
1625
+ const col = query.create('div', { data: { userId: '42', role: 'admin' } });
1626
+ const el = col.first();
1627
+ expect(el.dataset.userId).toBe('42');
1628
+ expect(el.dataset.role).toBe('admin');
1629
+ });
1630
+
1631
+ it('appends child Node elements', () => {
1632
+ const child = document.createElement('span');
1633
+ child.textContent = 'child';
1634
+ const col = query.create('div', {}, child);
1635
+ const el = col.first();
1636
+ expect(el.children.length).toBe(1);
1637
+ expect(el.querySelector('span').textContent).toBe('child');
1638
+ });
1639
+ });
1640
+
1641
+
1642
+ // ===========================================================================
1643
+ // data() — no key returns full dataset
1644
+ // ===========================================================================
1645
+
1646
+ describe('ZQueryCollection — data() full dataset', () => {
1647
+ it('returns the full dataset when no key is given', () => {
1648
+ document.body.innerHTML = '<div id="d" data-x="1" data-y="2"></div>';
1649
+ const ds = query('#d').data();
1650
+ expect(ds.x).toBe('1');
1651
+ expect(ds.y).toBe('2');
1652
+ });
1653
+ });
1654
+
1655
+
1656
+ // ===========================================================================
1657
+ // css() getter on empty collection
1658
+ // ===========================================================================
1659
+
1660
+ describe('ZQueryCollection — css() empty collection', () => {
1661
+ it('returns undefined when collection is empty', () => {
1662
+ const col = new ZQueryCollection([]);
1663
+ expect(col.css('color')).toBeUndefined();
1664
+ });
1665
+ });
1666
+
1667
+
1668
+ // ===========================================================================
1669
+ // append/prepend/after/before with Node
1670
+ // ===========================================================================
1671
+
1672
+ describe('ZQueryCollection — append/prepend with Node', () => {
1673
+ it('appends a Node element', () => {
1674
+ document.body.innerHTML = '<div id="container"><p>existing</p></div>';
1675
+ const newNode = document.createElement('span');
1676
+ newNode.textContent = 'appended';
1677
+ query('#container').append(newNode);
1678
+ expect(document.querySelector('#container span').textContent).toBe('appended');
1679
+ expect(document.querySelector('#container').lastElementChild.tagName).toBe('SPAN');
1680
+ });
1681
+
1682
+ it('prepends a Node element', () => {
1683
+ document.body.innerHTML = '<div id="container"><p>existing</p></div>';
1684
+ const newNode = document.createElement('span');
1685
+ newNode.textContent = 'prepended';
1686
+ query('#container').prepend(newNode);
1687
+ expect(document.querySelector('#container').firstElementChild.tagName).toBe('SPAN');
1688
+ });
1689
+
1690
+ it('appends a ZQueryCollection', () => {
1691
+ document.body.innerHTML = '<div id="container"></div><span class="source">item</span>';
1692
+ const source = queryAll('.source');
1693
+ query('#container').append(source);
1694
+ expect(document.querySelector('#container span').textContent).toBe('item');
1695
+ });
1696
+ });
1697
+
1698
+ describe('ZQueryCollection — after/before with Node', () => {
1699
+ it('inserts Node after element', () => {
1700
+ document.body.innerHTML = '<div id="anchor"></div>';
1701
+ const newNode = document.createElement('span');
1702
+ newNode.id = 'after';
1703
+ query('#anchor').after(newNode);
1704
+ expect(document.querySelector('#anchor').nextElementSibling.id).toBe('after');
1705
+ });
1706
+
1707
+ it('inserts Node before element', () => {
1708
+ document.body.innerHTML = '<div id="anchor"></div>';
1709
+ const newNode = document.createElement('span');
1710
+ newNode.id = 'before';
1711
+ query('#anchor').before(newNode);
1712
+ expect(document.querySelector('#anchor').previousElementSibling.id).toBe('before');
1713
+ });
1714
+ });
1715
+
1716
+
1717
+ // ===========================================================================
1718
+ // replaceWith using Node
1719
+ // ===========================================================================
1720
+
1721
+ describe('ZQueryCollection — replaceWith(Node)', () => {
1722
+ it('replaces element with a Node', () => {
1723
+ document.body.innerHTML = '<div id="old">old</div>';
1724
+ const newNode = document.createElement('span');
1725
+ newNode.id = 'new';
1726
+ newNode.textContent = 'replaced';
1727
+ query('#old').replaceWith(newNode);
1728
+ expect(document.querySelector('#old')).toBeNull();
1729
+ expect(document.querySelector('#new').textContent).toBe('replaced');
1730
+ });
1731
+ });
1732
+
1733
+
1734
+ // ===========================================================================
1735
+ // nextUntil/prevUntil/parentsUntil with filter
1736
+ // ===========================================================================
1737
+
1738
+ describe('ZQueryCollection — nextUntil with filter', () => {
1739
+ it('collects siblings until stop selector, applying filter', () => {
1740
+ document.body.innerHTML = '<div id="start"></div><span class="a">A</span><p>P</p><span class="a">A2</span><div id="stop"></div>';
1741
+ const result = query('#start').nextUntil('#stop', 'span');
1742
+ expect(result.length).toBe(2); // only <span> siblings
1743
+ });
1744
+ });
1745
+
1746
+ describe('ZQueryCollection — prevUntil with filter', () => {
1747
+ it('collects previous siblings until stop selector, applying filter', () => {
1748
+ document.body.innerHTML = '<div id="stop"></div><span>A</span><p>P</p><span>B</span><div id="end"></div>';
1749
+ const result = query('#end').prevUntil('#stop', 'span');
1750
+ expect(result.length).toBe(2);
1751
+ });
1752
+ });
1753
+
1754
+ describe('ZQueryCollection — parentsUntil with filter', () => {
1755
+ it('collects parent elements until stop selector, applying filter', () => {
1756
+ document.body.innerHTML = '<section><article><div><span id="target"></span></div></article></section>';
1757
+ const result = query('#target').parentsUntil('section', 'div');
1758
+ expect(result.length).toBe(1);
1759
+ expect(result.first().tagName).toBe('DIV');
1760
+ });
1761
+ });
1762
+
1763
+
1764
+ // ===========================================================================
1765
+ // delegated on() at document level
1766
+ // ===========================================================================
1767
+
1768
+ describe('ZQueryCollection — delegated on()', () => {
1769
+ it('delegates event to matching child selector', () => {
1770
+ document.body.innerHTML = '<div id="container"><button class="action">click</button></div>';
1771
+ const handler = vi.fn();
1772
+ query('#container').on('click', '.action', handler);
1773
+ document.querySelector('.action').click();
1774
+ expect(handler).toHaveBeenCalledTimes(1);
1775
+ });
1776
+
1777
+ it('does not fire for non-matching elements', () => {
1778
+ document.body.innerHTML = '<div id="container"><span class="other">x</span></div>';
1779
+ const handler = vi.fn();
1780
+ query('#container').on('click', '.action', handler);
1781
+ document.querySelector('.other').click();
1782
+ expect(handler).not.toHaveBeenCalled();
1783
+ });
1784
+ });
1785
+
1786
+
1787
+ // ===========================================================================
1788
+ // Multi-event on/off
1789
+ // ===========================================================================
1790
+
1791
+ describe('ZQueryCollection — multi-event on()', () => {
1792
+ it('binds handler to multiple space-separated events', () => {
1793
+ document.body.innerHTML = '<input id="inp" type="text">';
1794
+ const handler = vi.fn();
1795
+ query('#inp').on('focus blur', handler);
1796
+ document.querySelector('#inp').dispatchEvent(new Event('focus'));
1797
+ document.querySelector('#inp').dispatchEvent(new Event('blur'));
1798
+ expect(handler).toHaveBeenCalledTimes(2);
1799
+ });
1800
+ });
1801
+
1802
+
1803
+ // ===========================================================================
1804
+ // scrollTop/scrollLeft getters
1805
+ // ===========================================================================
1806
+
1807
+ describe('ZQueryCollection — scrollTop/scrollLeft', () => {
1808
+ it('gets and sets scrollTop', () => {
1809
+ document.body.innerHTML = '<div id="scr" style="overflow:auto; height: 50px;"><div style="height:200px;">x</div></div>';
1810
+ const el = document.querySelector('#scr');
1811
+ query('#scr').scrollTop(100);
1812
+ expect(el.scrollTop).toBe(100);
1813
+ });
1814
+
1815
+ it('gets scrollTop value', () => {
1816
+ document.body.innerHTML = '<div id="scr" style="overflow:auto; height: 50px;"><div style="height:200px;">x</div></div>';
1817
+ document.querySelector('#scr').scrollTop = 50;
1818
+ expect(query('#scr').scrollTop()).toBe(50);
1819
+ });
1820
+ });
1821
+
1822
+
1823
+ // ===========================================================================
1824
+ // slideDown/slideUp set styles
1825
+ // ===========================================================================
1826
+
1827
+ describe('ZQueryCollection — slideDown/slideUp', () => {
1828
+ it('slideDown sets overflow hidden and maxHeight initially', () => {
1829
+ vi.useFakeTimers();
1830
+ document.body.innerHTML = '<div id="slide" style="display:none;">content</div>';
1831
+ query('#slide').slideDown(100);
1832
+ const el = document.querySelector('#slide');
1833
+ expect(el.style.overflow).toBe('hidden');
1834
+ // maxHeight could be '0' or '0px' depending on jsdom normalization
1835
+ expect(el.style.maxHeight).toMatch(/^0(px)?$/);
1836
+ vi.advanceTimersByTime(100);
1837
+ vi.useRealTimers();
1838
+ });
1839
+
1840
+ it('slideUp hides element after duration', () => {
1841
+ vi.useFakeTimers();
1842
+ document.body.innerHTML = '<div id="slide">content</div>';
1843
+ query('#slide').slideUp(100);
1844
+ vi.advanceTimersByTime(100);
1845
+ expect(document.querySelector('#slide').style.display).toBe('none');
1846
+ vi.useRealTimers();
1847
+ });
1848
+ });
1849
+
1850
+
1851
+ // ===========================================================================
1852
+ // fadeIn/fadeOut set opacity
1853
+ // ===========================================================================
1854
+
1855
+ describe('ZQueryCollection — fadeIn/fadeOut', () => {
1856
+ it('fadeIn sets initial opacity to 0', () => {
1857
+ document.body.innerHTML = '<div id="fade" style="display:none;">content</div>';
1858
+ query('#fade').fadeIn(100);
1859
+ const el = document.querySelector('#fade');
1860
+ expect(el.style.opacity).toBe('0');
1861
+ });
1862
+
1863
+ it('fadeTo animates to specified opacity', () => {
1864
+ document.body.innerHTML = '<div id="fade">content</div>';
1865
+ query('#fade').fadeTo(100, 0.5);
1866
+ // Animation starts — just verify no throw
1867
+ expect(document.querySelector('#fade')).not.toBeNull();
1868
+ });
1869
+ });