Skip to content

Commit c211ea5

Browse files
committed
feat(test): add comprehensive unit test coverage for stores, composables, and utils
- Add unit tests for nodeRecommendation store (recordTestResult, recordBatchResults, toggleExclusion, localStorage persistence) - Add unit tests for shortcuts store (updateShortcut, findConflict, resetToDefaults) - Add unit tests for useKeyboardShortcuts composable (navigation shortcuts, input focus detection, help modal) - Add unit tests for useBatchLatencyTest composable (concurrency control, progress tracking) - Add unit tests for utils/index.ts (formatProxyType, getLatencyClassName, formatDuration, etc.) - Add unit tests for utils/nodeScoring.ts (calculateNodeScore, getScoreColorClass, formatTimeSince) - Configure vitest with jsdom environment and path aliases - Add Vue/VueUse auto-import mocks in test setup - Add GitHub Actions workflow for unit tests with coverage reporting - Install jsdom and @vitest/coverage-v8 dependencies Test results: 145 tests passing across 7 test files
1 parent 9550de2 commit c211ea5

File tree

17 files changed

+2224
-10
lines changed

17 files changed

+2224
-10
lines changed

.github/workflows/unit-tests.yml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: unit-tests
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
branches:
9+
- main
10+
11+
permissions:
12+
contents: read
13+
14+
jobs:
15+
unit-test:
16+
runs-on: ubuntu-latest
17+
18+
steps:
19+
- uses: actions/checkout@v4
20+
21+
- uses: pnpm/action-setup@v4
22+
23+
- uses: actions/setup-node@v4
24+
with:
25+
cache: pnpm
26+
node-version: lts/*
27+
28+
- name: Install dependencies
29+
run: pnpm install
30+
31+
- name: Run unit tests with coverage
32+
run: pnpm test:coverage
33+
34+
- name: Upload coverage reports
35+
uses: codecov/codecov-action@v4
36+
with:
37+
files: ./coverage/lcov.info
38+
fail_ci_if_error: false
39+
verbose: true
40+
env:
41+
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,5 @@ Thumbs.db
3030

3131
# Local build output
3232
dist
33+
34+
/coverage
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
import { createPinia, setActivePinia } from 'pinia'
2+
import { beforeEach, describe, expect, it, vi } from 'vitest'
3+
import { useBatchLatencyTest } from '../useBatchLatencyTest'
4+
5+
// Mock useRequest
6+
const mockGet = vi.fn()
7+
const mockJson = vi.fn()
8+
mockGet.mockReturnValue({ json: mockJson })
9+
10+
vi.mock('../useApi', () => ({
11+
useRequest: () => ({
12+
get: mockGet,
13+
}),
14+
}))
15+
16+
// Mock stores
17+
const mockConfigStore = {
18+
urlForLatencyTest: 'https://www.gstatic.com/generate_204',
19+
latencyTestTimeoutDuration: 5000,
20+
}
21+
22+
const mockNodeRecommendationStore = {
23+
batchTestProgress: {
24+
total: 0,
25+
completed: 0,
26+
current: null,
27+
isRunning: false,
28+
},
29+
recordTestResult: vi.fn(),
30+
recordBatchResults: vi.fn(),
31+
}
32+
33+
vi.stubGlobal('useConfigStore', () => mockConfigStore)
34+
vi.stubGlobal('useNodeRecommendationStore', () => mockNodeRecommendationStore)
35+
36+
describe('composables/useBatchLatencyTest', () => {
37+
beforeEach(() => {
38+
setActivePinia(createPinia())
39+
vi.clearAllMocks()
40+
mockNodeRecommendationStore.batchTestProgress = {
41+
total: 0,
42+
completed: 0,
43+
current: null,
44+
isRunning: false,
45+
}
46+
})
47+
48+
describe('initial state', () => {
49+
it('starts with isRunning as false', () => {
50+
const { isRunning } = useBatchLatencyTest()
51+
52+
expect(isRunning.value).toBe(false)
53+
})
54+
55+
it('starts with empty progress', () => {
56+
const { progress } = useBatchLatencyTest()
57+
58+
expect(progress.value).toEqual({
59+
completed: 0,
60+
total: 0,
61+
current: '',
62+
})
63+
})
64+
})
65+
66+
describe('batchTestNodes', () => {
67+
it('sets isRunning to true during test', async () => {
68+
mockJson.mockResolvedValue({ delay: 100 })
69+
70+
const { batchTestNodes, isRunning } = useBatchLatencyTest()
71+
72+
const testPromise = batchTestNodes(['node1'], {
73+
url: 'https://test.com',
74+
timeout: 5000,
75+
})
76+
77+
// During execution, isRunning should be true
78+
// After completion, it should be false
79+
await testPromise
80+
81+
expect(isRunning.value).toBe(false)
82+
})
83+
84+
it('updates progress during batch test', async () => {
85+
mockJson.mockResolvedValue({ delay: 100 })
86+
87+
const { batchTestNodes } = useBatchLatencyTest()
88+
const onProgress = vi.fn()
89+
90+
await batchTestNodes(['node1', 'node2'], {
91+
url: 'https://test.com',
92+
timeout: 5000,
93+
onProgress,
94+
})
95+
96+
expect(onProgress).toHaveBeenCalled()
97+
})
98+
99+
it('returns results for all nodes', async () => {
100+
mockJson
101+
.mockResolvedValueOnce({ delay: 100 })
102+
.mockResolvedValueOnce({ delay: 200 })
103+
104+
const { batchTestNodes } = useBatchLatencyTest()
105+
106+
const results = await batchTestNodes(['node1', 'node2'], {
107+
url: 'https://test.com',
108+
timeout: 5000,
109+
})
110+
111+
expect(results).toHaveProperty('node1')
112+
expect(results).toHaveProperty('node2')
113+
})
114+
115+
it('records test results in store', async () => {
116+
mockJson.mockResolvedValue({ delay: 150 })
117+
118+
const { batchTestNodes } = useBatchLatencyTest()
119+
120+
await batchTestNodes(['node1'], {
121+
url: 'https://test.com',
122+
timeout: 5000,
123+
})
124+
125+
expect(mockNodeRecommendationStore.recordTestResult).toHaveBeenCalledWith(
126+
'node1',
127+
150,
128+
true,
129+
)
130+
})
131+
132+
it('handles failed tests with delay 0', async () => {
133+
mockJson.mockRejectedValue(new Error('Network error'))
134+
135+
const { batchTestNodes } = useBatchLatencyTest()
136+
137+
const results = await batchTestNodes(['node1'], {
138+
url: 'https://test.com',
139+
timeout: 5000,
140+
})
141+
142+
expect(results.node1).toBe(0)
143+
expect(mockNodeRecommendationStore.recordTestResult).toHaveBeenCalledWith(
144+
'node1',
145+
null,
146+
false,
147+
)
148+
})
149+
150+
it('calls onNodeComplete callback for each node', async () => {
151+
mockJson.mockResolvedValue({ delay: 100 })
152+
153+
const { batchTestNodes } = useBatchLatencyTest()
154+
const onNodeComplete = vi.fn()
155+
156+
await batchTestNodes(['node1', 'node2'], {
157+
url: 'https://test.com',
158+
timeout: 5000,
159+
onNodeComplete,
160+
})
161+
162+
expect(onNodeComplete).toHaveBeenCalledTimes(2)
163+
expect(onNodeComplete).toHaveBeenCalledWith('node1', 100)
164+
expect(onNodeComplete).toHaveBeenCalledWith('node2', 100)
165+
})
166+
})
167+
168+
describe('concurrency control', () => {
169+
it('processes nodes in batches of 10', async () => {
170+
// Create 15 nodes to test batching
171+
const nodes = Array.from({ length: 15 }, (_, i) => `node${i + 1}`)
172+
mockJson.mockResolvedValue({ delay: 100 })
173+
174+
const { batchTestNodes } = useBatchLatencyTest()
175+
176+
await batchTestNodes(nodes, {
177+
url: 'https://test.com',
178+
timeout: 5000,
179+
})
180+
181+
// All 15 nodes should have been tested
182+
expect(
183+
mockNodeRecommendationStore.recordTestResult,
184+
).toHaveBeenCalledTimes(15)
185+
})
186+
})
187+
188+
describe('progress tracking', () => {
189+
it('updates store progress during test', async () => {
190+
mockJson.mockResolvedValue({ delay: 100 })
191+
192+
const { batchTestNodes } = useBatchLatencyTest()
193+
194+
await batchTestNodes(['node1', 'node2', 'node3'], {
195+
url: 'https://test.com',
196+
timeout: 5000,
197+
})
198+
199+
// After completion, progress should reflect completion
200+
expect(mockNodeRecommendationStore.batchTestProgress.isRunning).toBe(
201+
false,
202+
)
203+
expect(mockNodeRecommendationStore.batchTestProgress.completed).toBe(3)
204+
})
205+
206+
it('resets progress after test completes', async () => {
207+
mockJson.mockResolvedValue({ delay: 100 })
208+
209+
const { batchTestNodes, progress } = useBatchLatencyTest()
210+
211+
await batchTestNodes(['node1'], {
212+
url: 'https://test.com',
213+
timeout: 5000,
214+
})
215+
216+
expect(progress.value.current).toBe('')
217+
})
218+
})
219+
220+
describe('abortTest', () => {
221+
it('provides abort functionality', () => {
222+
const { abortTest } = useBatchLatencyTest()
223+
224+
// Should not throw
225+
expect(() => abortTest()).not.toThrow()
226+
})
227+
})
228+
229+
describe('testGroupNodes', () => {
230+
it('uses default config values when options not provided', async () => {
231+
mockJson.mockResolvedValue({ node1: 100, node2: 200 })
232+
233+
const { testGroupNodes } = useBatchLatencyTest()
234+
235+
await testGroupNodes('group1')
236+
237+
expect(mockGet).toHaveBeenCalledWith(
238+
'group/group1/delay',
239+
expect.objectContaining({
240+
searchParams: {
241+
url: mockConfigStore.urlForLatencyTest,
242+
timeout: mockConfigStore.latencyTestTimeoutDuration,
243+
},
244+
}),
245+
)
246+
})
247+
248+
it('records batch results in store', async () => {
249+
const groupResults = { node1: 100, node2: 200 }
250+
mockJson.mockResolvedValue(groupResults)
251+
252+
const { testGroupNodes } = useBatchLatencyTest()
253+
254+
await testGroupNodes('group1')
255+
256+
expect(
257+
mockNodeRecommendationStore.recordBatchResults,
258+
).toHaveBeenCalledWith(groupResults)
259+
})
260+
})
261+
})

0 commit comments

Comments
 (0)