import { expect, tap } from '@git.zone/tstest/tapbundle'; import { webrequest, WebrequestClient } from '../ts/index.js'; import * as typedserver from '@api.global/typedserver'; let testServer: typedserver.servertools.Server; // Setup test server tap.test('setup test server for v4 tests', async () => { testServer = new typedserver.servertools.Server({ cors: false, forceSsl: false, port: 2346, }); // Route that returns JSON with cache headers testServer.addRoute( '/cached', new typedserver.servertools.Handler('GET', (req, res) => { res.setHeader('Cache-Control', 'max-age=60'); res.setHeader('ETag', '"12345"'); res.status(200); res.send({ data: 'cached response', timestamp: Date.now() }); }), ); // Route that returns different data each time testServer.addRoute( '/dynamic', new typedserver.servertools.Handler('GET', (req, res) => { res.status(200); res.send({ data: 'dynamic response', timestamp: Date.now() }); }), ); // Route that sometimes fails (for retry testing) let requestCount = 0; testServer.addRoute( '/flaky', new typedserver.servertools.Handler('GET', (req, res) => { requestCount++; if (requestCount < 3) { res.status(500); res.end(); } else { res.status(200); res.send({ success: true, attempts: requestCount }); } }), ); // Route that returns 304 when ETag matches testServer.addRoute( '/conditional', new typedserver.servertools.Handler('GET', (req, res) => { const ifNoneMatch = req.headers['if-none-match']; if (ifNoneMatch === '"67890"') { res.status(304); res.end(); } else { res.setHeader('ETag', '"67890"'); res.status(200); res.send({ data: 'conditional response' }); } }), ); // POST route for testing testServer.addRoute( '/post', new typedserver.servertools.Handler('POST', (req, res) => { res.status(200); res.send({ received: true }); }), ); await testServer.start(); }); // Test 1: Basic fetch-compatible API tap.test('should work as fetch replacement', async () => { const response = await webrequest('http://localhost:2346/dynamic'); expect(response).toBeInstanceOf(Response); expect(response.ok).toEqual(true); expect(response.status).toEqual(200); const data = await response.json(); expect(data).toHaveProperty('data'); expect(data.data).toEqual('dynamic response'); }); // Test 2: getJson convenience method with generics tap.test('should support getJson with type safety', async () => { interface TestData { data: string; timestamp: number; } const data = await webrequest.getJson('http://localhost:2346/dynamic'); expect(data).toHaveProperty('data'); expect(data).toHaveProperty('timestamp'); expect(typeof data.timestamp).toEqual('number'); }); // Test 3: POST with JSON tap.test('should support postJson', async () => { const data = await webrequest.postJson('http://localhost:2346/post', { test: 'data' }); expect(data).toHaveProperty('received'); expect(data.received).toEqual(true); }); // Test 4: Cache strategy - network-first tap.test('should support network-first cache strategy', async () => { const response1 = await webrequest('http://localhost:2346/cached', { cacheStrategy: 'network-first' }); const data1 = await response1.json(); expect(data1).toHaveProperty('timestamp'); // Second request should hit network but may use cache on error const response2 = await webrequest('http://localhost:2346/cached', { cacheStrategy: 'network-first' }); const data2 = await response2.json(); expect(data2).toHaveProperty('timestamp'); }); // Test 5: Cache strategy - cache-first tap.test('should support cache-first strategy', async () => { // First request goes to network const response1 = await webrequest('http://localhost:2346/cached', { cacheStrategy: 'cache-first' }); const data1 = await response1.json(); const timestamp1 = data1.timestamp; // Second request should use cache const response2 = await webrequest('http://localhost:2346/cached', { cacheStrategy: 'cache-first' }); const data2 = await response2.json(); // Timestamps should be identical if cached expect(data2.timestamp).toEqual(timestamp1); }); // Test 6: Retry system tap.test('should retry failed requests', async () => { const response = await webrequest('http://localhost:2346/flaky', { retry: { maxAttempts: 3, backoff: 'constant', initialDelay: 100 } }); const data = await response.json(); expect(data.success).toEqual(true); expect(data.attempts).toBeGreaterThanOrEqual(3); }); // Test 7: Fallback URLs tap.test('should support fallback URLs', async () => { const response = await webrequest('http://localhost:2346/nonexistent', { fallbackUrls: ['http://localhost:2346/dynamic'], retry: { maxAttempts: 2 } }); expect(response.ok).toEqual(true); const data = await response.json(); expect(data).toHaveProperty('data'); }); // Test 8: Request interceptors tap.test('should support request interceptors', async () => { let interceptorCalled = false; const response = await webrequest('http://localhost:2346/dynamic', { interceptors: { request: [(req) => { interceptorCalled = true; const headers = new Headers(req.headers); headers.set('X-Custom-Header', 'test'); return new Request(req, { headers }); }] } }); expect(interceptorCalled).toEqual(true); expect(response.ok).toEqual(true); }); // Test 9: Response interceptors tap.test('should support response interceptors', async () => { let interceptorCalled = false; let capturedStatus: number; const response = await webrequest('http://localhost:2346/dynamic', { interceptors: { response: [(res) => { interceptorCalled = true; capturedStatus = res.status; return res; }] } }); expect(interceptorCalled).toEqual(true); expect(capturedStatus!).toEqual(200); }); // Test 10: Global interceptors with WebrequestClient tap.test('should support global interceptors', async () => { const client = new WebrequestClient(); let globalInterceptorCalled = false; client.addRequestInterceptor((req) => { globalInterceptorCalled = true; return req; }); await client.request('http://localhost:2346/dynamic'); expect(globalInterceptorCalled).toEqual(true); }); // Test 11: Request deduplication tap.test('should deduplicate simultaneous requests', async () => { const start = Date.now(); // Make 3 identical requests simultaneously const [res1, res2, res3] = await Promise.all([ webrequest('http://localhost:2346/dynamic', { deduplicate: true }), webrequest('http://localhost:2346/dynamic', { deduplicate: true }), webrequest('http://localhost:2346/dynamic', { deduplicate: true }), ]); const [data1, data2, data3] = await Promise.all([ res1.json(), res2.json(), res3.json(), ]); // All should have the same timestamp (same response) expect(data1.timestamp).toEqual(data2.timestamp); expect(data2.timestamp).toEqual(data3.timestamp); }); // Test 12: Timeout tap.test('should support timeout', async () => { try { await webrequest('http://localhost:2346/dynamic', { timeout: 1 // 1ms timeout should fail }); throw new Error('Should have timed out'); } catch (error) { expect(error.message).toContain('timeout'); } }); // Test 13: WebrequestClient with default options tap.test('should support WebrequestClient with defaults', async () => { const client = new WebrequestClient({ logging: false, cacheStrategy: 'network-first', timeout: 30000 }); const data = await client.getJson('http://localhost:2346/dynamic'); expect(data).toHaveProperty('data'); }); // Test 14: Clear cache tap.test('should clear cache', async () => { // Cache a request await webrequest('http://localhost:2346/cached', { cacheStrategy: 'cache-first' }); // Clear cache await webrequest.clearCache(); // This should work even though cache is cleared const response = await webrequest('http://localhost:2346/cached', { cacheStrategy: 'cache-first' }); expect(response.ok).toEqual(true); }); // Test 15: Backward compatibility with WebRequest class tap.test('should maintain backward compatibility with v3 API', async () => { const { WebRequest } = await import('../ts/index.js'); const client = new WebRequest({ logging: false }); const data = await client.getJson('http://localhost:2346/dynamic'); expect(data).toHaveProperty('data'); // Test POST const postData = await client.postJson('http://localhost:2346/post', { test: 'data' }); expect(postData).toHaveProperty('received'); }); // Cleanup tap.test('stop test server', async () => { await testServer.stop(); }); export default tap.start();