zero-query 0.9.0 → 0.9.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -9
- package/cli/commands/bundle.js +15 -2
- package/cli/commands/dev/devtools/js/elements.js +5 -0
- package/cli/scaffold/app/app.js +1 -1
- package/cli/scaffold/global.css +1 -2
- package/cli/scaffold/index.html +2 -1
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +544 -71
- package/dist/zquery.min.js +2 -2
- package/index.d.ts +51 -3
- package/index.js +32 -4
- package/package.json +1 -1
- package/src/component.js +60 -10
- package/src/core.js +65 -13
- package/src/diff.js +11 -5
- package/src/expression.js +104 -16
- package/src/http.js +23 -3
- package/src/reactive.js +8 -2
- package/src/router.js +43 -9
- package/src/ssr.js +1 -1
- package/src/store.js +5 -0
- package/src/utils.js +203 -11
- package/tests/audit.test.js +4030 -0
- package/tests/cli.test.js +456 -0
- package/tests/component.test.js +1387 -0
- package/tests/core.test.js +934 -1
- package/tests/diff.test.js +891 -0
- package/tests/errors.test.js +179 -0
- package/tests/expression.test.js +569 -0
- package/tests/http.test.js +160 -1
- package/tests/reactive.test.js +320 -0
- package/tests/router.test.js +1187 -0
- package/tests/ssr.test.js +261 -0
- package/tests/store.test.js +210 -0
- package/tests/utils.test.js +729 -1
- package/types/store.d.ts +3 -0
- package/types/utils.d.ts +103 -0
package/tests/core.test.js
CHANGED
|
@@ -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
|
|
|
@@ -546,6 +546,47 @@ describe('query quick refs', () => {
|
|
|
546
546
|
expect(el.id).toBe('created');
|
|
547
547
|
expect(el.textContent).toBe('text');
|
|
548
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
|
+
});
|
|
549
590
|
});
|
|
550
591
|
|
|
551
592
|
|
|
@@ -975,3 +1016,895 @@ describe('hover()', () => {
|
|
|
975
1016
|
expect(count).toBe(2);
|
|
976
1017
|
});
|
|
977
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
|
+
});
|