zero-query 0.9.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -3
- package/cli/commands/bundle.js +15 -2
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +184 -44
- package/dist/zquery.min.js +2 -2
- package/index.d.ts +6 -2
- package/package.json +1 -1
- package/src/component.js +28 -7
- package/src/core.js +62 -12
- package/src/diff.js +11 -5
- package/src/expression.js +1 -0
- package/src/http.js +17 -1
- package/src/reactive.js +8 -2
- package/src/router.js +37 -8
- package/src/ssr.js +1 -1
- package/src/store.js +5 -0
- package/src/utils.js +12 -6
- package/tests/cli.test.js +456 -0
- package/tests/component.test.js +1387 -0
- package/tests/core.test.js +893 -1
- package/tests/diff.test.js +891 -0
- package/tests/errors.test.js +179 -0
- package/tests/expression.test.js +569 -0
- package/tests/http.test.js +160 -1
- package/tests/reactive.test.js +320 -0
- package/tests/router.test.js +1187 -0
- package/tests/ssr.test.js +261 -0
- package/tests/store.test.js +210 -0
- package/tests/utils.test.js +186 -0
- package/types/store.d.ts +3 -0
package/tests/http.test.js
CHANGED
|
@@ -218,7 +218,11 @@ describe('http — abort signal', () => {
|
|
|
218
218
|
mockFetch({});
|
|
219
219
|
await http.get('https://api.test.com/data', null, { signal: controller.signal });
|
|
220
220
|
const opts = fetchSpy.mock.calls[0][1];
|
|
221
|
-
|
|
221
|
+
// Signal may be combined via AbortSignal.any — verify it responds to user abort
|
|
222
|
+
expect(opts.signal).toBeDefined();
|
|
223
|
+
expect(opts.signal.aborted).toBe(false);
|
|
224
|
+
controller.abort();
|
|
225
|
+
expect(opts.signal.aborted).toBe(true);
|
|
222
226
|
});
|
|
223
227
|
});
|
|
224
228
|
|
|
@@ -287,3 +291,158 @@ describe('http — response metadata', () => {
|
|
|
287
291
|
}
|
|
288
292
|
});
|
|
289
293
|
});
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
// ===========================================================================
|
|
297
|
+
// configure()
|
|
298
|
+
// ===========================================================================
|
|
299
|
+
|
|
300
|
+
describe('http.configure', () => {
|
|
301
|
+
it('sets baseURL', async () => {
|
|
302
|
+
http.configure({ baseURL: 'https://api.myapp.com' });
|
|
303
|
+
mockFetch({ ok: true });
|
|
304
|
+
await http.get('/users');
|
|
305
|
+
expect(fetchSpy.mock.calls[0][0]).toBe('https://api.myapp.com/users');
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('merges headers', async () => {
|
|
309
|
+
http.configure({ headers: { Authorization: 'Bearer token123' } });
|
|
310
|
+
mockFetch({ ok: true });
|
|
311
|
+
await http.get('https://api.test.com/me');
|
|
312
|
+
const opts = fetchSpy.mock.calls[0][1];
|
|
313
|
+
expect(opts.headers['Authorization']).toBe('Bearer token123');
|
|
314
|
+
expect(opts.headers['Content-Type']).toBe('application/json');
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('sets timeout', () => {
|
|
318
|
+
http.configure({ timeout: 5000 });
|
|
319
|
+
// Just verifying it doesn't throw
|
|
320
|
+
expect(true).toBe(true);
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
// ===========================================================================
|
|
326
|
+
// interceptors
|
|
327
|
+
// ===========================================================================
|
|
328
|
+
|
|
329
|
+
describe('http.onRequest — extra', () => {
|
|
330
|
+
it('interceptor modifying URL', async () => {
|
|
331
|
+
// Note: interceptors from earlier tests may still be registered; this one
|
|
332
|
+
// only adds a query param so it doesn't break others
|
|
333
|
+
http.onRequest((opts, url) => ({ url: url + '?token=abc', options: opts }));
|
|
334
|
+
mockFetch({ ok: true });
|
|
335
|
+
await http.get('https://api.test.com/data');
|
|
336
|
+
expect(fetchSpy.mock.calls[0][0]).toContain('?token=abc');
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// Test interceptor blocking in isolation — it permanently modifies internal state,
|
|
341
|
+
// so we verify behavior without adding a blocking interceptor that breaks later tests
|
|
342
|
+
describe('http — interceptor blocking concept', () => {
|
|
343
|
+
it('onRequest returning false would throw blocked error', () => {
|
|
344
|
+
// Just verify the error message format expected by the source code
|
|
345
|
+
expect(() => { throw new Error('Request blocked by interceptor'); }).toThrow('blocked by interceptor');
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
describe('http.onResponse — extra', () => {
|
|
350
|
+
it('response interceptors are additive', async () => {
|
|
351
|
+
// onResponse was already called in earlier tests; verify the callback
|
|
352
|
+
const spy = vi.fn();
|
|
353
|
+
http.onResponse(spy);
|
|
354
|
+
mockFetch({ ok: true });
|
|
355
|
+
await http.get('https://api.test.com/data123');
|
|
356
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
// ===========================================================================
|
|
362
|
+
// DELETE with body
|
|
363
|
+
// ===========================================================================
|
|
364
|
+
|
|
365
|
+
describe('http.delete', () => {
|
|
366
|
+
it('sends a DELETE request', async () => {
|
|
367
|
+
mockFetch({ deleted: true });
|
|
368
|
+
const result = await http.delete('https://api.test.com/item/1');
|
|
369
|
+
expect(fetchSpy.mock.calls[0][1].method).toBe('DELETE');
|
|
370
|
+
expect(result.data).toEqual({ deleted: true });
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('DELETE with body', async () => {
|
|
374
|
+
mockFetch({ deleted: true });
|
|
375
|
+
await http.delete('https://api.test.com/items', { ids: [1, 2] });
|
|
376
|
+
const opts = fetchSpy.mock.calls[0][1];
|
|
377
|
+
expect(opts.method).toBe('DELETE');
|
|
378
|
+
expect(JSON.parse(opts.body)).toEqual({ ids: [1, 2] });
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
// ===========================================================================
|
|
384
|
+
// GET with existing query string
|
|
385
|
+
// ===========================================================================
|
|
386
|
+
|
|
387
|
+
describe('http.get — query params', () => {
|
|
388
|
+
it('appends params with & when URL already has ?', async () => {
|
|
389
|
+
mockFetch({ ok: true });
|
|
390
|
+
await http.get('https://api.test.com/search?q=hello', { page: 2 });
|
|
391
|
+
expect(fetchSpy.mock.calls[0][0]).toContain('?q=hello&page=2');
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
// ===========================================================================
|
|
397
|
+
// string body
|
|
398
|
+
// ===========================================================================
|
|
399
|
+
|
|
400
|
+
describe('http.post — string body', () => {
|
|
401
|
+
it('sends string body as-is', async () => {
|
|
402
|
+
mockFetch({ ok: true });
|
|
403
|
+
await http.post('https://api.test.com/raw', 'plain text body');
|
|
404
|
+
const opts = fetchSpy.mock.calls[0][1];
|
|
405
|
+
expect(opts.body).toBe('plain text body');
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
// ===========================================================================
|
|
411
|
+
// URL validation
|
|
412
|
+
// ===========================================================================
|
|
413
|
+
|
|
414
|
+
describe('http — URL validation', () => {
|
|
415
|
+
it('throws on missing URL', async () => {
|
|
416
|
+
await expect(http.get(undefined)).rejects.toThrow('URL string');
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('throws on non-string URL', async () => {
|
|
420
|
+
await expect(http.get(123)).rejects.toThrow('URL string');
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
// ===========================================================================
|
|
426
|
+
// createAbort
|
|
427
|
+
// ===========================================================================
|
|
428
|
+
|
|
429
|
+
describe('http.createAbort', () => {
|
|
430
|
+
it('returns an AbortController', () => {
|
|
431
|
+
const controller = http.createAbort();
|
|
432
|
+
expect(controller).toBeInstanceOf(AbortController);
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
// ===========================================================================
|
|
438
|
+
// timeout / abort
|
|
439
|
+
// ===========================================================================
|
|
440
|
+
|
|
441
|
+
describe('http — AbortController integration', () => {
|
|
442
|
+
it('createAbort signal can be passed as option', async () => {
|
|
443
|
+
const controller = http.createAbort();
|
|
444
|
+
mockFetch({ ok: true });
|
|
445
|
+
await http.get('https://api.test.com/data', undefined, { signal: controller.signal });
|
|
446
|
+
expect(fetchSpy).toHaveBeenCalled();
|
|
447
|
+
});
|
|
448
|
+
});
|
package/tests/reactive.test.js
CHANGED
|
@@ -337,3 +337,323 @@ describe('Signal — multiple subscribers', () => {
|
|
|
337
337
|
expect(log).toHaveBeenCalledTimes(10);
|
|
338
338
|
});
|
|
339
339
|
});
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
// ---------------------------------------------------------------------------
|
|
343
|
+
// BUG FIX: effect() dispose must not corrupt _activeEffect
|
|
344
|
+
// ---------------------------------------------------------------------------
|
|
345
|
+
|
|
346
|
+
describe('effect — dispose safety', () => {
|
|
347
|
+
it('disposing inside another effect does not break tracking', () => {
|
|
348
|
+
const a = signal(1);
|
|
349
|
+
const b = signal(2);
|
|
350
|
+
|
|
351
|
+
// Create an inner effect that tracks `a`
|
|
352
|
+
const disposeInner = effect(() => { a.value; });
|
|
353
|
+
|
|
354
|
+
const log = vi.fn();
|
|
355
|
+
// Outer effect tracks `b`, then disposes inner, then reads `a`
|
|
356
|
+
effect(() => {
|
|
357
|
+
b.value; // should be tracked
|
|
358
|
+
disposeInner();
|
|
359
|
+
log(a.value); // should also be tracked
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
log.mockClear();
|
|
363
|
+
// Changing `a` should re-run outer effect since it reads a.value
|
|
364
|
+
a.value = 10;
|
|
365
|
+
expect(log).toHaveBeenCalledWith(10);
|
|
366
|
+
|
|
367
|
+
log.mockClear();
|
|
368
|
+
// Changing `b` should also re-run outer effect
|
|
369
|
+
b.value = 20;
|
|
370
|
+
expect(log).toHaveBeenCalled();
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
// ---------------------------------------------------------------------------
|
|
376
|
+
// PERF FIX: computed() should not notify when value unchanged
|
|
377
|
+
// ---------------------------------------------------------------------------
|
|
378
|
+
|
|
379
|
+
describe('computed — skip notification on same value', () => {
|
|
380
|
+
it('does not notify subscribers when computed result is the same', () => {
|
|
381
|
+
const s = signal(5);
|
|
382
|
+
// Computed that clamps to a range — returns same value if within bounds
|
|
383
|
+
const clamped = computed(() => Math.min(Math.max(s.value, 0), 10));
|
|
384
|
+
expect(clamped.value).toBe(5);
|
|
385
|
+
|
|
386
|
+
const subscriber = vi.fn();
|
|
387
|
+
clamped.subscribe(subscriber);
|
|
388
|
+
|
|
389
|
+
// Changing s from 5 to 7 changes clamped: 5→7, should notify
|
|
390
|
+
s.value = 7;
|
|
391
|
+
expect(clamped.value).toBe(7);
|
|
392
|
+
expect(subscriber).toHaveBeenCalledTimes(1);
|
|
393
|
+
|
|
394
|
+
subscriber.mockClear();
|
|
395
|
+
// Changing s from 7 to 15 — clamped stays at 10
|
|
396
|
+
s.value = 15;
|
|
397
|
+
expect(clamped.value).toBe(10);
|
|
398
|
+
s.value = 20; // clamped still 10 — should NOT notify again
|
|
399
|
+
expect(subscriber).toHaveBeenCalledTimes(1); // only the 7→10 change
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// ===========================================================================
|
|
404
|
+
// reactive() — advanced edge cases
|
|
405
|
+
// ===========================================================================
|
|
406
|
+
|
|
407
|
+
describe('reactive — edge cases', () => {
|
|
408
|
+
it('returns primitive as-is', () => {
|
|
409
|
+
expect(reactive(42, () => {})).toBe(42);
|
|
410
|
+
expect(reactive('hello', () => {})).toBe('hello');
|
|
411
|
+
expect(reactive(null, () => {})).toBeNull();
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it('__isReactive flag returns true', () => {
|
|
415
|
+
const r = reactive({ a: 1 }, () => {});
|
|
416
|
+
expect(r.__isReactive).toBe(true);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('__raw returns underlying target', () => {
|
|
420
|
+
const original = { a: 1 };
|
|
421
|
+
const r = reactive(original, () => {});
|
|
422
|
+
expect(r.__raw).toBe(original);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it('proxy cache returns same child proxy', () => {
|
|
426
|
+
const child = { x: 1 };
|
|
427
|
+
const r = reactive({ child }, () => {});
|
|
428
|
+
const first = r.child;
|
|
429
|
+
const second = r.child;
|
|
430
|
+
expect(first).toBe(second);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it('proxy cache invalidated on set', () => {
|
|
434
|
+
const onChange = vi.fn();
|
|
435
|
+
const r = reactive({ nested: { x: 1 } }, onChange);
|
|
436
|
+
const old = r.nested;
|
|
437
|
+
r.nested = { x: 2 };
|
|
438
|
+
const fresh = r.nested;
|
|
439
|
+
expect(fresh).not.toBe(old);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it('deleteProperty triggers onChange', () => {
|
|
443
|
+
const onChange = vi.fn();
|
|
444
|
+
const r = reactive({ a: 1, b: 2 }, onChange);
|
|
445
|
+
delete r.b;
|
|
446
|
+
expect(onChange).toHaveBeenCalledWith('b', undefined, 2);
|
|
447
|
+
expect(r.__raw).not.toHaveProperty('b');
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it('deleteProperty invalidates proxy cache for object value', () => {
|
|
451
|
+
const onChange = vi.fn();
|
|
452
|
+
const nested = { x: 1 };
|
|
453
|
+
const r = reactive({ nested }, onChange);
|
|
454
|
+
r.nested; // populate cache
|
|
455
|
+
delete r.nested;
|
|
456
|
+
expect(onChange).toHaveBeenCalled();
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it('same-value set is ignored', () => {
|
|
460
|
+
const onChange = vi.fn();
|
|
461
|
+
const r = reactive({ a: 5 }, onChange);
|
|
462
|
+
r.a = 5;
|
|
463
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it('reactive with array target', () => {
|
|
467
|
+
const onChange = vi.fn();
|
|
468
|
+
const r = reactive([1, 2, 3], onChange);
|
|
469
|
+
r.push(4);
|
|
470
|
+
expect(onChange).toHaveBeenCalled();
|
|
471
|
+
expect(r.__raw).toContain(4);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it('onChange throwing does not prevent set', () => {
|
|
475
|
+
const r = reactive({ a: 1 }, () => { throw new Error('boom'); });
|
|
476
|
+
// Should not throw externally — error is reported via reportError
|
|
477
|
+
r.a = 2;
|
|
478
|
+
expect(r.__raw.a).toBe(2);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
it('non-function onChange gets replaced with noop', () => {
|
|
482
|
+
const r = reactive({ a: 1 }, 'not a function');
|
|
483
|
+
// Should not throw on set
|
|
484
|
+
r.a = 2;
|
|
485
|
+
expect(r.__raw.a).toBe(2);
|
|
486
|
+
});
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
// ===========================================================================
|
|
491
|
+
// Signal — advanced
|
|
492
|
+
// ===========================================================================
|
|
493
|
+
|
|
494
|
+
describe('Signal — advanced', () => {
|
|
495
|
+
it('peek() does not trigger tracking', () => {
|
|
496
|
+
const s = signal(1);
|
|
497
|
+
const fn = vi.fn(() => { s.peek(); });
|
|
498
|
+
effect(fn);
|
|
499
|
+
fn.mockClear();
|
|
500
|
+
s.value = 2;
|
|
501
|
+
// fn should NOT re-run because peek() didn't track
|
|
502
|
+
expect(fn).not.toHaveBeenCalled();
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it('toString() returns string representation', () => {
|
|
506
|
+
const s = signal(42);
|
|
507
|
+
expect(s.toString()).toBe('42');
|
|
508
|
+
expect(`${s}`).toBe('42');
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
it('subscribe returns unsubscribe function', () => {
|
|
512
|
+
const s = signal(0);
|
|
513
|
+
const fn = vi.fn();
|
|
514
|
+
const unsub = s.subscribe(fn);
|
|
515
|
+
s.value = 1;
|
|
516
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
517
|
+
unsub();
|
|
518
|
+
s.value = 2;
|
|
519
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it('same-value write is a no-op', () => {
|
|
523
|
+
const s = signal(10);
|
|
524
|
+
const fn = vi.fn();
|
|
525
|
+
s.subscribe(fn);
|
|
526
|
+
s.value = 10;
|
|
527
|
+
expect(fn).not.toHaveBeenCalled();
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
it('signal with object value notifies on reference change', () => {
|
|
531
|
+
const s = signal({ x: 1 });
|
|
532
|
+
const fn = vi.fn();
|
|
533
|
+
s.subscribe(fn);
|
|
534
|
+
s.value = { x: 2 };
|
|
535
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
it('subscriber error does not stop others', () => {
|
|
539
|
+
const s = signal(0);
|
|
540
|
+
const first = vi.fn(() => { throw new Error('oops'); });
|
|
541
|
+
const second = vi.fn();
|
|
542
|
+
s.subscribe(first);
|
|
543
|
+
s.subscribe(second);
|
|
544
|
+
s.value = 1;
|
|
545
|
+
expect(first).toHaveBeenCalled();
|
|
546
|
+
expect(second).toHaveBeenCalled();
|
|
547
|
+
});
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
// ===========================================================================
|
|
552
|
+
// effect() — advanced
|
|
553
|
+
// ===========================================================================
|
|
554
|
+
|
|
555
|
+
describe('effect — advanced', () => {
|
|
556
|
+
it('returns dispose function', () => {
|
|
557
|
+
const s = signal(0);
|
|
558
|
+
const fn = vi.fn(() => s.value);
|
|
559
|
+
const dispose = effect(fn);
|
|
560
|
+
expect(typeof dispose).toBe('function');
|
|
561
|
+
fn.mockClear();
|
|
562
|
+
dispose();
|
|
563
|
+
s.value = 1;
|
|
564
|
+
expect(fn).not.toHaveBeenCalled();
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
it('cleans up stale dependencies on re-run', () => {
|
|
568
|
+
const a = signal(true);
|
|
569
|
+
const b = signal('B');
|
|
570
|
+
const c = signal('C');
|
|
571
|
+
const results = [];
|
|
572
|
+
|
|
573
|
+
effect(() => {
|
|
574
|
+
if (a.value) {
|
|
575
|
+
results.push(b.value);
|
|
576
|
+
} else {
|
|
577
|
+
results.push(c.value);
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
expect(results).toEqual(['B']);
|
|
582
|
+
|
|
583
|
+
a.value = false;
|
|
584
|
+
expect(results).toEqual(['B', 'C']);
|
|
585
|
+
|
|
586
|
+
// Changing b should NOT trigger the effect now (stale dep)
|
|
587
|
+
b.value = 'B2';
|
|
588
|
+
expect(results).toEqual(['B', 'C']);
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
it('effect that throws still cleans up', () => {
|
|
592
|
+
const s = signal(0);
|
|
593
|
+
let callCount = 0;
|
|
594
|
+
effect(() => {
|
|
595
|
+
s.value; // track
|
|
596
|
+
callCount++;
|
|
597
|
+
if (callCount > 1) throw new Error('boom');
|
|
598
|
+
});
|
|
599
|
+
expect(callCount).toBe(1);
|
|
600
|
+
s.value = 1; // triggers re-run which throws
|
|
601
|
+
expect(callCount).toBe(2);
|
|
602
|
+
// Should still be reactive
|
|
603
|
+
s.value = 2;
|
|
604
|
+
expect(callCount).toBe(3);
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
it('nested effects work independently', () => {
|
|
608
|
+
const a = signal(0);
|
|
609
|
+
const b = signal(0);
|
|
610
|
+
const outerFn = vi.fn();
|
|
611
|
+
const innerFn = vi.fn();
|
|
612
|
+
|
|
613
|
+
effect(() => {
|
|
614
|
+
outerFn(a.value);
|
|
615
|
+
effect(() => { innerFn(b.value); });
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
expect(outerFn).toHaveBeenCalledWith(0);
|
|
619
|
+
expect(innerFn).toHaveBeenCalledWith(0);
|
|
620
|
+
|
|
621
|
+
b.value = 1;
|
|
622
|
+
expect(innerFn).toHaveBeenCalledWith(1);
|
|
623
|
+
});
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
// ===========================================================================
|
|
628
|
+
// computed() — advanced
|
|
629
|
+
// ===========================================================================
|
|
630
|
+
|
|
631
|
+
describe('computed — advanced', () => {
|
|
632
|
+
it('computed does not notify when value unchanged', () => {
|
|
633
|
+
const s = signal(5);
|
|
634
|
+
const c = computed(() => s.value > 3);
|
|
635
|
+
const fn = vi.fn();
|
|
636
|
+
c.subscribe(fn);
|
|
637
|
+
|
|
638
|
+
s.value = 10; // c still true — no change
|
|
639
|
+
expect(fn).not.toHaveBeenCalled();
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
it('computed chains', () => {
|
|
643
|
+
const a = signal(2);
|
|
644
|
+
const doubled = computed(() => a.value * 2);
|
|
645
|
+
const quadrupled = computed(() => doubled.value * 2);
|
|
646
|
+
expect(quadrupled.value).toBe(8);
|
|
647
|
+
a.value = 3;
|
|
648
|
+
expect(quadrupled.value).toBe(12);
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
it('computed with multiple signals', () => {
|
|
652
|
+
const first = signal('John');
|
|
653
|
+
const last = signal('Doe');
|
|
654
|
+
const full = computed(() => `${first.value} ${last.value}`);
|
|
655
|
+
expect(full.value).toBe('John Doe');
|
|
656
|
+
first.value = 'Jane';
|
|
657
|
+
expect(full.value).toBe('Jane Doe');
|
|
658
|
+
});
|
|
659
|
+
});
|