zero-query 0.6.3 → 0.8.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -29
- package/cli/commands/build.js +113 -4
- package/cli/commands/bundle.js +392 -29
- package/cli/commands/create.js +1 -1
- package/cli/commands/dev/devtools/index.js +56 -0
- package/cli/commands/dev/devtools/js/components.js +49 -0
- package/cli/commands/dev/devtools/js/core.js +409 -0
- package/cli/commands/dev/devtools/js/elements.js +413 -0
- package/cli/commands/dev/devtools/js/network.js +166 -0
- package/cli/commands/dev/devtools/js/performance.js +73 -0
- package/cli/commands/dev/devtools/js/router.js +105 -0
- package/cli/commands/dev/devtools/js/source.js +132 -0
- package/cli/commands/dev/devtools/js/stats.js +35 -0
- package/cli/commands/dev/devtools/js/tabs.js +79 -0
- package/cli/commands/dev/devtools/panel.html +95 -0
- package/cli/commands/dev/devtools/styles.css +244 -0
- package/cli/commands/dev/index.js +29 -4
- package/cli/commands/dev/logger.js +6 -1
- package/cli/commands/dev/overlay.js +428 -2
- package/cli/commands/dev/server.js +42 -5
- package/cli/commands/dev/watcher.js +59 -1
- package/cli/help.js +8 -5
- package/cli/scaffold/{scripts → app}/app.js +16 -23
- package/cli/scaffold/{scripts → app}/components/about.js +4 -4
- package/cli/scaffold/{scripts → app}/components/api-demo.js +1 -1
- package/cli/scaffold/{scripts → app}/components/contacts/contacts.css +0 -7
- package/cli/scaffold/{scripts → app}/components/contacts/contacts.html +3 -3
- package/cli/scaffold/app/components/home.js +137 -0
- package/cli/scaffold/{scripts → app}/routes.js +1 -1
- package/cli/scaffold/{scripts → app}/store.js +6 -6
- package/cli/scaffold/assets/.gitkeep +0 -0
- package/cli/scaffold/{styles/styles.css → global.css} +4 -2
- package/cli/scaffold/index.html +12 -11
- package/cli/utils.js +111 -6
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +1122 -158
- package/dist/zquery.min.js +3 -16
- package/index.d.ts +129 -1290
- package/index.js +15 -10
- package/package.json +7 -6
- package/src/component.js +172 -49
- package/src/core.js +359 -18
- package/src/diff.js +256 -58
- package/src/expression.js +33 -3
- package/src/reactive.js +37 -5
- package/src/router.js +243 -7
- package/tests/component.test.js +886 -0
- package/tests/core.test.js +977 -0
- package/tests/diff.test.js +525 -0
- package/tests/errors.test.js +162 -0
- package/tests/expression.test.js +482 -0
- package/tests/http.test.js +289 -0
- package/tests/reactive.test.js +339 -0
- package/tests/router.test.js +649 -0
- package/tests/store.test.js +379 -0
- package/tests/utils.test.js +512 -0
- package/types/collection.d.ts +383 -0
- package/types/component.d.ts +217 -0
- package/types/errors.d.ts +103 -0
- package/types/http.d.ts +81 -0
- package/types/misc.d.ts +179 -0
- package/types/reactive.d.ts +76 -0
- package/types/router.d.ts +161 -0
- package/types/ssr.d.ts +49 -0
- package/types/store.d.ts +107 -0
- package/types/utils.d.ts +142 -0
- package/cli/commands/dev.old.js +0 -520
- package/cli/scaffold/scripts/components/home.js +0 -137
- /package/cli/scaffold/{scripts → app}/components/contacts/contacts.js +0 -0
- /package/cli/scaffold/{scripts → app}/components/counter.js +0 -0
- /package/cli/scaffold/{scripts → app}/components/not-found.js +0 -0
- /package/cli/scaffold/{scripts → app}/components/todos.js +0 -0
|
@@ -0,0 +1,977 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { query, queryAll, ZQueryCollection } from '../src/core.js';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Setup
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
document.body.innerHTML = `
|
|
10
|
+
<div id="main">
|
|
11
|
+
<p class="text first-p">Hello</p>
|
|
12
|
+
<p class="text second-p">World</p>
|
|
13
|
+
<span class="other">Span</span>
|
|
14
|
+
<p class="text third-p">Extra</p>
|
|
15
|
+
</div>
|
|
16
|
+
<div id="sidebar">
|
|
17
|
+
<ul id="nav">
|
|
18
|
+
<li class="nav-item active">Home</li>
|
|
19
|
+
<li class="nav-item">About</li>
|
|
20
|
+
<li class="nav-item">Contact</li>
|
|
21
|
+
</ul>
|
|
22
|
+
</div>
|
|
23
|
+
`;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// query() — single selector
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
describe('query()', () => {
|
|
32
|
+
it('returns ZQueryCollection by CSS selector', () => {
|
|
33
|
+
const col = query('#main');
|
|
34
|
+
expect(col).toBeInstanceOf(ZQueryCollection);
|
|
35
|
+
expect(col.first()).toBe(document.getElementById('main'));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('returns empty collection for non-matching selector', () => {
|
|
39
|
+
const col = query('#nonexistent');
|
|
40
|
+
expect(col).toBeInstanceOf(ZQueryCollection);
|
|
41
|
+
expect(col.length).toBe(0);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('returns empty collection for null/undefined', () => {
|
|
45
|
+
expect(query(null).length).toBe(0);
|
|
46
|
+
expect(query(undefined).length).toBe(0);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('wraps DOM element in collection', () => {
|
|
50
|
+
const div = document.createElement('div');
|
|
51
|
+
const col = query(div);
|
|
52
|
+
expect(col).toBeInstanceOf(ZQueryCollection);
|
|
53
|
+
expect(col.first()).toBe(div);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('creates elements from HTML string as collection', () => {
|
|
57
|
+
const col = query('<span>hello</span>');
|
|
58
|
+
expect(col).toBeInstanceOf(ZQueryCollection);
|
|
59
|
+
expect(col.first().tagName).toBe('SPAN');
|
|
60
|
+
expect(col.first().textContent).toBe('hello');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('returns all matching elements', () => {
|
|
64
|
+
const col = query('.text');
|
|
65
|
+
expect(col.length).toBe(3);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('uses context parameter', () => {
|
|
69
|
+
const col = query('.text', '#main');
|
|
70
|
+
expect(col.length).toBe(3);
|
|
71
|
+
expect(col.first().textContent).toBe('Hello');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('supports chaining on result', () => {
|
|
75
|
+
const col = query('#main').addClass('chained');
|
|
76
|
+
expect(col.first().classList.contains('chained')).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// queryAll() — collection selector
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
describe('queryAll()', () => {
|
|
86
|
+
it('returns ZQueryCollection for CSS selector', () => {
|
|
87
|
+
const col = queryAll('.text');
|
|
88
|
+
expect(col).toBeInstanceOf(ZQueryCollection);
|
|
89
|
+
expect(col.length).toBe(3);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('returns empty collection for non-matching', () => {
|
|
93
|
+
const col = queryAll('.nonexistent');
|
|
94
|
+
expect(col.length).toBe(0);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('wraps single element', () => {
|
|
98
|
+
const div = document.createElement('div');
|
|
99
|
+
const col = queryAll(div);
|
|
100
|
+
expect(col.length).toBe(1);
|
|
101
|
+
expect(col.first()).toBe(div);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('creates elements from HTML', () => {
|
|
105
|
+
const col = queryAll('<li>a</li><li>b</li>');
|
|
106
|
+
expect(col.length).toBe(2);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('returns empty for null', () => {
|
|
110
|
+
expect(queryAll(null).length).toBe(0);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// ZQueryCollection methods
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
describe('ZQueryCollection', () => {
|
|
120
|
+
describe('iteration', () => {
|
|
121
|
+
it('each() iterates elements', () => {
|
|
122
|
+
const col = queryAll('.text');
|
|
123
|
+
const tags = [];
|
|
124
|
+
col.each((_, el) => tags.push(el.textContent));
|
|
125
|
+
expect(tags).toEqual(['Hello', 'World', 'Extra']);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('map() maps elements', () => {
|
|
129
|
+
const texts = queryAll('.text').map((_, el) => el.textContent);
|
|
130
|
+
expect(texts).toEqual(['Hello', 'World', 'Extra']);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('first() and last()', () => {
|
|
134
|
+
const col = queryAll('.text');
|
|
135
|
+
expect(col.first().textContent).toBe('Hello');
|
|
136
|
+
expect(col.last().textContent).toBe('Extra');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('eq() returns sub-collection', () => {
|
|
140
|
+
const col = queryAll('.text');
|
|
141
|
+
expect(col.eq(1).first().textContent).toBe('World');
|
|
142
|
+
expect(col.eq(5).length).toBe(0);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('toArray() returns plain array', () => {
|
|
146
|
+
const arr = queryAll('.text').toArray();
|
|
147
|
+
expect(Array.isArray(arr)).toBe(true);
|
|
148
|
+
expect(arr.length).toBe(3);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('is iterable', () => {
|
|
152
|
+
const col = queryAll('.text');
|
|
153
|
+
const items = [...col];
|
|
154
|
+
expect(items.length).toBe(3);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
describe('traversal', () => {
|
|
160
|
+
it('find() searches descendants', () => {
|
|
161
|
+
const main = queryAll('#main');
|
|
162
|
+
const ps = main.find('.text');
|
|
163
|
+
expect(ps.length).toBe(3);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('parent() returns parents', () => {
|
|
167
|
+
const p = queryAll('.text').parent();
|
|
168
|
+
expect(p.first().id).toBe('main');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('children() returns direct children', () => {
|
|
172
|
+
const main = queryAll('#main');
|
|
173
|
+
expect(main.children().length).toBe(4); // 3 p + 1 span
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('filter() with string selector', () => {
|
|
177
|
+
const col = queryAll('#main').children();
|
|
178
|
+
const ps = col.filter('p');
|
|
179
|
+
expect(ps.length).toBe(3);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('filter() with function', () => {
|
|
183
|
+
const col = queryAll('#main').children();
|
|
184
|
+
const ps = col.filter(el => el.tagName === 'P');
|
|
185
|
+
expect(ps.length).toBe(3);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('not() excludes elements', () => {
|
|
189
|
+
const col = queryAll('#main').children();
|
|
190
|
+
const nonP = col.not('p');
|
|
191
|
+
expect(nonP.length).toBe(1);
|
|
192
|
+
expect(nonP.first().tagName).toBe('SPAN');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('next() returns next sibling', () => {
|
|
196
|
+
const col = queryAll('.first-p');
|
|
197
|
+
expect(col.next().first().classList.contains('second-p')).toBe(true);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('next(selector) filters by selector', () => {
|
|
201
|
+
const col = queryAll('.first-p');
|
|
202
|
+
expect(col.next('.second-p').length).toBe(1);
|
|
203
|
+
expect(col.next('.third-p').length).toBe(0);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('prev() returns previous sibling', () => {
|
|
207
|
+
const col = queryAll('.second-p');
|
|
208
|
+
expect(col.prev().first().classList.contains('first-p')).toBe(true);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('prev(selector) filters by selector', () => {
|
|
212
|
+
const col = queryAll('.second-p');
|
|
213
|
+
expect(col.prev('.first-p').length).toBe(1);
|
|
214
|
+
expect(col.prev('.other').length).toBe(0);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('nextAll() returns all following siblings', () => {
|
|
218
|
+
const col = queryAll('.first-p');
|
|
219
|
+
expect(col.nextAll().length).toBe(3); // second-p, other, third-p
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('nextAll(selector) filters', () => {
|
|
223
|
+
const col = queryAll('.first-p');
|
|
224
|
+
expect(col.nextAll('.text').length).toBe(2);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('nextUntil() stops at selector', () => {
|
|
228
|
+
const col = queryAll('.first-p');
|
|
229
|
+
const result = col.nextUntil('.third-p');
|
|
230
|
+
expect(result.length).toBe(2); // second-p, other
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('prevAll() returns all preceding siblings', () => {
|
|
234
|
+
const col = queryAll('.third-p');
|
|
235
|
+
expect(col.prevAll().length).toBe(3);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('prevAll(selector) filters', () => {
|
|
239
|
+
const col = queryAll('.third-p');
|
|
240
|
+
expect(col.prevAll('.text').length).toBe(2);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('prevUntil() stops at selector', () => {
|
|
244
|
+
const col = queryAll('.third-p');
|
|
245
|
+
const result = col.prevUntil('.first-p');
|
|
246
|
+
expect(result.length).toBe(2); // other, second-p
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('parents() returns all ancestors', () => {
|
|
250
|
+
const col = queryAll('.first-p');
|
|
251
|
+
const parents = col.parents();
|
|
252
|
+
expect(parents.length).toBeGreaterThanOrEqual(2); // #main, body, html
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('parents(selector) filters', () => {
|
|
256
|
+
const col = queryAll('.first-p');
|
|
257
|
+
expect(col.parents('#main').length).toBe(1);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('parentsUntil() stops at selector', () => {
|
|
261
|
+
const col = queryAll('.first-p');
|
|
262
|
+
const result = col.parentsUntil('body');
|
|
263
|
+
expect(result.length).toBe(1); // just #main
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('contents() includes text nodes', () => {
|
|
267
|
+
document.body.innerHTML = '<div id="ct">text<span>child</span></div>';
|
|
268
|
+
const col = queryAll('#ct');
|
|
269
|
+
expect(col.contents().length).toBe(2); // text node + span
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('siblings() returns all siblings excluding self', () => {
|
|
273
|
+
const col = queryAll('.other');
|
|
274
|
+
const sibs = col.siblings();
|
|
275
|
+
expect(sibs.length).toBe(3); // three .text p's
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('closest() finds ancestor', () => {
|
|
279
|
+
const col = queryAll('.nav-item').eq(0);
|
|
280
|
+
expect(col.closest('#nav').length).toBe(1);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
describe('classes', () => {
|
|
286
|
+
it('addClass / hasClass / removeClass', () => {
|
|
287
|
+
const col = queryAll('#main');
|
|
288
|
+
col.addClass('active');
|
|
289
|
+
expect(col.hasClass('active')).toBe(true);
|
|
290
|
+
col.removeClass('active');
|
|
291
|
+
expect(col.hasClass('active')).toBe(false);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('toggleClass', () => {
|
|
295
|
+
const col = queryAll('#main');
|
|
296
|
+
col.toggleClass('toggled');
|
|
297
|
+
expect(col.hasClass('toggled')).toBe(true);
|
|
298
|
+
col.toggleClass('toggled');
|
|
299
|
+
expect(col.hasClass('toggled')).toBe(false);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('toggleClass with multiple classes', () => {
|
|
303
|
+
const col = queryAll('#main');
|
|
304
|
+
col.toggleClass('a', 'b');
|
|
305
|
+
expect(col[0].classList.contains('a')).toBe(true);
|
|
306
|
+
expect(col[0].classList.contains('b')).toBe(true);
|
|
307
|
+
col.toggleClass('a', 'b');
|
|
308
|
+
expect(col[0].classList.contains('a')).toBe(false);
|
|
309
|
+
expect(col[0].classList.contains('b')).toBe(false);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('toggleClass with force boolean', () => {
|
|
313
|
+
const col = queryAll('#main');
|
|
314
|
+
col.toggleClass('forced', true);
|
|
315
|
+
expect(col.hasClass('forced')).toBe(true);
|
|
316
|
+
col.toggleClass('forced', true);
|
|
317
|
+
expect(col.hasClass('forced')).toBe(true);
|
|
318
|
+
col.toggleClass('forced', false);
|
|
319
|
+
expect(col.hasClass('forced')).toBe(false);
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
describe('attributes', () => {
|
|
325
|
+
it('attr get/set', () => {
|
|
326
|
+
const col = queryAll('#main');
|
|
327
|
+
col.attr('data-test', 'value');
|
|
328
|
+
expect(col.attr('data-test')).toBe('value');
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('removeAttr', () => {
|
|
332
|
+
const col = queryAll('#main');
|
|
333
|
+
col.attr('data-x', 'y');
|
|
334
|
+
col.removeAttr('data-x');
|
|
335
|
+
expect(col.attr('data-x')).toBeNull();
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('data get/set', () => {
|
|
339
|
+
const col = queryAll('#main');
|
|
340
|
+
col.data('count', 42);
|
|
341
|
+
expect(col.data('count')).toBe(42);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('data handles objects via JSON', () => {
|
|
345
|
+
const col = queryAll('#main');
|
|
346
|
+
col.data('info', { a: 1 });
|
|
347
|
+
expect(col.data('info')).toEqual({ a: 1 });
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
describe('content', () => {
|
|
353
|
+
it('html get/set', () => {
|
|
354
|
+
const col = queryAll('#main');
|
|
355
|
+
col.html('<b>bold</b>');
|
|
356
|
+
expect(col.html()).toBe('<b>bold</b>');
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('html() auto-morphs when element has existing children', () => {
|
|
360
|
+
const main = document.querySelector('#main');
|
|
361
|
+
main.innerHTML = '<p id="preserved">old text</p>';
|
|
362
|
+
const ref = main.children[0]; // grab DOM reference
|
|
363
|
+
const col = queryAll('#main');
|
|
364
|
+
col.html('<p id="preserved">new text</p>');
|
|
365
|
+
// Same DOM node preserved — morph, not innerHTML replace
|
|
366
|
+
expect(main.children[0]).toBe(ref);
|
|
367
|
+
expect(main.children[0].textContent).toBe('new text');
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('html() uses innerHTML for empty elements (fast first-paint)', () => {
|
|
371
|
+
const main = document.querySelector('#main');
|
|
372
|
+
main.innerHTML = ''; // make it empty
|
|
373
|
+
const col = queryAll('#main');
|
|
374
|
+
col.html('<p id="fresh">hello</p>');
|
|
375
|
+
expect(main.innerHTML).toBe('<p id="fresh">hello</p>');
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('empty().html() forces raw innerHTML (opt-out of morph)', () => {
|
|
379
|
+
const main = document.querySelector('#main');
|
|
380
|
+
main.innerHTML = '<p id="old">will be destroyed</p>';
|
|
381
|
+
const ref = main.children[0];
|
|
382
|
+
const col = queryAll('#main');
|
|
383
|
+
col.empty().html('<p id="old">replaced</p>');
|
|
384
|
+
// NOT the same node — empty() cleared children, so html() used innerHTML
|
|
385
|
+
expect(main.children[0]).not.toBe(ref);
|
|
386
|
+
expect(main.children[0].textContent).toBe('replaced');
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('text get/set', () => {
|
|
390
|
+
const col = queryAll('.text').eq(0);
|
|
391
|
+
col.text('Changed');
|
|
392
|
+
expect(col.text()).toBe('Changed');
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it('morph() diffs content instead of replacing', () => {
|
|
396
|
+
const main = document.querySelector('#main');
|
|
397
|
+
main.innerHTML = '<p id="keep">old</p>';
|
|
398
|
+
const ref = main.children[0];
|
|
399
|
+
const col = queryAll('#main');
|
|
400
|
+
col.morph('<p id="keep">new</p>');
|
|
401
|
+
// Same DOM node preserved (morph, not innerHTML)
|
|
402
|
+
expect(main.children[0]).toBe(ref);
|
|
403
|
+
expect(main.children[0].textContent).toBe('new');
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it('morph() is chainable', () => {
|
|
407
|
+
const col = queryAll('#main');
|
|
408
|
+
const ret = col.morph('<p>m</p>');
|
|
409
|
+
expect(ret).toBe(col);
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
describe('DOM manipulation', () => {
|
|
415
|
+
it('append() adds content', () => {
|
|
416
|
+
const col = queryAll('#main');
|
|
417
|
+
col.append('<div class="appended">new</div>');
|
|
418
|
+
expect(document.querySelector('.appended').textContent).toBe('new');
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it('prepend() adds at start', () => {
|
|
422
|
+
const col = queryAll('#main');
|
|
423
|
+
col.prepend('<div class="first">start</div>');
|
|
424
|
+
expect(col.first().firstElementChild.className).toBe('first');
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it('remove() removes elements', () => {
|
|
428
|
+
queryAll('.other').remove();
|
|
429
|
+
expect(document.querySelector('.other')).toBeNull();
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it('empty() clears content', () => {
|
|
433
|
+
queryAll('#main').empty();
|
|
434
|
+
expect(document.querySelector('#main').children.length).toBe(0);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it('clone() creates deep copy', () => {
|
|
438
|
+
const col = queryAll('.text').eq(0);
|
|
439
|
+
const clone = col.clone();
|
|
440
|
+
expect(clone.first().textContent).toBe('Hello');
|
|
441
|
+
expect(clone.first()).not.toBe(col.first());
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
describe('visibility', () => {
|
|
447
|
+
it('hide() sets display none', () => {
|
|
448
|
+
queryAll('#main').hide();
|
|
449
|
+
expect(document.getElementById('main').style.display).toBe('none');
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it('show() clears display', () => {
|
|
453
|
+
const main = document.getElementById('main');
|
|
454
|
+
main.style.display = 'none';
|
|
455
|
+
queryAll('#main').show();
|
|
456
|
+
expect(main.style.display).toBe('');
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
describe('events', () => {
|
|
462
|
+
it('on() and trigger()', () => {
|
|
463
|
+
let clicked = false;
|
|
464
|
+
const col = queryAll('#main');
|
|
465
|
+
col.on('click', () => { clicked = true; });
|
|
466
|
+
col.trigger('click');
|
|
467
|
+
expect(clicked).toBe(true);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it('off() removes handler', () => {
|
|
471
|
+
let count = 0;
|
|
472
|
+
const handler = () => { count++; };
|
|
473
|
+
const col = queryAll('#main');
|
|
474
|
+
col.on('click', handler);
|
|
475
|
+
col.trigger('click');
|
|
476
|
+
col.off('click', handler);
|
|
477
|
+
col.trigger('click');
|
|
478
|
+
expect(count).toBe(1);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
it('on() works on non-Element targets like window', () => {
|
|
482
|
+
let fired = false;
|
|
483
|
+
const col = new ZQueryCollection([window]);
|
|
484
|
+
col.on('custom-win-evt', () => { fired = true; });
|
|
485
|
+
window.dispatchEvent(new Event('custom-win-evt'));
|
|
486
|
+
expect(fired).toBe(true);
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it('$.on() with EventTarget binds directly to that target', () => {
|
|
490
|
+
let fired = false;
|
|
491
|
+
const target = new EventTarget();
|
|
492
|
+
query.on('test-evt', target, () => { fired = true; });
|
|
493
|
+
target.dispatchEvent(new Event('test-evt'));
|
|
494
|
+
expect(fired).toBe(true);
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
// ---------------------------------------------------------------------------
|
|
501
|
+
// Quick ref helpers
|
|
502
|
+
// ---------------------------------------------------------------------------
|
|
503
|
+
|
|
504
|
+
describe('query quick refs', () => {
|
|
505
|
+
it('$.id() returns element by id', () => {
|
|
506
|
+
expect(query.id('main')).toBe(document.getElementById('main'));
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it('$.class() returns first element by class', () => {
|
|
510
|
+
expect(query.class('text').textContent).toBe('Hello');
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
it('$.classes() returns ZQueryCollection', () => {
|
|
514
|
+
const col = query.classes('text');
|
|
515
|
+
expect(col).toBeInstanceOf(ZQueryCollection);
|
|
516
|
+
expect(col.length).toBe(3);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
it('$.children() returns ZQueryCollection', () => {
|
|
520
|
+
const col = query.children('main');
|
|
521
|
+
expect(col).toBeInstanceOf(ZQueryCollection);
|
|
522
|
+
expect(col.length).toBe(4);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it('$.tag() returns ZQueryCollection', () => {
|
|
526
|
+
const col = query.tag('p');
|
|
527
|
+
expect(col).toBeInstanceOf(ZQueryCollection);
|
|
528
|
+
expect(col.length).toBe(3);
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
describe('collection forEach() works like Array.forEach()', () => {
|
|
532
|
+
it('iterates with el, index, array args', () => {
|
|
533
|
+
const results = [];
|
|
534
|
+
query.classes('text').forEach((el, i) => results.push({ i, text: el.textContent }));
|
|
535
|
+
expect(results).toEqual([{ i: 0, text: 'Hello' }, { i: 1, text: 'World' }, { i: 2, text: 'Extra' }]);
|
|
536
|
+
});
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
it('$.create() creates element with attributes', () => {
|
|
540
|
+
const col = query.create('div', { class: 'new', id: 'created' }, 'text');
|
|
541
|
+
expect(col).toBeInstanceOf(ZQueryCollection);
|
|
542
|
+
expect(col.length).toBe(1);
|
|
543
|
+
const el = col[0];
|
|
544
|
+
expect(el.tagName).toBe('DIV');
|
|
545
|
+
expect(el.className).toBe('new');
|
|
546
|
+
expect(el.id).toBe('created');
|
|
547
|
+
expect(el.textContent).toBe('text');
|
|
548
|
+
});
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
// ---------------------------------------------------------------------------
|
|
553
|
+
// New filtering / collection methods
|
|
554
|
+
// ---------------------------------------------------------------------------
|
|
555
|
+
|
|
556
|
+
describe('filtering & collection', () => {
|
|
557
|
+
it('is() checks if any element matches selector', () => {
|
|
558
|
+
expect(queryAll('#main').children().is('p')).toBe(true);
|
|
559
|
+
expect(queryAll('#main').children().is('table')).toBe(false);
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
it('is() with function', () => {
|
|
563
|
+
const result = queryAll('.text').is(function(i) { return this.textContent === 'World'; });
|
|
564
|
+
expect(result).toBe(true);
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
it('has() keeps elements containing matching descendant', () => {
|
|
568
|
+
const col = queryAll('#sidebar').has('.nav-item');
|
|
569
|
+
expect(col.length).toBe(1);
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
it('slice() returns subset', () => {
|
|
573
|
+
const col = queryAll('.text').slice(0, 2);
|
|
574
|
+
expect(col.length).toBe(2);
|
|
575
|
+
expect(col.first().textContent).toBe('Hello');
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
it('slice() with negative index', () => {
|
|
579
|
+
const col = queryAll('.text').slice(-1);
|
|
580
|
+
expect(col.length).toBe(1);
|
|
581
|
+
expect(col.first().textContent).toBe('Extra');
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
it('add() merges collections', () => {
|
|
585
|
+
const col = queryAll('.first-p').add('.other');
|
|
586
|
+
expect(col.length).toBe(2);
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
it('add() with element', () => {
|
|
590
|
+
const el = document.getElementById('main');
|
|
591
|
+
const col = queryAll('.first-p').add(el);
|
|
592
|
+
expect(col.length).toBe(2);
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
it('add() with ZQueryCollection', () => {
|
|
596
|
+
const col = queryAll('.first-p').add(queryAll('.other'));
|
|
597
|
+
expect(col.length).toBe(2);
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
it('get() with no args returns array', () => {
|
|
601
|
+
const arr = queryAll('.text').get();
|
|
602
|
+
expect(Array.isArray(arr)).toBe(true);
|
|
603
|
+
expect(arr.length).toBe(3);
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
it('get(index) returns element', () => {
|
|
607
|
+
const el = queryAll('.text').get(1);
|
|
608
|
+
expect(el.textContent).toBe('World');
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
it('get(negative) returns from end', () => {
|
|
612
|
+
const el = queryAll('.text').get(-1);
|
|
613
|
+
expect(el.textContent).toBe('Extra');
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
it('index() returns position among siblings', () => {
|
|
617
|
+
const col = queryAll('.other');
|
|
618
|
+
expect(col.index()).toBe(2); // 3rd child (0-indexed)
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
it('index(element) returns position in collection', () => {
|
|
622
|
+
const other = document.querySelector('.other');
|
|
623
|
+
const col = queryAll('#main').children();
|
|
624
|
+
expect(col.index(other)).toBe(2);
|
|
625
|
+
});
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
// ---------------------------------------------------------------------------
|
|
630
|
+
// Inverse DOM manipulation (appendTo, prependTo, insertAfter, insertBefore, etc.)
|
|
631
|
+
// ---------------------------------------------------------------------------
|
|
632
|
+
|
|
633
|
+
describe('inverse DOM manipulation', () => {
|
|
634
|
+
it('appendTo() moves elements into target', () => {
|
|
635
|
+
queryAll('<div class="new-item">new</div>').appendTo('#nav');
|
|
636
|
+
const nav = document.getElementById('nav');
|
|
637
|
+
expect(nav.lastElementChild.className).toBe('new-item');
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
it('prependTo() inserts at start of target', () => {
|
|
641
|
+
queryAll('<li class="first-item">first</li>').prependTo('#nav');
|
|
642
|
+
const nav = document.getElementById('nav');
|
|
643
|
+
expect(nav.firstElementChild.className).toBe('first-item');
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
it('insertAfter() inserts after target', () => {
|
|
647
|
+
queryAll('<span class="inserted">!</span>').insertAfter('.first-p');
|
|
648
|
+
const firstP = document.querySelector('.first-p');
|
|
649
|
+
expect(firstP.nextElementSibling.className).toBe('inserted');
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
it('insertBefore() inserts before target', () => {
|
|
653
|
+
queryAll('<span class="inserted">!</span>').insertBefore('.second-p');
|
|
654
|
+
const secondP = document.querySelector('.second-p');
|
|
655
|
+
expect(secondP.previousElementSibling.className).toBe('inserted');
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
it('replaceAll() replaces target elements', () => {
|
|
659
|
+
queryAll('<em>replaced</em>').replaceAll('.other');
|
|
660
|
+
expect(document.querySelector('.other')).toBeNull();
|
|
661
|
+
expect(document.querySelector('em').textContent).toBe('replaced');
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
it('unwrap() removes parent wrapper', () => {
|
|
665
|
+
// wrap nav-items in a div first
|
|
666
|
+
const items = queryAll('.nav-item');
|
|
667
|
+
const count = items.length;
|
|
668
|
+
items.eq(0).wrap('<div class="wrapper"></div>');
|
|
669
|
+
expect(document.querySelector('.wrapper')).not.toBeNull();
|
|
670
|
+
queryAll('.nav-item').eq(0).unwrap('.wrapper');
|
|
671
|
+
expect(document.querySelector('.wrapper')).toBeNull();
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
it('wrapAll() wraps all elements in one wrapper', () => {
|
|
675
|
+
queryAll('.nav-item').wrapAll('<div class="all-wrap"></div>');
|
|
676
|
+
const wrap = document.querySelector('.all-wrap');
|
|
677
|
+
expect(wrap).not.toBeNull();
|
|
678
|
+
expect(wrap.children.length).toBe(3);
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
it('wrapInner() wraps inner contents', () => {
|
|
682
|
+
queryAll('.first-p').wrapInner('<strong></strong>');
|
|
683
|
+
const strong = document.querySelector('.first-p > strong');
|
|
684
|
+
expect(strong).not.toBeNull();
|
|
685
|
+
expect(strong.textContent).toBe('Hello');
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
it('detach() removes elements (alias for remove)', () => {
|
|
689
|
+
queryAll('.other').detach();
|
|
690
|
+
expect(document.querySelector('.other')).toBeNull();
|
|
691
|
+
});
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
// ---------------------------------------------------------------------------
|
|
696
|
+
// CSS dimension methods
|
|
697
|
+
// ---------------------------------------------------------------------------
|
|
698
|
+
|
|
699
|
+
describe('CSS dimension methods', () => {
|
|
700
|
+
it('scrollTop() get returns a number', () => {
|
|
701
|
+
const val = queryAll('#main').scrollTop();
|
|
702
|
+
expect(typeof val).toBe('number');
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
it('scrollTop(value) sets scroll position', () => {
|
|
706
|
+
const main = document.getElementById('main');
|
|
707
|
+
main.style.overflow = 'auto';
|
|
708
|
+
main.style.height = '10px';
|
|
709
|
+
main.innerHTML = '<div style="height:1000px">tall</div>';
|
|
710
|
+
queryAll('#main').scrollTop(50);
|
|
711
|
+
expect(main.scrollTop).toBe(50);
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
it('scrollLeft() get returns a number', () => {
|
|
715
|
+
const val = queryAll('#main').scrollLeft();
|
|
716
|
+
expect(typeof val).toBe('number');
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
it('innerWidth() returns clientWidth', () => {
|
|
720
|
+
const main = document.getElementById('main');
|
|
721
|
+
// jsdom sets clientWidth to 0 but the method should still return a number
|
|
722
|
+
const val = queryAll('#main').innerWidth();
|
|
723
|
+
expect(typeof val).toBe('number');
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
it('innerHeight() returns clientHeight', () => {
|
|
727
|
+
const val = queryAll('#main').innerHeight();
|
|
728
|
+
expect(typeof val).toBe('number');
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
it('outerWidth() returns offsetWidth', () => {
|
|
732
|
+
const val = queryAll('#main').outerWidth();
|
|
733
|
+
expect(typeof val).toBe('number');
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
it('outerHeight() returns offsetHeight', () => {
|
|
737
|
+
const val = queryAll('#main').outerHeight();
|
|
738
|
+
expect(typeof val).toBe('number');
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
it('outerWidth(true) includes margin', () => {
|
|
742
|
+
const main = document.getElementById('main');
|
|
743
|
+
main.style.margin = '10px';
|
|
744
|
+
const val = queryAll('#main').outerWidth(true);
|
|
745
|
+
expect(typeof val).toBe('number');
|
|
746
|
+
});
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
// ---------------------------------------------------------------------------
|
|
751
|
+
// prop() method
|
|
752
|
+
// ---------------------------------------------------------------------------
|
|
753
|
+
|
|
754
|
+
describe('ZQueryCollection — prop()', () => {
|
|
755
|
+
it('gets a DOM property', () => {
|
|
756
|
+
document.body.innerHTML = '<input type="checkbox" checked>';
|
|
757
|
+
const col = queryAll('input');
|
|
758
|
+
expect(col.prop('checked')).toBe(true);
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
it('sets a DOM property', () => {
|
|
762
|
+
document.body.innerHTML = '<input type="checkbox">';
|
|
763
|
+
const col = queryAll('input');
|
|
764
|
+
col.prop('checked', true);
|
|
765
|
+
expect(col[0].checked).toBe(true);
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
it('sets multiple properties via sequential calls', () => {
|
|
769
|
+
document.body.innerHTML = '<input type="text">';
|
|
770
|
+
const col = queryAll('input');
|
|
771
|
+
col.prop('disabled', true);
|
|
772
|
+
col.prop('value', 'hello');
|
|
773
|
+
expect(col[0].disabled).toBe(true);
|
|
774
|
+
expect(col[0].value).toBe('hello');
|
|
775
|
+
});
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
// ---------------------------------------------------------------------------
|
|
780
|
+
// css() method
|
|
781
|
+
// ---------------------------------------------------------------------------
|
|
782
|
+
|
|
783
|
+
describe('ZQueryCollection — css()', () => {
|
|
784
|
+
beforeEach(() => {
|
|
785
|
+
document.body.innerHTML = '<div id="styled">test</div>';
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
it('sets style properties from object', () => {
|
|
789
|
+
queryAll('#styled').css({ color: 'red', 'font-size': '16px' });
|
|
790
|
+
const el = document.getElementById('styled');
|
|
791
|
+
expect(el.style.color).toBe('red');
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
it('returns collection for chaining', () => {
|
|
795
|
+
const col = queryAll('#styled').css({ color: 'blue' });
|
|
796
|
+
expect(col).toBeInstanceOf(ZQueryCollection);
|
|
797
|
+
});
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
// ---------------------------------------------------------------------------
|
|
802
|
+
// val() method
|
|
803
|
+
// ---------------------------------------------------------------------------
|
|
804
|
+
|
|
805
|
+
describe('ZQueryCollection — val()', () => {
|
|
806
|
+
it('gets input value', () => {
|
|
807
|
+
document.body.innerHTML = '<input value="test">';
|
|
808
|
+
expect(queryAll('input').val()).toBe('test');
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
it('sets input value', () => {
|
|
812
|
+
document.body.innerHTML = '<input value="">';
|
|
813
|
+
queryAll('input').val('new value');
|
|
814
|
+
expect(document.querySelector('input').value).toBe('new value');
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
it('gets select value', () => {
|
|
818
|
+
document.body.innerHTML = '<select><option value="a" selected>A</option><option value="b">B</option></select>';
|
|
819
|
+
expect(queryAll('select').val()).toBe('a');
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
it('gets textarea value', () => {
|
|
823
|
+
document.body.innerHTML = '<textarea>hello</textarea>';
|
|
824
|
+
expect(queryAll('textarea').val()).toBe('hello');
|
|
825
|
+
});
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
// ---------------------------------------------------------------------------
|
|
830
|
+
// after(), before() methods
|
|
831
|
+
// ---------------------------------------------------------------------------
|
|
832
|
+
|
|
833
|
+
describe('ZQueryCollection — after() / before()', () => {
|
|
834
|
+
beforeEach(() => {
|
|
835
|
+
document.body.innerHTML = '<div id="container"><p id="target">target</p></div>';
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
it('after() inserts content after element', () => {
|
|
839
|
+
queryAll('#target').after('<span class="after">after</span>');
|
|
840
|
+
const next = document.getElementById('target').nextElementSibling;
|
|
841
|
+
expect(next.className).toBe('after');
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
it('before() inserts content before element', () => {
|
|
845
|
+
queryAll('#target').before('<span class="before">before</span>');
|
|
846
|
+
const prev = document.getElementById('target').previousElementSibling;
|
|
847
|
+
expect(prev.className).toBe('before');
|
|
848
|
+
});
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
// ---------------------------------------------------------------------------
|
|
853
|
+
// wrap() method
|
|
854
|
+
// ---------------------------------------------------------------------------
|
|
855
|
+
|
|
856
|
+
describe('ZQueryCollection — wrap()', () => {
|
|
857
|
+
it('wraps element in new parent', () => {
|
|
858
|
+
document.body.innerHTML = '<div id="container"><p id="target">text</p></div>';
|
|
859
|
+
queryAll('#target').wrap('<div class="wrapper"></div>');
|
|
860
|
+
const wrapper = document.querySelector('.wrapper');
|
|
861
|
+
expect(wrapper).not.toBeNull();
|
|
862
|
+
expect(wrapper.querySelector('#target')).not.toBeNull();
|
|
863
|
+
});
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
// ---------------------------------------------------------------------------
|
|
868
|
+
// replaceWith() method
|
|
869
|
+
// ---------------------------------------------------------------------------
|
|
870
|
+
|
|
871
|
+
describe('ZQueryCollection — replaceWith()', () => {
|
|
872
|
+
it('replaces element with new content', () => {
|
|
873
|
+
document.body.innerHTML = '<div id="container"><p id="old">old</p></div>';
|
|
874
|
+
queryAll('#old').replaceWith('<span id="new">new</span>');
|
|
875
|
+
expect(document.querySelector('#old')).toBeNull();
|
|
876
|
+
expect(document.querySelector('#new')).not.toBeNull();
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
it('auto-morphs when tag name matches (preserves identity)', () => {
|
|
880
|
+
document.body.innerHTML = '<div id="container"><p id="target" class="old">old text</p></div>';
|
|
881
|
+
const target = document.querySelector('#target');
|
|
882
|
+
queryAll('#target').replaceWith('<p id="target" class="new">new text</p>');
|
|
883
|
+
// Same DOM node — morphed, not replaced
|
|
884
|
+
expect(document.querySelector('#target')).toBe(target);
|
|
885
|
+
expect(target.className).toBe('new');
|
|
886
|
+
expect(target.textContent).toBe('new text');
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
it('replaces when tag name differs', () => {
|
|
890
|
+
document.body.innerHTML = '<div id="container"><p id="old">old</p></div>';
|
|
891
|
+
const oldRef = document.querySelector('#old');
|
|
892
|
+
queryAll('#old').replaceWith('<section id="replaced">new</section>');
|
|
893
|
+
expect(document.querySelector('#replaced')).not.toBe(oldRef);
|
|
894
|
+
expect(document.querySelector('#replaced').tagName).toBe('SECTION');
|
|
895
|
+
});
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
// ---------------------------------------------------------------------------
|
|
900
|
+
// offset() and position()
|
|
901
|
+
// ---------------------------------------------------------------------------
|
|
902
|
+
|
|
903
|
+
describe('ZQueryCollection — offset() / position()', () => {
|
|
904
|
+
it('offset() returns object with top and left', () => {
|
|
905
|
+
document.body.innerHTML = '<div id="box">box</div>';
|
|
906
|
+
const off = queryAll('#box').offset();
|
|
907
|
+
expect(off).toHaveProperty('top');
|
|
908
|
+
expect(off).toHaveProperty('left');
|
|
909
|
+
expect(typeof off.top).toBe('number');
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
it('position() returns object with top and left', () => {
|
|
913
|
+
document.body.innerHTML = '<div id="box">box</div>';
|
|
914
|
+
const pos = queryAll('#box').position();
|
|
915
|
+
expect(pos).toHaveProperty('top');
|
|
916
|
+
expect(pos).toHaveProperty('left');
|
|
917
|
+
});
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
|
|
921
|
+
// ---------------------------------------------------------------------------
|
|
922
|
+
// width() and height()
|
|
923
|
+
// ---------------------------------------------------------------------------
|
|
924
|
+
|
|
925
|
+
describe('ZQueryCollection — width() / height()', () => {
|
|
926
|
+
it('width() returns a number', () => {
|
|
927
|
+
document.body.innerHTML = '<div id="box" style="width:100px">box</div>';
|
|
928
|
+
const val = queryAll('#box').width();
|
|
929
|
+
expect(typeof val).toBe('number');
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
it('height() returns a number', () => {
|
|
933
|
+
document.body.innerHTML = '<div id="box" style="height:50px">box</div>';
|
|
934
|
+
const val = queryAll('#box').height();
|
|
935
|
+
expect(typeof val).toBe('number');
|
|
936
|
+
});
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
|
|
940
|
+
// ---------------------------------------------------------------------------
|
|
941
|
+
// animate()
|
|
942
|
+
// ---------------------------------------------------------------------------
|
|
943
|
+
|
|
944
|
+
describe('ZQueryCollection — animate()', () => {
|
|
945
|
+
it('returns a promise', () => {
|
|
946
|
+
document.body.innerHTML = '<div id="box">box</div>';
|
|
947
|
+
const result = queryAll('#box').animate({ opacity: 0 }, 100);
|
|
948
|
+
// animate() returns a Promise
|
|
949
|
+
expect(result).toBeInstanceOf(Promise);
|
|
950
|
+
});
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
// ---------------------------------------------------------------------------
|
|
955
|
+
// hover() convenience
|
|
956
|
+
// ---------------------------------------------------------------------------
|
|
957
|
+
|
|
958
|
+
describe('hover()', () => {
|
|
959
|
+
it('binds mouseenter and mouseleave', () => {
|
|
960
|
+
let entered = false, left = false;
|
|
961
|
+
const col = queryAll('#main');
|
|
962
|
+
col.hover(() => { entered = true; }, () => { left = true; });
|
|
963
|
+
col.first().dispatchEvent(new Event('mouseenter'));
|
|
964
|
+
col.first().dispatchEvent(new Event('mouseleave'));
|
|
965
|
+
expect(entered).toBe(true);
|
|
966
|
+
expect(left).toBe(true);
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
it('uses same fn for both if only one provided', () => {
|
|
970
|
+
let count = 0;
|
|
971
|
+
const col = queryAll('#main');
|
|
972
|
+
col.hover(() => { count++; });
|
|
973
|
+
col.first().dispatchEvent(new Event('mouseenter'));
|
|
974
|
+
col.first().dispatchEvent(new Event('mouseleave'));
|
|
975
|
+
expect(count).toBe(2);
|
|
976
|
+
});
|
|
977
|
+
});
|