zero-query 0.7.5 → 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 +37 -27
- package/cli/commands/build.js +110 -1
- package/cli/commands/bundle.js +107 -22
- 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 +28 -3
- package/cli/commands/dev/logger.js +6 -1
- package/cli/commands/dev/overlay.js +377 -0
- package/cli/commands/dev/server.js +8 -0
- package/cli/commands/dev/watcher.js +26 -1
- package/cli/help.js +8 -5
- package/cli/scaffold/{scripts → app}/app.js +1 -1
- package/cli/scaffold/{scripts → app}/components/about.js +4 -4
- package/cli/scaffold/{scripts → app}/components/api-demo.js +1 -1
- 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} +3 -2
- package/cli/scaffold/index.html +11 -11
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +746 -134
- package/dist/zquery.min.js +2 -2
- package/index.d.ts +11 -9
- package/index.js +15 -10
- package/package.json +3 -2
- package/src/component.js +161 -48
- package/src/core.js +57 -11
- package/src/diff.js +256 -58
- package/src/expression.js +33 -3
- package/src/reactive.js +37 -5
- package/src/router.js +195 -6
- package/tests/component.test.js +582 -0
- package/tests/core.test.js +251 -0
- package/tests/diff.test.js +333 -2
- package/tests/expression.test.js +148 -0
- package/tests/http.test.js +108 -0
- package/tests/reactive.test.js +148 -0
- package/tests/router.test.js +317 -0
- package/tests/store.test.js +126 -0
- package/tests/utils.test.js +161 -2
- package/types/collection.d.ts +17 -2
- package/types/component.d.ts +7 -0
- package/types/misc.d.ts +13 -0
- package/types/router.d.ts +30 -1
- 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.css +0 -0
- /package/cli/scaffold/{scripts → app}/components/contacts/contacts.html +0 -0
- /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
package/tests/component.test.js
CHANGED
|
@@ -122,6 +122,21 @@ describe('component — lifecycle', () => {
|
|
|
122
122
|
document.body.innerHTML = '<life-throw id="lt"></life-throw>';
|
|
123
123
|
expect(() => mount('#lt', 'life-throw')).not.toThrow();
|
|
124
124
|
});
|
|
125
|
+
|
|
126
|
+
it('calls updated on re-render', async () => {
|
|
127
|
+
const updatedFn = vi.fn();
|
|
128
|
+
component('life-update', {
|
|
129
|
+
state: () => ({ n: 0 }),
|
|
130
|
+
updated: updatedFn,
|
|
131
|
+
render() { return `<div>${this.state.n}</div>`; },
|
|
132
|
+
});
|
|
133
|
+
document.body.innerHTML = '<life-update id="lu"></life-update>';
|
|
134
|
+
const inst = mount('#lu', 'life-update');
|
|
135
|
+
expect(updatedFn).not.toHaveBeenCalled();
|
|
136
|
+
inst.state.n = 1;
|
|
137
|
+
await new Promise(r => queueMicrotask(r));
|
|
138
|
+
expect(updatedFn).toHaveBeenCalledOnce();
|
|
139
|
+
});
|
|
125
140
|
});
|
|
126
141
|
|
|
127
142
|
|
|
@@ -144,6 +159,26 @@ describe('component — reactive state', () => {
|
|
|
144
159
|
await new Promise(r => queueMicrotask(r));
|
|
145
160
|
expect(document.querySelector('.count').textContent).toBe('5');
|
|
146
161
|
});
|
|
162
|
+
|
|
163
|
+
it('batches multiple state changes into one render', async () => {
|
|
164
|
+
const renderSpy = vi.fn();
|
|
165
|
+
component('batch-state', {
|
|
166
|
+
state: () => ({ a: 0, b: 0 }),
|
|
167
|
+
render() {
|
|
168
|
+
renderSpy();
|
|
169
|
+
return `<div>${this.state.a}-${this.state.b}</div>`;
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
document.body.innerHTML = '<batch-state id="bs"></batch-state>';
|
|
173
|
+
const inst = mount('#bs', 'batch-state');
|
|
174
|
+
renderSpy.mockClear();
|
|
175
|
+
|
|
176
|
+
inst.state.a = 1;
|
|
177
|
+
inst.state.b = 2;
|
|
178
|
+
await new Promise(r => queueMicrotask(r));
|
|
179
|
+
// Should only render once despite two state changes
|
|
180
|
+
expect(renderSpy).toHaveBeenCalledOnce();
|
|
181
|
+
});
|
|
147
182
|
});
|
|
148
183
|
|
|
149
184
|
|
|
@@ -190,6 +225,22 @@ describe('component — computed', () => {
|
|
|
190
225
|
expect(inst.computed.doubled).toBe(10);
|
|
191
226
|
expect(document.querySelector('.doubled').textContent).toBe('10');
|
|
192
227
|
});
|
|
228
|
+
|
|
229
|
+
it('recomputes when state changes', async () => {
|
|
230
|
+
component('comp-recompute', {
|
|
231
|
+
state: () => ({ val: 3 }),
|
|
232
|
+
computed: {
|
|
233
|
+
tripled(state) { return state.val * 3; },
|
|
234
|
+
},
|
|
235
|
+
render() { return `<span class="tri">${this.computed.tripled}</span>`; },
|
|
236
|
+
});
|
|
237
|
+
document.body.innerHTML = '<comp-recompute id="cr"></comp-recompute>';
|
|
238
|
+
const inst = mount('#cr', 'comp-recompute');
|
|
239
|
+
expect(inst.computed.tripled).toBe(9);
|
|
240
|
+
inst.state.val = 10;
|
|
241
|
+
await new Promise(r => queueMicrotask(r));
|
|
242
|
+
expect(inst.computed.tripled).toBe(30);
|
|
243
|
+
});
|
|
193
244
|
});
|
|
194
245
|
|
|
195
246
|
|
|
@@ -230,6 +281,20 @@ describe('component — setState', () => {
|
|
|
230
281
|
expect(inst.state.a).toBe(10);
|
|
231
282
|
expect(inst.state.b).toBe(20);
|
|
232
283
|
});
|
|
284
|
+
|
|
285
|
+
it('forces re-render with empty setState', async () => {
|
|
286
|
+
let renderCount = 0;
|
|
287
|
+
component('force-render', {
|
|
288
|
+
state: () => ({ x: 1 }),
|
|
289
|
+
render() { renderCount++; return `<div>${this.state.x}</div>`; },
|
|
290
|
+
});
|
|
291
|
+
document.body.innerHTML = '<force-render id="fr"></force-render>';
|
|
292
|
+
const inst = mount('#fr', 'force-render');
|
|
293
|
+
renderCount = 0;
|
|
294
|
+
inst.setState({});
|
|
295
|
+
await new Promise(r => queueMicrotask(r));
|
|
296
|
+
expect(renderCount).toBe(1);
|
|
297
|
+
});
|
|
233
298
|
});
|
|
234
299
|
|
|
235
300
|
|
|
@@ -281,6 +346,21 @@ describe('component — destroy', () => {
|
|
|
281
346
|
destroy('#d2');
|
|
282
347
|
expect(() => destroy('#d2')).not.toThrow();
|
|
283
348
|
});
|
|
349
|
+
|
|
350
|
+
it('stops re-rendering after destroy', async () => {
|
|
351
|
+
let renderCount = 0;
|
|
352
|
+
component('destroy-stop', {
|
|
353
|
+
state: () => ({ n: 0 }),
|
|
354
|
+
render() { renderCount++; return `<div>${this.state.n}</div>`; },
|
|
355
|
+
});
|
|
356
|
+
document.body.innerHTML = '<destroy-stop id="ds"></destroy-stop>';
|
|
357
|
+
const inst = mount('#ds', 'destroy-stop');
|
|
358
|
+
renderCount = 0;
|
|
359
|
+
destroy('#ds');
|
|
360
|
+
inst.state.n = 99;
|
|
361
|
+
await new Promise(r => queueMicrotask(r));
|
|
362
|
+
expect(renderCount).toBe(0);
|
|
363
|
+
});
|
|
284
364
|
});
|
|
285
365
|
|
|
286
366
|
|
|
@@ -301,4 +381,506 @@ describe('mountAll()', () => {
|
|
|
301
381
|
expect(document.querySelector('.auto-a').textContent).toBe('A');
|
|
302
382
|
expect(document.querySelector('.auto-b').textContent).toBe('B');
|
|
303
383
|
});
|
|
384
|
+
|
|
385
|
+
it('does not re-mount already mounted components', () => {
|
|
386
|
+
let initCount = 0;
|
|
387
|
+
component('auto-once', {
|
|
388
|
+
init() { initCount++; },
|
|
389
|
+
render() { return '<div>once</div>'; },
|
|
390
|
+
});
|
|
391
|
+
document.body.innerHTML = '<auto-once></auto-once>';
|
|
392
|
+
mountAll();
|
|
393
|
+
mountAll(); // second call
|
|
394
|
+
expect(initCount).toBe(1);
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
// ---------------------------------------------------------------------------
|
|
400
|
+
// z-if / z-else-if / z-else
|
|
401
|
+
// ---------------------------------------------------------------------------
|
|
402
|
+
|
|
403
|
+
describe('component — z-if directive', () => {
|
|
404
|
+
it('shows element when condition is true', () => {
|
|
405
|
+
component('zif-true', {
|
|
406
|
+
state: () => ({ show: true }),
|
|
407
|
+
render() { return '<p z-if="show">visible</p>'; },
|
|
408
|
+
});
|
|
409
|
+
document.body.innerHTML = '<zif-true id="zt"></zif-true>';
|
|
410
|
+
mount('#zt', 'zif-true');
|
|
411
|
+
expect(document.querySelector('#zt p')).not.toBeNull();
|
|
412
|
+
expect(document.querySelector('#zt p').textContent).toBe('visible');
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it('removes element when condition is false', () => {
|
|
416
|
+
component('zif-false', {
|
|
417
|
+
state: () => ({ show: false }),
|
|
418
|
+
render() { return '<p z-if="show">hidden</p>'; },
|
|
419
|
+
});
|
|
420
|
+
document.body.innerHTML = '<zif-false id="zf"></zif-false>';
|
|
421
|
+
mount('#zf', 'zif-false');
|
|
422
|
+
expect(document.querySelector('#zf p')).toBeNull();
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it('supports z-else-if and z-else chain', () => {
|
|
426
|
+
component('zif-chain', {
|
|
427
|
+
state: () => ({ status: 'loading' }),
|
|
428
|
+
render() {
|
|
429
|
+
return `
|
|
430
|
+
<p z-if="status === 'ok'">OK</p>
|
|
431
|
+
<p z-else-if="status === 'loading'">Loading…</p>
|
|
432
|
+
<p z-else>Error</p>
|
|
433
|
+
`;
|
|
434
|
+
},
|
|
435
|
+
});
|
|
436
|
+
document.body.innerHTML = '<zif-chain id="zc"></zif-chain>';
|
|
437
|
+
mount('#zc', 'zif-chain');
|
|
438
|
+
const paras = document.querySelectorAll('#zc p');
|
|
439
|
+
expect(paras.length).toBe(1);
|
|
440
|
+
expect(paras[0].textContent).toBe('Loading…');
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
// ---------------------------------------------------------------------------
|
|
446
|
+
// z-show
|
|
447
|
+
// ---------------------------------------------------------------------------
|
|
448
|
+
|
|
449
|
+
describe('component — z-show directive', () => {
|
|
450
|
+
it('sets display none when falsy', () => {
|
|
451
|
+
component('zshow-hide', {
|
|
452
|
+
state: () => ({ visible: false }),
|
|
453
|
+
render() { return '<div z-show="visible">content</div>'; },
|
|
454
|
+
});
|
|
455
|
+
document.body.innerHTML = '<zshow-hide id="zsh"></zshow-hide>';
|
|
456
|
+
mount('#zsh', 'zshow-hide');
|
|
457
|
+
expect(document.querySelector('#zsh div').style.display).toBe('none');
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it('removes display none when truthy', () => {
|
|
461
|
+
component('zshow-show', {
|
|
462
|
+
state: () => ({ visible: true }),
|
|
463
|
+
render() { return '<div z-show="visible">content</div>'; },
|
|
464
|
+
});
|
|
465
|
+
document.body.innerHTML = '<zshow-show id="zss"></zshow-show>';
|
|
466
|
+
mount('#zss', 'zshow-show');
|
|
467
|
+
expect(document.querySelector('#zss div').style.display).toBe('');
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
// ---------------------------------------------------------------------------
|
|
473
|
+
// z-for
|
|
474
|
+
// ---------------------------------------------------------------------------
|
|
475
|
+
|
|
476
|
+
describe('component — z-for directive', () => {
|
|
477
|
+
it('renders list items', () => {
|
|
478
|
+
component('zfor-list', {
|
|
479
|
+
state: () => ({ items: ['red', 'green', 'blue'] }),
|
|
480
|
+
render() { return '<ul><li z-for="item in items">{{item}}</li></ul>'; },
|
|
481
|
+
});
|
|
482
|
+
document.body.innerHTML = '<zfor-list id="zfl"></zfor-list>';
|
|
483
|
+
mount('#zfl', 'zfor-list');
|
|
484
|
+
const lis = document.querySelectorAll('#zfl li');
|
|
485
|
+
expect(lis.length).toBe(3);
|
|
486
|
+
expect(lis[0].textContent).toBe('red');
|
|
487
|
+
expect(lis[1].textContent).toBe('green');
|
|
488
|
+
expect(lis[2].textContent).toBe('blue');
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it('supports (item, index) syntax', () => {
|
|
492
|
+
component('zfor-idx', {
|
|
493
|
+
state: () => ({ items: ['a', 'b'] }),
|
|
494
|
+
render() { return '<ul><li z-for="(item, i) in items">{{i}}-{{item}}</li></ul>'; },
|
|
495
|
+
});
|
|
496
|
+
document.body.innerHTML = '<zfor-idx id="zfi"></zfor-idx>';
|
|
497
|
+
mount('#zfi', 'zfor-idx');
|
|
498
|
+
const lis = document.querySelectorAll('#zfi li');
|
|
499
|
+
expect(lis.length).toBe(2);
|
|
500
|
+
expect(lis[0].textContent).toBe('0-a');
|
|
501
|
+
expect(lis[1].textContent).toBe('1-b');
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
it('supports numeric range', () => {
|
|
505
|
+
component('zfor-range', {
|
|
506
|
+
state: () => ({ count: 3 }),
|
|
507
|
+
render() { return '<span z-for="n in count">{{n}}</span>'; },
|
|
508
|
+
});
|
|
509
|
+
document.body.innerHTML = '<zfor-range id="zfr"></zfor-range>';
|
|
510
|
+
mount('#zfr', 'zfor-range');
|
|
511
|
+
const spans = document.querySelectorAll('#zfr span');
|
|
512
|
+
expect(spans.length).toBe(3);
|
|
513
|
+
expect(spans[0].textContent).toBe('1');
|
|
514
|
+
expect(spans[2].textContent).toBe('3');
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
it('renders empty when list is empty', () => {
|
|
518
|
+
component('zfor-empty', {
|
|
519
|
+
state: () => ({ items: [] }),
|
|
520
|
+
render() { return '<ul><li z-for="item in items">{{item}}</li></ul>'; },
|
|
521
|
+
});
|
|
522
|
+
document.body.innerHTML = '<zfor-empty id="zfe"></zfor-empty>';
|
|
523
|
+
mount('#zfe', 'zfor-empty');
|
|
524
|
+
expect(document.querySelectorAll('#zfe li').length).toBe(0);
|
|
525
|
+
});
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
// ---------------------------------------------------------------------------
|
|
530
|
+
// z-bind / :attr
|
|
531
|
+
// ---------------------------------------------------------------------------
|
|
532
|
+
|
|
533
|
+
describe('component — z-bind directive', () => {
|
|
534
|
+
it('binds attribute dynamically with :attr', () => {
|
|
535
|
+
component('zbind-attr', {
|
|
536
|
+
state: () => ({ cls: 'active' }),
|
|
537
|
+
render() { return '<div :class="cls">content</div>'; },
|
|
538
|
+
});
|
|
539
|
+
document.body.innerHTML = '<zbind-attr id="zba"></zbind-attr>';
|
|
540
|
+
mount('#zba', 'zbind-attr');
|
|
541
|
+
expect(document.querySelector('#zba div').className).toBe('active');
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it('removes attribute when value is false', () => {
|
|
545
|
+
component('zbind-false', {
|
|
546
|
+
state: () => ({ isDisabled: false }),
|
|
547
|
+
render() { return '<button :disabled="isDisabled">click</button>'; },
|
|
548
|
+
});
|
|
549
|
+
document.body.innerHTML = '<zbind-false id="zbf"></zbind-false>';
|
|
550
|
+
mount('#zbf', 'zbind-false');
|
|
551
|
+
expect(document.querySelector('#zbf button').hasAttribute('disabled')).toBe(false);
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
it('sets boolean attribute when true', () => {
|
|
555
|
+
component('zbind-true', {
|
|
556
|
+
state: () => ({ isDisabled: true }),
|
|
557
|
+
render() { return '<button :disabled="isDisabled">click</button>'; },
|
|
558
|
+
});
|
|
559
|
+
document.body.innerHTML = '<zbind-true id="zbt"></zbind-true>';
|
|
560
|
+
mount('#zbt', 'zbind-true');
|
|
561
|
+
expect(document.querySelector('#zbt button').hasAttribute('disabled')).toBe(true);
|
|
562
|
+
});
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
// ---------------------------------------------------------------------------
|
|
567
|
+
// z-class
|
|
568
|
+
// ---------------------------------------------------------------------------
|
|
569
|
+
|
|
570
|
+
describe('component — z-class directive', () => {
|
|
571
|
+
it('adds classes from object', () => {
|
|
572
|
+
component('zclass-obj', {
|
|
573
|
+
state: () => ({ isActive: true, isHidden: false }),
|
|
574
|
+
render() { return `<div z-class="{ active: isActive, hidden: isHidden }">test</div>`; },
|
|
575
|
+
});
|
|
576
|
+
document.body.innerHTML = '<zclass-obj id="zco"></zclass-obj>';
|
|
577
|
+
mount('#zco', 'zclass-obj');
|
|
578
|
+
const div = document.querySelector('#zco div');
|
|
579
|
+
expect(div.classList.contains('active')).toBe(true);
|
|
580
|
+
expect(div.classList.contains('hidden')).toBe(false);
|
|
581
|
+
});
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
// ---------------------------------------------------------------------------
|
|
586
|
+
// z-text
|
|
587
|
+
// ---------------------------------------------------------------------------
|
|
588
|
+
|
|
589
|
+
describe('component — z-text directive', () => {
|
|
590
|
+
it('sets textContent safely', () => {
|
|
591
|
+
component('ztext-test', {
|
|
592
|
+
state: () => ({ msg: 'Hello <b>world</b>' }),
|
|
593
|
+
render() { return '<span z-text="msg"></span>'; },
|
|
594
|
+
});
|
|
595
|
+
document.body.innerHTML = '<ztext-test id="ztt"></ztext-test>';
|
|
596
|
+
mount('#ztt', 'ztext-test');
|
|
597
|
+
const span = document.querySelector('#ztt span');
|
|
598
|
+
expect(span.textContent).toBe('Hello <b>world</b>');
|
|
599
|
+
expect(span.innerHTML).toBe('Hello <b>world</b>'); // XSS safe
|
|
600
|
+
});
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
// ---------------------------------------------------------------------------
|
|
605
|
+
// z-html
|
|
606
|
+
// ---------------------------------------------------------------------------
|
|
607
|
+
|
|
608
|
+
describe('component — z-html directive', () => {
|
|
609
|
+
it('sets innerHTML', () => {
|
|
610
|
+
component('zhtml-test', {
|
|
611
|
+
state: () => ({ content: '<strong>bold</strong>' }),
|
|
612
|
+
render() { return '<div z-html="content"></div>'; },
|
|
613
|
+
});
|
|
614
|
+
document.body.innerHTML = '<zhtml-test id="zht"></zhtml-test>';
|
|
615
|
+
mount('#zht', 'zhtml-test');
|
|
616
|
+
expect(document.querySelector('#zht strong').textContent).toBe('bold');
|
|
617
|
+
});
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
// ---------------------------------------------------------------------------
|
|
622
|
+
// z-ref
|
|
623
|
+
// ---------------------------------------------------------------------------
|
|
624
|
+
|
|
625
|
+
describe('component — z-ref', () => {
|
|
626
|
+
it('populates refs on mount', () => {
|
|
627
|
+
component('zref-test', {
|
|
628
|
+
render() { return '<input z-ref="myInput" type="text">'; },
|
|
629
|
+
});
|
|
630
|
+
document.body.innerHTML = '<zref-test id="zrt"></zref-test>';
|
|
631
|
+
const inst = mount('#zrt', 'zref-test');
|
|
632
|
+
expect(inst.refs.myInput).toBeTruthy();
|
|
633
|
+
expect(inst.refs.myInput.tagName).toBe('INPUT');
|
|
634
|
+
});
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
// ---------------------------------------------------------------------------
|
|
639
|
+
// z-model (two-way binding)
|
|
640
|
+
// ---------------------------------------------------------------------------
|
|
641
|
+
|
|
642
|
+
describe('component — z-model', () => {
|
|
643
|
+
it('syncs input value from state on mount', () => {
|
|
644
|
+
component('zmodel-init', {
|
|
645
|
+
state: () => ({ name: 'Tony' }),
|
|
646
|
+
render() { return '<input z-model="name">'; },
|
|
647
|
+
});
|
|
648
|
+
document.body.innerHTML = '<zmodel-init id="zmi"></zmodel-init>';
|
|
649
|
+
mount('#zmi', 'zmodel-init');
|
|
650
|
+
expect(document.querySelector('#zmi input').value).toBe('Tony');
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
it('writes input value back to state on input event', async () => {
|
|
654
|
+
component('zmodel-input', {
|
|
655
|
+
state: () => ({ name: '' }),
|
|
656
|
+
render() { return '<input z-model="name">'; },
|
|
657
|
+
});
|
|
658
|
+
document.body.innerHTML = '<zmodel-input id="zmip"></zmodel-input>';
|
|
659
|
+
const inst = mount('#zmip', 'zmodel-input');
|
|
660
|
+
const input = document.querySelector('#zmip input');
|
|
661
|
+
input.value = 'test';
|
|
662
|
+
input.dispatchEvent(new Event('input'));
|
|
663
|
+
expect(inst.state.name).toBe('test');
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
it('syncs checkbox checked state', () => {
|
|
667
|
+
component('zmodel-check', {
|
|
668
|
+
state: () => ({ agree: true }),
|
|
669
|
+
render() { return '<input type="checkbox" z-model="agree">'; },
|
|
670
|
+
});
|
|
671
|
+
document.body.innerHTML = '<zmodel-check id="zmc"></zmodel-check>';
|
|
672
|
+
mount('#zmc', 'zmodel-check');
|
|
673
|
+
expect(document.querySelector('#zmc input').checked).toBe(true);
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
it('writes checkbox change back to state', () => {
|
|
677
|
+
component('zmodel-check2', {
|
|
678
|
+
state: () => ({ agree: false }),
|
|
679
|
+
render() { return '<input type="checkbox" z-model="agree">'; },
|
|
680
|
+
});
|
|
681
|
+
document.body.innerHTML = '<zmodel-check2 id="zmc2"></zmodel-check2>';
|
|
682
|
+
const inst = mount('#zmc2', 'zmodel-check2');
|
|
683
|
+
const input = document.querySelector('#zmc2 input');
|
|
684
|
+
input.checked = true;
|
|
685
|
+
input.dispatchEvent(new Event('change'));
|
|
686
|
+
expect(inst.state.agree).toBe(true);
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
it('syncs textarea value', () => {
|
|
690
|
+
component('zmodel-textarea', {
|
|
691
|
+
state: () => ({ bio: 'Hello' }),
|
|
692
|
+
render() { return '<textarea z-model="bio"></textarea>'; },
|
|
693
|
+
});
|
|
694
|
+
document.body.innerHTML = '<zmodel-textarea id="zmta"></zmodel-textarea>';
|
|
695
|
+
mount('#zmta', 'zmodel-textarea');
|
|
696
|
+
expect(document.querySelector('#zmta textarea').value).toBe('Hello');
|
|
697
|
+
});
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
// ---------------------------------------------------------------------------
|
|
702
|
+
// z-cloak
|
|
703
|
+
// ---------------------------------------------------------------------------
|
|
704
|
+
|
|
705
|
+
describe('component — z-cloak', () => {
|
|
706
|
+
it('removes z-cloak attribute after render', () => {
|
|
707
|
+
component('zcloak-test', {
|
|
708
|
+
render() { return '<div z-cloak>content</div>'; },
|
|
709
|
+
});
|
|
710
|
+
document.body.innerHTML = '<zcloak-test id="zcl"></zcloak-test>';
|
|
711
|
+
mount('#zcl', 'zcloak-test');
|
|
712
|
+
expect(document.querySelector('#zcl div').hasAttribute('z-cloak')).toBe(false);
|
|
713
|
+
});
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
// ---------------------------------------------------------------------------
|
|
718
|
+
// z-pre
|
|
719
|
+
// ---------------------------------------------------------------------------
|
|
720
|
+
|
|
721
|
+
describe('component — z-pre', () => {
|
|
722
|
+
it('skips directive processing inside z-pre', () => {
|
|
723
|
+
component('zpre-test', {
|
|
724
|
+
state: () => ({ x: 42 }),
|
|
725
|
+
render() {
|
|
726
|
+
return '<div z-pre><span z-text="x"></span></div>';
|
|
727
|
+
},
|
|
728
|
+
});
|
|
729
|
+
document.body.innerHTML = '<zpre-test id="zpr"></zpre-test>';
|
|
730
|
+
mount('#zpr', 'zpre-test');
|
|
731
|
+
// z-text should NOT be processed inside z-pre
|
|
732
|
+
const span = document.querySelector('#zpr span');
|
|
733
|
+
expect(span.hasAttribute('z-text')).toBe(true);
|
|
734
|
+
expect(span.textContent).toBe('');
|
|
735
|
+
});
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
// ---------------------------------------------------------------------------
|
|
740
|
+
// z-style
|
|
741
|
+
// ---------------------------------------------------------------------------
|
|
742
|
+
|
|
743
|
+
describe('component — z-style directive', () => {
|
|
744
|
+
it('applies object styles', () => {
|
|
745
|
+
component('zstyle-obj', {
|
|
746
|
+
state: () => ({ color: 'red' }),
|
|
747
|
+
render() { return `<div z-style="{ color: color }">text</div>`; },
|
|
748
|
+
});
|
|
749
|
+
document.body.innerHTML = '<zstyle-obj id="zso"></zstyle-obj>';
|
|
750
|
+
mount('#zso', 'zstyle-obj');
|
|
751
|
+
expect(document.querySelector('#zso div').style.color).toBe('red');
|
|
752
|
+
});
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
// ---------------------------------------------------------------------------
|
|
757
|
+
// Event binding (@event)
|
|
758
|
+
// ---------------------------------------------------------------------------
|
|
759
|
+
|
|
760
|
+
describe('component — event binding', () => {
|
|
761
|
+
it('handles @click events', () => {
|
|
762
|
+
component('evt-click', {
|
|
763
|
+
state: () => ({ count: 0 }),
|
|
764
|
+
increment() { this.state.count++; },
|
|
765
|
+
render() { return '<button @click="increment">+</button><span class="v">' + this.state.count + '</span>'; },
|
|
766
|
+
});
|
|
767
|
+
document.body.innerHTML = '<evt-click id="ec"></evt-click>';
|
|
768
|
+
const inst = mount('#ec', 'evt-click');
|
|
769
|
+
document.querySelector('#ec button').click();
|
|
770
|
+
expect(inst.state.count).toBe(1);
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
it('passes $event argument', () => {
|
|
774
|
+
let receivedEvent = null;
|
|
775
|
+
component('evt-arg', {
|
|
776
|
+
handleClick(e) { receivedEvent = e; },
|
|
777
|
+
render() { return '<button @click="handleClick($event)">test</button>'; },
|
|
778
|
+
});
|
|
779
|
+
document.body.innerHTML = '<evt-arg id="ea"></evt-arg>';
|
|
780
|
+
mount('#ea', 'evt-arg');
|
|
781
|
+
document.querySelector('#ea button').click();
|
|
782
|
+
expect(receivedEvent).toBeInstanceOf(Event);
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
it('handles z-on:click syntax', () => {
|
|
786
|
+
component('evt-zon', {
|
|
787
|
+
state: () => ({ x: 0 }),
|
|
788
|
+
inc() { this.state.x++; },
|
|
789
|
+
render() { return '<button z-on:click="inc">+</button>'; },
|
|
790
|
+
});
|
|
791
|
+
document.body.innerHTML = '<evt-zon id="ez"></evt-zon>';
|
|
792
|
+
const inst = mount('#ez', 'evt-zon');
|
|
793
|
+
document.querySelector('#ez button').click();
|
|
794
|
+
expect(inst.state.x).toBe(1);
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
it('passes string and number arguments', () => {
|
|
798
|
+
let captured = [];
|
|
799
|
+
component('evt-args', {
|
|
800
|
+
handler(a, b) { captured = [a, b]; },
|
|
801
|
+
render() { return `<button @click="handler('hello', 42)">test</button>`; },
|
|
802
|
+
});
|
|
803
|
+
document.body.innerHTML = '<evt-args id="eag"></evt-args>';
|
|
804
|
+
mount('#eag', 'evt-args');
|
|
805
|
+
document.querySelector('#eag button').click();
|
|
806
|
+
expect(captured).toEqual(['hello', 42]);
|
|
807
|
+
});
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
// ---------------------------------------------------------------------------
|
|
812
|
+
// Watchers
|
|
813
|
+
// ---------------------------------------------------------------------------
|
|
814
|
+
|
|
815
|
+
describe('component — watchers', () => {
|
|
816
|
+
it('fires watcher when state key changes', async () => {
|
|
817
|
+
const watchFn = vi.fn();
|
|
818
|
+
component('watch-test', {
|
|
819
|
+
state: () => ({ count: 0 }),
|
|
820
|
+
watch: {
|
|
821
|
+
count: watchFn,
|
|
822
|
+
},
|
|
823
|
+
render() { return `<div>${this.state.count}</div>`; },
|
|
824
|
+
});
|
|
825
|
+
document.body.innerHTML = '<watch-test id="wt"></watch-test>';
|
|
826
|
+
const inst = mount('#wt', 'watch-test');
|
|
827
|
+
inst.state.count = 5;
|
|
828
|
+
expect(watchFn).toHaveBeenCalledWith(5, 0);
|
|
829
|
+
});
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
// ---------------------------------------------------------------------------
|
|
834
|
+
// Slots
|
|
835
|
+
// ---------------------------------------------------------------------------
|
|
836
|
+
|
|
837
|
+
describe('component — slots', () => {
|
|
838
|
+
it('distributes default slot content', () => {
|
|
839
|
+
component('slot-test', {
|
|
840
|
+
render() { return '<div><slot>fallback</slot></div>'; },
|
|
841
|
+
});
|
|
842
|
+
document.body.innerHTML = '<slot-test id="sl"><p>projected</p></slot-test>';
|
|
843
|
+
mount('#sl', 'slot-test');
|
|
844
|
+
expect(document.querySelector('#sl p').textContent).toBe('projected');
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
it('uses fallback when no content provided', () => {
|
|
848
|
+
component('slot-fallback', {
|
|
849
|
+
render() { return '<div><slot>fallback</slot></div>'; },
|
|
850
|
+
});
|
|
851
|
+
document.body.innerHTML = '<slot-fallback id="sf"></slot-fallback>';
|
|
852
|
+
mount('#sf', 'slot-fallback');
|
|
853
|
+
expect(document.querySelector('#sf div').textContent).toBe('fallback');
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
it('distributes named slots', () => {
|
|
857
|
+
component('slot-named', {
|
|
858
|
+
render() {
|
|
859
|
+
return '<header><slot name="header">default header</slot></header><main><slot>main content</slot></main>';
|
|
860
|
+
},
|
|
861
|
+
});
|
|
862
|
+
document.body.innerHTML = '<slot-named id="sn"><div slot="header">My Header</div><p>Body</p></slot-named>';
|
|
863
|
+
mount('#sn', 'slot-named');
|
|
864
|
+
expect(document.querySelector('#sn header').textContent).toBe('My Header');
|
|
865
|
+
expect(document.querySelector('#sn main').textContent).toBe('Body');
|
|
866
|
+
});
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
|
|
870
|
+
// ---------------------------------------------------------------------------
|
|
871
|
+
// Scoped styles
|
|
872
|
+
// ---------------------------------------------------------------------------
|
|
873
|
+
|
|
874
|
+
describe('component — scoped styles', () => {
|
|
875
|
+
it('injects scoped style tag', () => {
|
|
876
|
+
component('style-test', {
|
|
877
|
+
styles: '.box { color: red; }',
|
|
878
|
+
render() { return '<div class="box">styled</div>'; },
|
|
879
|
+
});
|
|
880
|
+
document.body.innerHTML = '<style-test id="st"></style-test>';
|
|
881
|
+
mount('#st', 'style-test');
|
|
882
|
+
const styleEl = document.querySelector('style[data-zq-component="style-test"]');
|
|
883
|
+
expect(styleEl).not.toBeNull();
|
|
884
|
+
expect(styleEl.textContent).toContain('color: red');
|
|
885
|
+
});
|
|
304
886
|
});
|