zero-query 0.9.5 → 0.9.7
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 +6 -6
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +107 -8
- package/dist/zquery.min.js +2 -2
- package/index.d.ts +1 -1
- package/index.js +1 -0
- package/package.json +1 -1
- package/src/component.js +66 -5
- package/src/http.js +37 -0
- package/tests/component.test.js +1185 -0
- package/tests/http.test.js +200 -0
- package/types/http.d.ts +15 -4
package/tests/http.test.js
CHANGED
|
@@ -446,3 +446,203 @@ describe('http — AbortController integration', () => {
|
|
|
446
446
|
expect(fetchSpy).toHaveBeenCalled();
|
|
447
447
|
});
|
|
448
448
|
});
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
// ===========================================================================
|
|
452
|
+
// HEAD requests
|
|
453
|
+
// ===========================================================================
|
|
454
|
+
|
|
455
|
+
describe('http.head', () => {
|
|
456
|
+
it('sends a HEAD request', async () => {
|
|
457
|
+
mockFetch({});
|
|
458
|
+
const result = await http.head('https://api.test.com/resource');
|
|
459
|
+
expect(fetchSpy.mock.calls[0][1].method).toBe('HEAD');
|
|
460
|
+
expect(result.ok).toBe(true);
|
|
461
|
+
expect(result.status).toBe(200);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it('does not send a body', async () => {
|
|
465
|
+
mockFetch({});
|
|
466
|
+
await http.head('https://api.test.com/resource');
|
|
467
|
+
const opts = fetchSpy.mock.calls[0][1];
|
|
468
|
+
expect(opts.body).toBeUndefined();
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it('accepts per-request options', async () => {
|
|
472
|
+
mockFetch({});
|
|
473
|
+
await http.head('https://api.test.com/resource', {
|
|
474
|
+
headers: { 'X-Check': 'exists' },
|
|
475
|
+
});
|
|
476
|
+
const opts = fetchSpy.mock.calls[0][1];
|
|
477
|
+
expect(opts.headers['X-Check']).toBe('exists');
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it('returns response headers', async () => {
|
|
481
|
+
mockFetch({});
|
|
482
|
+
const result = await http.head('https://api.test.com/resource');
|
|
483
|
+
expect(result.headers).toBeDefined();
|
|
484
|
+
expect(typeof result.headers).toBe('object');
|
|
485
|
+
});
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
// ===========================================================================
|
|
490
|
+
// Interceptor unsubscribe
|
|
491
|
+
// ===========================================================================
|
|
492
|
+
|
|
493
|
+
describe('http — interceptor unsubscribe', () => {
|
|
494
|
+
it('onRequest returns an unsubscribe function', async () => {
|
|
495
|
+
http.clearInterceptors();
|
|
496
|
+
const spy = vi.fn();
|
|
497
|
+
const unsub = http.onRequest(spy);
|
|
498
|
+
expect(typeof unsub).toBe('function');
|
|
499
|
+
|
|
500
|
+
mockFetch({});
|
|
501
|
+
await http.get('https://api.test.com/a');
|
|
502
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
503
|
+
|
|
504
|
+
unsub();
|
|
505
|
+
await http.get('https://api.test.com/b');
|
|
506
|
+
expect(spy).toHaveBeenCalledTimes(1); // not called again
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it('onResponse returns an unsubscribe function', async () => {
|
|
510
|
+
http.clearInterceptors();
|
|
511
|
+
const spy = vi.fn();
|
|
512
|
+
const unsub = http.onResponse(spy);
|
|
513
|
+
expect(typeof unsub).toBe('function');
|
|
514
|
+
|
|
515
|
+
mockFetch({});
|
|
516
|
+
await http.get('https://api.test.com/a');
|
|
517
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
518
|
+
|
|
519
|
+
unsub();
|
|
520
|
+
await http.get('https://api.test.com/b');
|
|
521
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
it('double-unsubscribe is safe', async () => {
|
|
525
|
+
http.clearInterceptors();
|
|
526
|
+
const spy = vi.fn();
|
|
527
|
+
const unsub = http.onRequest(spy);
|
|
528
|
+
unsub();
|
|
529
|
+
unsub(); // should not throw
|
|
530
|
+
mockFetch({});
|
|
531
|
+
await http.get('https://api.test.com/data');
|
|
532
|
+
expect(spy).not.toHaveBeenCalled();
|
|
533
|
+
});
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
// ===========================================================================
|
|
538
|
+
// clearInterceptors
|
|
539
|
+
// ===========================================================================
|
|
540
|
+
|
|
541
|
+
describe('http.clearInterceptors', () => {
|
|
542
|
+
it('clears all interceptors when called with no args', async () => {
|
|
543
|
+
http.clearInterceptors();
|
|
544
|
+
const reqSpy = vi.fn();
|
|
545
|
+
const resSpy = vi.fn();
|
|
546
|
+
http.onRequest(reqSpy);
|
|
547
|
+
http.onResponse(resSpy);
|
|
548
|
+
|
|
549
|
+
http.clearInterceptors();
|
|
550
|
+
mockFetch({});
|
|
551
|
+
await http.get('https://api.test.com/data');
|
|
552
|
+
expect(reqSpy).not.toHaveBeenCalled();
|
|
553
|
+
expect(resSpy).not.toHaveBeenCalled();
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
it('clears only request interceptors with "request"', async () => {
|
|
557
|
+
http.clearInterceptors();
|
|
558
|
+
const reqSpy = vi.fn();
|
|
559
|
+
const resSpy = vi.fn();
|
|
560
|
+
http.onRequest(reqSpy);
|
|
561
|
+
http.onResponse(resSpy);
|
|
562
|
+
|
|
563
|
+
http.clearInterceptors('request');
|
|
564
|
+
mockFetch({});
|
|
565
|
+
await http.get('https://api.test.com/data');
|
|
566
|
+
expect(reqSpy).not.toHaveBeenCalled();
|
|
567
|
+
expect(resSpy).toHaveBeenCalledTimes(1);
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
it('clears only response interceptors with "response"', async () => {
|
|
571
|
+
http.clearInterceptors();
|
|
572
|
+
const reqSpy = vi.fn();
|
|
573
|
+
const resSpy = vi.fn();
|
|
574
|
+
http.onRequest(reqSpy);
|
|
575
|
+
http.onResponse(resSpy);
|
|
576
|
+
|
|
577
|
+
http.clearInterceptors('response');
|
|
578
|
+
mockFetch({});
|
|
579
|
+
await http.get('https://api.test.com/data');
|
|
580
|
+
expect(reqSpy).toHaveBeenCalledTimes(1);
|
|
581
|
+
expect(resSpy).not.toHaveBeenCalled();
|
|
582
|
+
});
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
// ===========================================================================
|
|
587
|
+
// http.all — parallel requests
|
|
588
|
+
// ===========================================================================
|
|
589
|
+
|
|
590
|
+
describe('http.all', () => {
|
|
591
|
+
it('resolves all parallel requests', async () => {
|
|
592
|
+
mockFetch({ ok: true });
|
|
593
|
+
const results = await http.all([
|
|
594
|
+
http.get('https://api.test.com/a'),
|
|
595
|
+
http.get('https://api.test.com/b'),
|
|
596
|
+
http.get('https://api.test.com/c'),
|
|
597
|
+
]);
|
|
598
|
+
expect(results).toHaveLength(3);
|
|
599
|
+
expect(results.every(r => r.ok)).toBe(true);
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
it('rejects if any request fails', async () => {
|
|
603
|
+
http.clearInterceptors();
|
|
604
|
+
mockFetch({ error: 'fail' }, false, 500);
|
|
605
|
+
await expect(
|
|
606
|
+
http.all([
|
|
607
|
+
http.get('https://api.test.com/a'),
|
|
608
|
+
http.get('https://api.test.com/b'),
|
|
609
|
+
])
|
|
610
|
+
).rejects.toThrow();
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
it('handles empty array', async () => {
|
|
614
|
+
const results = await http.all([]);
|
|
615
|
+
expect(results).toEqual([]);
|
|
616
|
+
});
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
// ===========================================================================
|
|
621
|
+
// http.getConfig
|
|
622
|
+
// ===========================================================================
|
|
623
|
+
|
|
624
|
+
describe('http.getConfig', () => {
|
|
625
|
+
it('returns current config', () => {
|
|
626
|
+
http.configure({ baseURL: 'https://myapi.com', timeout: 5000 });
|
|
627
|
+
const config = http.getConfig();
|
|
628
|
+
expect(config.baseURL).toBe('https://myapi.com');
|
|
629
|
+
expect(config.timeout).toBe(5000);
|
|
630
|
+
expect(config.headers).toBeDefined();
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
it('returns a copy (not the internal reference)', () => {
|
|
634
|
+
const config = http.getConfig();
|
|
635
|
+
config.baseURL = 'https://mutated.com';
|
|
636
|
+
config.headers['X-Evil'] = 'injected';
|
|
637
|
+
const fresh = http.getConfig();
|
|
638
|
+
expect(fresh.baseURL).not.toBe('https://mutated.com');
|
|
639
|
+
expect(fresh.headers['X-Evil']).toBeUndefined();
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
it('reflects updates after configure', () => {
|
|
643
|
+
http.configure({ baseURL: '' });
|
|
644
|
+
expect(http.getConfig().baseURL).toBe('');
|
|
645
|
+
http.configure({ baseURL: 'https://updated.com' });
|
|
646
|
+
expect(http.getConfig().baseURL).toBe('https://updated.com');
|
|
647
|
+
});
|
|
648
|
+
});
|
package/types/http.d.ts
CHANGED
|
@@ -61,15 +61,26 @@ export interface HttpClient {
|
|
|
61
61
|
patch<T = any>(url: string, data?: any, opts?: HttpRequestOptions): Promise<HttpResponse<T>>;
|
|
62
62
|
/** DELETE request. */
|
|
63
63
|
delete<T = any>(url: string, data?: any, opts?: HttpRequestOptions): Promise<HttpResponse<T>>;
|
|
64
|
+
/** HEAD request — no body, useful for checking resource existence or headers. */
|
|
65
|
+
head<T = any>(url: string, opts?: HttpRequestOptions): Promise<HttpResponse<T>>;
|
|
64
66
|
|
|
65
67
|
/** Update default configuration for all subsequent requests. */
|
|
66
68
|
configure(options: HttpConfigureOptions): void;
|
|
67
69
|
|
|
68
|
-
/**
|
|
69
|
-
|
|
70
|
+
/** Read-only snapshot of the current configuration. Returns a shallow copy. */
|
|
71
|
+
getConfig(): { baseURL: string; headers: Record<string, string>; timeout: number };
|
|
70
72
|
|
|
71
|
-
/** Add a
|
|
72
|
-
|
|
73
|
+
/** Add a request interceptor (called before every request). Returns an unsubscribe function. */
|
|
74
|
+
onRequest(fn: HttpRequestInterceptor): () => void;
|
|
75
|
+
|
|
76
|
+
/** Add a response interceptor (called after every response, before error check). Returns an unsubscribe function. */
|
|
77
|
+
onResponse(fn: HttpResponseInterceptor): () => void;
|
|
78
|
+
|
|
79
|
+
/** Clear interceptors. No args = all; `'request'` or `'response'` for one type. */
|
|
80
|
+
clearInterceptors(type?: 'request' | 'response'): void;
|
|
81
|
+
|
|
82
|
+
/** Run multiple request promises in parallel via `Promise.all`. */
|
|
83
|
+
all<T extends readonly Promise<HttpResponse<any>>[]>(requests: T): Promise<{ -readonly [K in keyof T]: Awaited<T[K]> }>;
|
|
73
84
|
|
|
74
85
|
/** Create a new `AbortController` for manual request cancellation. */
|
|
75
86
|
createAbort(): AbortController;
|