|
| 1 | +// tests/network.http.test.ts |
1 | 2 | import { beforeEach, describe, expect, it, vi } from 'vitest' |
2 | | -import { ofetch } from 'ofetch' |
3 | | -import { fetchWithTimeout, parallelRequests } from '../../src/network/http' |
4 | | - |
5 | | -// Mocking ofetch |
6 | | -vi.mock('ofetch', () => ({ |
7 | | - ofetch: vi.fn(), |
8 | | -})) |
| 3 | +import { |
| 4 | + checkNetworkStatus, |
| 5 | + fetchWithTimeout, |
| 6 | + getClientIP, |
| 7 | + parallelRequests, |
| 8 | + request, |
| 9 | +} from '../../src/network/http' |
9 | 10 |
|
| 11 | +// ====== fetchWithTimeout 测试 ====== |
10 | 12 | describe('fetchWithTimeout', () => { |
11 | | - beforeEach(() => { |
12 | | - vi.clearAllMocks() |
13 | | - }) |
| 13 | + beforeEach(() => vi.restoreAllMocks()) |
14 | 14 |
|
15 | | - it('should successfully fetch data within the timeout period', async () => { |
16 | | - const mockResponse = { success: true } |
17 | | - vi.mocked(ofetch).mockResolvedValueOnce(mockResponse) |
18 | | - |
19 | | - const response = await fetchWithTimeout('http://example.com', {}, 3000) |
20 | | - expect(response).toEqual(mockResponse) |
21 | | - expect(ofetch).toHaveBeenCalledWith('http://example.com', expect.objectContaining({ |
22 | | - signal: expect.any(AbortSignal), |
| 15 | + it('successfully fetches data', async () => { |
| 16 | + vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ |
| 17 | + ok: true, |
| 18 | + json: async () => ({ success: true }), |
23 | 19 | })) |
| 20 | + const response = await fetchWithTimeout('http://example.com', {}, 3000) |
| 21 | + expect(response.ok).toBe(true) |
| 22 | + const data = await response.json() |
| 23 | + expect(data).toEqual({ success: true }) |
24 | 24 | }) |
25 | 25 |
|
26 | | - it('should throw an error when the request times out', async () => { |
27 | | - vi.mocked(ofetch).mockRejectedValueOnce(new Error('Request timed out')) |
28 | | - |
| 26 | + it('throws error when fetch fails', async () => { |
| 27 | + vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('fail'))) |
29 | 28 | await expect(fetchWithTimeout('http://example.com', {}, 1000)) |
30 | 29 | .rejects |
31 | | - .toThrow('请求超时或失败: Request timed out') |
| 30 | + .toThrow('请求超时或失败: fail') |
32 | 31 | }) |
| 32 | +}) |
33 | 33 |
|
34 | | - it('should use default timeout if not provided', async () => { |
35 | | - const mockResponse = { success: true } |
36 | | - vi.mocked(ofetch).mockResolvedValueOnce(mockResponse) |
| 34 | +// ====== request 测试 ====== |
| 35 | +describe('request', () => { |
| 36 | + beforeEach(() => vi.restoreAllMocks()) |
37 | 37 |
|
38 | | - await fetchWithTimeout('http://example.com') |
39 | | - expect(ofetch).toHaveBeenCalledWith( |
| 38 | + it('gET request returns parsed JSON', async () => { |
| 39 | + vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ |
| 40 | + ok: true, |
| 41 | + json: async () => ({ message: 'ok' }), |
| 42 | + })) |
| 43 | + const data = await request('GET', 'http://example.com') |
| 44 | + expect(data).toEqual({ message: 'ok' }) |
| 45 | + }) |
| 46 | + |
| 47 | + it('pOST request sends JSON body', async () => { |
| 48 | + const spy = vi.fn().mockResolvedValue({ ok: true, json: async () => ({}) }) |
| 49 | + vi.stubGlobal('fetch', spy) |
| 50 | + await request('POST', 'http://example.com', { foo: 'bar' }) |
| 51 | + expect(spy).toHaveBeenCalledWith( |
40 | 52 | 'http://example.com', |
41 | 53 | expect.objectContaining({ |
| 54 | + method: 'POST', |
| 55 | + body: JSON.stringify({ foo: 'bar' }), |
| 56 | + headers: expect.objectContaining({ 'Content-Type': 'application/json' }), |
42 | 57 | signal: expect.any(AbortSignal), |
43 | 58 | }), |
44 | 59 | ) |
45 | 60 | }) |
46 | 61 | }) |
47 | 62 |
|
48 | | -/** |
49 | | - * Mocks a promise-returning function that resolves after a delay. |
50 | | - * @param value The value to resolve the promise with. |
51 | | - * @param delay The time in milliseconds before the promise resolves. |
52 | | - */ |
53 | | -function createDelayedPromise<T>(value: T, delay: number = 0): () => Promise<T> { |
54 | | - return () => new Promise(resolve => setTimeout(() => resolve(value), delay)) |
55 | | -} |
56 | | - |
| 63 | +// ====== parallelRequests 测试 ====== |
57 | 64 | describe('parallelRequests', () => { |
58 | | - it('should execute requests sequentially when concurrency is 1', async () => { |
| 65 | + function createDelayedPromise<T>(value: T, delay: number = 0) { |
| 66 | + return () => new Promise<T>(resolve => setTimeout(() => resolve(value), delay)) |
| 67 | + } |
| 68 | + |
| 69 | + it('executes sequentially when concurrency = 1', async () => { |
59 | 70 | const mockRequests = [ |
60 | 71 | createDelayedPromise('first', 100), |
61 | 72 | createDelayedPromise('second', 50), |
62 | 73 | createDelayedPromise('third', 200), |
63 | 74 | ] |
64 | | - |
65 | 75 | const results = await parallelRequests(mockRequests, 1) |
66 | | - |
67 | 76 | expect(results).toEqual(['first', 'second', 'third']) |
68 | 77 | }) |
69 | 78 |
|
70 | | - it('should execute requests concurrently up to the specified limit', async () => { |
71 | | - // Arrange mocks where the first request takes the longest, |
72 | | - // demonstrating concurrent execution. |
| 79 | + it('executes concurrently up to concurrency limit', async () => { |
73 | 80 | const mockRequests = [ |
74 | | - createDelayedPromise('last', 300), |
75 | | - createDelayedPromise('first', 100), |
76 | | - createDelayedPromise('second', 200), |
| 81 | + createDelayedPromise('first', 200), |
| 82 | + createDelayedPromise('second', 100), |
| 83 | + createDelayedPromise('third', 150), |
77 | 84 | ] |
78 | | - |
79 | | - const startTime = Date.now() |
| 85 | + const start = Date.now() |
80 | 86 | const results = await parallelRequests(mockRequests, 2) |
81 | | - const duration = Date.now() - startTime |
| 87 | + const duration = Date.now() - start |
| 88 | + expect(duration).toBeLessThan(350) |
| 89 | + expect(results).toEqual(['first', 'second', 'third']) |
| 90 | + }) |
| 91 | +}) |
| 92 | + |
| 93 | +// ====== getClientIP 测试 ====== |
| 94 | +describe('getClientIP', () => { |
| 95 | + beforeEach(() => vi.restoreAllMocks()) |
| 96 | + |
| 97 | + it('returns IP from successful request', async () => { |
| 98 | + vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ |
| 99 | + ok: true, |
| 100 | + json: async () => ({ ip: '127.0.0.1' }), |
| 101 | + })) |
| 102 | + const ip = await getClientIP() |
| 103 | + expect(ip).toBe('127.0.0.1') |
| 104 | + }) |
| 105 | + |
| 106 | + it('returns "unknown" if request fails', async () => { |
| 107 | + vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('fail'))) |
| 108 | + const ip = await getClientIP() |
| 109 | + expect(ip).toBe('unknown') |
| 110 | + }) |
| 111 | +}) |
| 112 | + |
| 113 | +// ====== checkNetworkStatus 测试 ====== |
| 114 | +describe('checkNetworkStatus', () => { |
| 115 | + it('returns navigator.onLine if available', async () => { |
| 116 | + Object.defineProperty(navigator, 'onLine', { value: true, configurable: true }) |
| 117 | + const status = await checkNetworkStatus() |
| 118 | + expect(status).toBe(true) |
| 119 | + }) |
82 | 120 |
|
83 | | - // Ensure that it took less than if they were run sequentially |
84 | | - expect(duration).toBeLessThan(600) // Total sequential time would be 600ms |
85 | | - expect(results).toEqual(['first', 'last', 'second']) |
| 121 | + it('falls back to image load if navigator.onLine undefined', async () => { |
| 122 | + Object.defineProperty(navigator, 'onLine', { value: undefined, configurable: true }) |
| 123 | + vi.stubGlobal('Image', class { |
| 124 | + onload: () => void = () => { } |
| 125 | + onerror: () => void = () => { } |
| 126 | + // eslint-disable-next-line accessor-pairs |
| 127 | + set src(_val: string) { this.onload() } |
| 128 | + } as any) |
| 129 | + const status = await checkNetworkStatus() |
| 130 | + expect(status).toBe(true) |
86 | 131 | }) |
87 | 132 | }) |
0 commit comments