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