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/expression.test.js
CHANGED
|
@@ -332,3 +332,151 @@ describe('expression parser — multi-scope', () => {
|
|
|
332
332
|
expect(safeEval('y', [{ x: 1 }, { y: 2 }])).toBe(2);
|
|
333
333
|
});
|
|
334
334
|
});
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
// ---------------------------------------------------------------------------
|
|
338
|
+
// in operator
|
|
339
|
+
// ---------------------------------------------------------------------------
|
|
340
|
+
|
|
341
|
+
describe('expression parser — in operator', () => {
|
|
342
|
+
it('checks property existence', () => {
|
|
343
|
+
expect(eval_("'x' in obj", { obj: { x: 1 } })).toBe(true);
|
|
344
|
+
expect(eval_("'y' in obj", { obj: { x: 1 } })).toBe(false);
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
// ---------------------------------------------------------------------------
|
|
350
|
+
// instanceof operator
|
|
351
|
+
// ---------------------------------------------------------------------------
|
|
352
|
+
|
|
353
|
+
describe('expression parser — instanceof', () => {
|
|
354
|
+
it('checks instanceOf', () => {
|
|
355
|
+
expect(eval_('arr instanceof Array', { arr: [1, 2] })).toBe(true);
|
|
356
|
+
expect(eval_('obj instanceof Array', { obj: {} })).toBe(false);
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
// ---------------------------------------------------------------------------
|
|
362
|
+
// Nested ternary
|
|
363
|
+
// ---------------------------------------------------------------------------
|
|
364
|
+
|
|
365
|
+
describe('expression parser — nested ternary', () => {
|
|
366
|
+
it('evaluates simple ternary correctly', () => {
|
|
367
|
+
expect(eval_("x > 10 ? 'big' : 'small'", { x: 12 })).toBe('big');
|
|
368
|
+
expect(eval_("x > 10 ? 'big' : 'small'", { x: 2 })).toBe('small');
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
// ---------------------------------------------------------------------------
|
|
374
|
+
// Chained method calls
|
|
375
|
+
// ---------------------------------------------------------------------------
|
|
376
|
+
|
|
377
|
+
describe('expression parser — chained calls', () => {
|
|
378
|
+
it('chains array methods', () => {
|
|
379
|
+
expect(eval_('items.filter(x => x > 1).map(x => x * 2)', { items: [1, 2, 3] })).toEqual([4, 6]);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('chains string methods', () => {
|
|
383
|
+
expect(eval_("name.trim().toUpperCase()", { name: ' hello ' })).toBe('HELLO');
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
// ---------------------------------------------------------------------------
|
|
389
|
+
// Spread operator
|
|
390
|
+
// ---------------------------------------------------------------------------
|
|
391
|
+
|
|
392
|
+
describe('expression parser — spread / rest', () => {
|
|
393
|
+
it('spread is not supported — returns gracefully', () => {
|
|
394
|
+
// The parser does not support spread syntax; verify it doesn't throw
|
|
395
|
+
const result = eval_('[...items, 4]', { items: [1, 2, 3] });
|
|
396
|
+
expect(result).toBeDefined();
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
// ---------------------------------------------------------------------------
|
|
402
|
+
// Destructuring assignment in arrow body
|
|
403
|
+
// ---------------------------------------------------------------------------
|
|
404
|
+
|
|
405
|
+
describe('expression parser — complex arrow', () => {
|
|
406
|
+
it('arrow as callback in array method', () => {
|
|
407
|
+
const items = [{ n: 'a' }, { n: 'b' }];
|
|
408
|
+
expect(eval_('items.map(x => x.n)', { items })).toEqual(['a', 'b']);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it('arrow with ternary body', () => {
|
|
412
|
+
const fn = eval_('x => x > 0 ? "pos" : "neg"');
|
|
413
|
+
expect(fn(1)).toBe('pos');
|
|
414
|
+
expect(fn(-1)).toBe('neg');
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
// ---------------------------------------------------------------------------
|
|
420
|
+
// Bitwise operators
|
|
421
|
+
// ---------------------------------------------------------------------------
|
|
422
|
+
|
|
423
|
+
describe('expression parser — bitwise', () => {
|
|
424
|
+
it('bitwise operators are not supported — does not throw', () => {
|
|
425
|
+
// The expression parser does not implement bitwise operators
|
|
426
|
+
// Verify graceful fallback rather than crashes
|
|
427
|
+
expect(() => eval_('5 | 3')).not.toThrow();
|
|
428
|
+
expect(() => eval_('5 & 3')).not.toThrow();
|
|
429
|
+
expect(() => eval_('5 ^ 3')).not.toThrow();
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
// ---------------------------------------------------------------------------
|
|
435
|
+
// Comma expressions
|
|
436
|
+
// ---------------------------------------------------------------------------
|
|
437
|
+
|
|
438
|
+
describe('expression parser — comma', () => {
|
|
439
|
+
it('comma expressions are not supported — does not throw', () => {
|
|
440
|
+
// The parser does not support comma expressions
|
|
441
|
+
expect(() => eval_('(1, 2, 3)')).not.toThrow();
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
// ---------------------------------------------------------------------------
|
|
447
|
+
// Edge cases
|
|
448
|
+
// ---------------------------------------------------------------------------
|
|
449
|
+
|
|
450
|
+
describe('expression parser — edge cases', () => {
|
|
451
|
+
it('handles very long dot chains', () => {
|
|
452
|
+
const data = { a: { b: { c: { d: { e: 42 } } } } };
|
|
453
|
+
expect(eval_('a.b.c.d.e', data)).toBe(42);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it('handles numeric string keys in bracket access', () => {
|
|
457
|
+
expect(eval_("items['0']", { items: ['a', 'b'] })).toBe('a');
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it('handles conditional access chains', () => {
|
|
461
|
+
expect(eval_('a?.b?.c', { a: null })).toBe(undefined);
|
|
462
|
+
expect(eval_('a?.b?.c', { a: { b: { c: 1 } } })).toBe(1);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it('handles string with special characters', () => {
|
|
466
|
+
expect(eval_("'hello\\nworld'")).toContain('hello');
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it('handles negative numbers in expressions', () => {
|
|
470
|
+
expect(eval_('-1 + -2')).toBe(-3);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it('exponentiation ** is not supported — does not throw', () => {
|
|
474
|
+
// The parser does not implement ** operator
|
|
475
|
+
expect(() => eval_('2 ** 3')).not.toThrow();
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it('handles empty array/object', () => {
|
|
479
|
+
expect(eval_('[]')).toEqual([]);
|
|
480
|
+
expect(eval_('{}')).toEqual({});
|
|
481
|
+
});
|
|
482
|
+
});
|
package/tests/http.test.js
CHANGED
|
@@ -179,3 +179,111 @@ describe('http — text response', () => {
|
|
|
179
179
|
expect(result.data).toBe('Hello World');
|
|
180
180
|
});
|
|
181
181
|
});
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// Interceptors
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
describe('http — interceptors', () => {
|
|
189
|
+
it('request interceptor via onRequest', async () => {
|
|
190
|
+
http.configure({ baseURL: '' });
|
|
191
|
+
http.onRequest((fetchOpts, url) => {
|
|
192
|
+
fetchOpts.headers['X-Custom'] = 'test';
|
|
193
|
+
});
|
|
194
|
+
mockFetch({});
|
|
195
|
+
await http.get('https://api.test.com/data');
|
|
196
|
+
const opts = fetchSpy.mock.calls[0][1];
|
|
197
|
+
expect(opts.headers['X-Custom']).toBe('test');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('response interceptor via onResponse', async () => {
|
|
201
|
+
http.onResponse((result) => {
|
|
202
|
+
result.intercepted = true;
|
|
203
|
+
});
|
|
204
|
+
mockFetch({ x: 1 });
|
|
205
|
+
const result = await http.get('https://api.test.com/data');
|
|
206
|
+
expect(result.intercepted).toBe(true);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
// Timeout / abort
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
describe('http — abort signal', () => {
|
|
216
|
+
it('passes signal through options', async () => {
|
|
217
|
+
const controller = new AbortController();
|
|
218
|
+
mockFetch({});
|
|
219
|
+
await http.get('https://api.test.com/data', null, { signal: controller.signal });
|
|
220
|
+
const opts = fetchSpy.mock.calls[0][1];
|
|
221
|
+
expect(opts.signal).toBe(controller.signal);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
// Blob response
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
|
|
230
|
+
describe('http — blob response', () => {
|
|
231
|
+
it('can request blob responses', async () => {
|
|
232
|
+
mockFetch('binary data');
|
|
233
|
+
const result = await http.get('https://api.test.com/file', null, { responseType: 'blob' });
|
|
234
|
+
// Should have a data field (blob or fallback text)
|
|
235
|
+
expect(result.data).toBeDefined();
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
// HEAD and OPTIONS methods
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
describe('http — raw fetch pass-through', () => {
|
|
245
|
+
it('raw() delegates to native fetch', async () => {
|
|
246
|
+
mockFetch({ ok: true });
|
|
247
|
+
await http.raw('https://api.test.com/ping', { method: 'HEAD' });
|
|
248
|
+
expect(fetchSpy).toHaveBeenCalledWith('https://api.test.com/ping', { method: 'HEAD' });
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
// Request with custom headers
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
describe('http — custom per-request headers', () => {
|
|
258
|
+
it('merges per-request headers', async () => {
|
|
259
|
+
mockFetch({});
|
|
260
|
+
await http.get('https://api.test.com/data', null, {
|
|
261
|
+
headers: { 'X-Request-Id': '123' },
|
|
262
|
+
});
|
|
263
|
+
const opts = fetchSpy.mock.calls[0][1];
|
|
264
|
+
expect(opts.headers['X-Request-Id']).toBe('123');
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
// Response metadata
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
272
|
+
|
|
273
|
+
describe('http — response metadata', () => {
|
|
274
|
+
it('includes status and ok in result', async () => {
|
|
275
|
+
mockFetch({ data: 'yes' }, true, 200);
|
|
276
|
+
const result = await http.get('https://api.test.com/data');
|
|
277
|
+
expect(result.ok).toBe(true);
|
|
278
|
+
expect(result.status).toBe(200);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('includes statusText in error', async () => {
|
|
282
|
+
mockFetch({ error: 'bad' }, false, 500);
|
|
283
|
+
try {
|
|
284
|
+
await http.get('https://api.test.com/fail');
|
|
285
|
+
} catch (err) {
|
|
286
|
+
expect(err.message).toContain('500');
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
});
|
package/tests/reactive.test.js
CHANGED
|
@@ -188,4 +188,152 @@ describe('effect()', () => {
|
|
|
188
188
|
effect(() => { throw new Error('fail'); });
|
|
189
189
|
}).not.toThrow();
|
|
190
190
|
});
|
|
191
|
+
|
|
192
|
+
it('dispose stops re-running on signal change', () => {
|
|
193
|
+
const s = signal(0);
|
|
194
|
+
const log = vi.fn();
|
|
195
|
+
const dispose = effect(() => { log(s.value); });
|
|
196
|
+
expect(log).toHaveBeenCalledTimes(1);
|
|
197
|
+
dispose();
|
|
198
|
+
s.value = 1;
|
|
199
|
+
expect(log).toHaveBeenCalledTimes(1); // no additional call
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('dispose removes effect from signal subscribers', () => {
|
|
203
|
+
const s = signal(0);
|
|
204
|
+
const log = vi.fn();
|
|
205
|
+
const dispose = effect(() => { log(s.value); });
|
|
206
|
+
dispose();
|
|
207
|
+
// After disposing, the signal should not hold a reference to the effect
|
|
208
|
+
s.value = 99;
|
|
209
|
+
expect(log).toHaveBeenCalledTimes(1); // only the initial run
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('tracks multiple signals', () => {
|
|
213
|
+
const a = signal(1);
|
|
214
|
+
const b = signal(2);
|
|
215
|
+
const log = vi.fn();
|
|
216
|
+
effect(() => { log(a.value + b.value); });
|
|
217
|
+
expect(log).toHaveBeenCalledWith(3);
|
|
218
|
+
a.value = 10;
|
|
219
|
+
expect(log).toHaveBeenCalledWith(12);
|
|
220
|
+
b.value = 20;
|
|
221
|
+
expect(log).toHaveBeenCalledWith(30);
|
|
222
|
+
expect(log).toHaveBeenCalledTimes(3);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('handles conditional dependency tracking', () => {
|
|
226
|
+
const toggle = signal(true);
|
|
227
|
+
const a = signal('A');
|
|
228
|
+
const b = signal('B');
|
|
229
|
+
const log = vi.fn();
|
|
230
|
+
effect(() => {
|
|
231
|
+
log(toggle.value ? a.value : b.value);
|
|
232
|
+
});
|
|
233
|
+
expect(log).toHaveBeenCalledWith('A');
|
|
234
|
+
// Change b — should NOT re-run because b is not tracked when toggle=true
|
|
235
|
+
b.value = 'B2';
|
|
236
|
+
// After toggle switches, b becomes tracked
|
|
237
|
+
toggle.value = false;
|
|
238
|
+
expect(log).toHaveBeenCalledWith('B2');
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
// reactive — array mutations
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
|
|
247
|
+
describe('reactive — arrays', () => {
|
|
248
|
+
it('detects push on a reactive array', () => {
|
|
249
|
+
const fn = vi.fn();
|
|
250
|
+
const obj = reactive({ items: [1, 2, 3] }, fn);
|
|
251
|
+
obj.items.push(4);
|
|
252
|
+
expect(fn).toHaveBeenCalled();
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('detects index assignment', () => {
|
|
256
|
+
const fn = vi.fn();
|
|
257
|
+
const obj = reactive({ items: ['a', 'b'] }, fn);
|
|
258
|
+
obj.items[0] = 'z';
|
|
259
|
+
expect(fn).toHaveBeenCalledWith('0', 'z', 'a');
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
// computed — advanced
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
|
|
268
|
+
describe('computed — advanced', () => {
|
|
269
|
+
it('chains computed signals', () => {
|
|
270
|
+
const count = signal(2);
|
|
271
|
+
const doubled = computed(() => count.value * 2);
|
|
272
|
+
const quadrupled = computed(() => doubled.value * 2);
|
|
273
|
+
expect(quadrupled.value).toBe(8);
|
|
274
|
+
count.value = 3;
|
|
275
|
+
expect(quadrupled.value).toBe(12);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('does not recompute when dependencies unchanged (diamond)', () => {
|
|
279
|
+
const s = signal(1);
|
|
280
|
+
const a = computed(() => s.value + 1);
|
|
281
|
+
const b = computed(() => s.value + 2);
|
|
282
|
+
const spy = vi.fn(() => a.value + b.value);
|
|
283
|
+
const c = computed(spy);
|
|
284
|
+
expect(c.value).toBe(5); // (1+1)+(1+2)
|
|
285
|
+
spy.mockClear();
|
|
286
|
+
s.value = 10;
|
|
287
|
+
expect(c.value).toBe(23); // (10+1)+(10+2)
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('peek does not create dependency', () => {
|
|
291
|
+
const s = signal(0);
|
|
292
|
+
const log = vi.fn();
|
|
293
|
+
effect(() => {
|
|
294
|
+
log(s.peek());
|
|
295
|
+
});
|
|
296
|
+
expect(log).toHaveBeenCalledWith(0);
|
|
297
|
+
s.value = 1;
|
|
298
|
+
// peek doesn't track, so effect should NOT re-run
|
|
299
|
+
expect(log).toHaveBeenCalledTimes(1);
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
// ---------------------------------------------------------------------------
|
|
305
|
+
// Signal — batch behavior
|
|
306
|
+
// ---------------------------------------------------------------------------
|
|
307
|
+
|
|
308
|
+
describe('Signal — multiple subscribers', () => {
|
|
309
|
+
it('notifies all subscribers', () => {
|
|
310
|
+
const s = signal(0);
|
|
311
|
+
const fn1 = vi.fn();
|
|
312
|
+
const fn2 = vi.fn();
|
|
313
|
+
s.subscribe(fn1);
|
|
314
|
+
s.subscribe(fn2);
|
|
315
|
+
s.value = 1;
|
|
316
|
+
expect(fn1).toHaveBeenCalledOnce();
|
|
317
|
+
expect(fn2).toHaveBeenCalledOnce();
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('unsubscribing one does not affect others', () => {
|
|
321
|
+
const s = signal(0);
|
|
322
|
+
const fn1 = vi.fn();
|
|
323
|
+
const fn2 = vi.fn();
|
|
324
|
+
const unsub1 = s.subscribe(fn1);
|
|
325
|
+
s.subscribe(fn2);
|
|
326
|
+
unsub1();
|
|
327
|
+
s.value = 1;
|
|
328
|
+
expect(fn1).not.toHaveBeenCalled();
|
|
329
|
+
expect(fn2).toHaveBeenCalledOnce();
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('handles rapid sequential updates', () => {
|
|
333
|
+
const s = signal(0);
|
|
334
|
+
const log = vi.fn();
|
|
335
|
+
s.subscribe(log);
|
|
336
|
+
for (let i = 1; i <= 10; i++) s.value = i;
|
|
337
|
+
expect(log).toHaveBeenCalledTimes(10);
|
|
338
|
+
});
|
|
191
339
|
});
|