Skip to content

Commit a859616

Browse files
committed
test: add unit tests for NetworkProvider (#531)
- 18 tests covering: - Default network value ('testnet') - Children rendering - isMockMode true/false based on streamContractId - Context consumption (network name, config.rpcUrl) - setNetwork switching testnet ↔ mainnet - localStorage persistence (save, read, ignore invalid, update) - useNetwork throws outside provider - useNetwork does not throw inside provider - Hook return shape (network, setNetwork, config, isMockMode) Closes #531
1 parent b6d388a commit a859616

1 file changed

Lines changed: 270 additions & 0 deletions

File tree

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2+
import { render, screen, act, renderHook } from '@testing-library/react'
3+
import React from 'react'
4+
5+
// ─── Mock stellar getNetworkConfig ────────────────────────────────────────────
6+
// Controlled via `mockContractId` so individual tests can flip isMockMode.
7+
let mockContractId = ''
8+
9+
vi.mock('@/lib/stellar', () => ({
10+
getNetworkConfig: vi.fn((network: string) => ({
11+
name: network,
12+
passphrase:
13+
network === 'mainnet'
14+
? 'Public Global Stellar Network ; September 2015'
15+
: 'Test SDF Network ; September 2015',
16+
rpcUrl: `https://soroban-${network}.stellar.org`,
17+
horizonUrl: `https://horizon-${network}.stellar.org`,
18+
streamContractId: mockContractId,
19+
knownTokens: [],
20+
})),
21+
}))
22+
23+
// Import AFTER mocks are declared
24+
import { NetworkProvider, useNetwork } from '@/components/providers/network-provider'
25+
26+
// ─── localStorage helpers ─────────────────────────────────────────────────────
27+
const NETWORK_KEY = 'flowstar:network'
28+
29+
function renderWithProvider(ui: React.ReactNode) {
30+
return render(<NetworkProvider>{ui}</NetworkProvider>)
31+
}
32+
33+
// ─── Consumer component for integration tests ─────────────────────────────────
34+
function NetworkConsumer() {
35+
const { network, isMockMode, config, setNetwork } = useNetwork()
36+
return (
37+
<div>
38+
<span data-testid="network">{network}</span>
39+
<span data-testid="mock-mode">{String(isMockMode)}</span>
40+
<span data-testid="rpc-url">{config.rpcUrl}</span>
41+
<button onClick={() => setNetwork('mainnet')}>switch-mainnet</button>
42+
<button onClick={() => setNetwork('testnet')}>switch-testnet</button>
43+
</div>
44+
)
45+
}
46+
47+
// ─── Tests ─────────────────────────────────────────────────────────────────────
48+
49+
describe('NetworkProvider — default values', () => {
50+
beforeEach(() => {
51+
mockContractId = ''
52+
localStorage.clear()
53+
})
54+
55+
it('provides "testnet" as the default network', async () => {
56+
renderWithProvider(<NetworkConsumer />)
57+
// After mount the useEffect runs; wait for it
58+
await act(async () => {})
59+
expect(screen.getByTestId('network').textContent).toBe('testnet')
60+
})
61+
62+
it('renders children without crashing', () => {
63+
renderWithProvider(<span data-testid="child">hello</span>)
64+
expect(screen.getByTestId('child')).toBeInTheDocument()
65+
})
66+
})
67+
68+
describe('NetworkProvider — isMockMode', () => {
69+
beforeEach(() => {
70+
localStorage.clear()
71+
})
72+
73+
it('isMockMode is true when streamContractId is empty string', async () => {
74+
mockContractId = ''
75+
renderWithProvider(<NetworkConsumer />)
76+
await act(async () => {})
77+
expect(screen.getByTestId('mock-mode').textContent).toBe('true')
78+
})
79+
80+
it('isMockMode is false when streamContractId is non-empty', async () => {
81+
mockContractId = 'CBNDCZTRFNTDAPQLPK2ESOKO4XFMSC4PX37QE75BBYFOYIEWIPMHAKFV'
82+
renderWithProvider(<NetworkConsumer />)
83+
await act(async () => {})
84+
expect(screen.getByTestId('mock-mode').textContent).toBe('false')
85+
})
86+
})
87+
88+
describe('NetworkProvider — context values are consumed correctly', () => {
89+
beforeEach(() => {
90+
mockContractId = 'CONTRACT_ID'
91+
localStorage.clear()
92+
})
93+
94+
it('exposes the network name via context', async () => {
95+
renderWithProvider(<NetworkConsumer />)
96+
await act(async () => {})
97+
expect(screen.getByTestId('network').textContent).toBe('testnet')
98+
})
99+
100+
it('exposes config.rpcUrl that matches the active network', async () => {
101+
renderWithProvider(<NetworkConsumer />)
102+
await act(async () => {})
103+
expect(screen.getByTestId('rpc-url').textContent).toBe('https://soroban-testnet.stellar.org')
104+
})
105+
})
106+
107+
describe('NetworkProvider — setNetwork', () => {
108+
beforeEach(() => {
109+
mockContractId = 'CONTRACT_ID'
110+
localStorage.clear()
111+
})
112+
113+
it('switches network to mainnet when setNetwork("mainnet") is called', async () => {
114+
renderWithProvider(<NetworkConsumer />)
115+
await act(async () => {})
116+
117+
await act(async () => {
118+
screen.getByText('switch-mainnet').click()
119+
})
120+
121+
expect(screen.getByTestId('network').textContent).toBe('mainnet')
122+
})
123+
124+
it('switches config.rpcUrl when network changes', async () => {
125+
renderWithProvider(<NetworkConsumer />)
126+
await act(async () => {})
127+
128+
await act(async () => {
129+
screen.getByText('switch-mainnet').click()
130+
})
131+
132+
expect(screen.getByTestId('rpc-url').textContent).toBe('https://soroban-mainnet.stellar.org')
133+
})
134+
135+
it('can switch back to testnet after switching to mainnet', async () => {
136+
renderWithProvider(<NetworkConsumer />)
137+
await act(async () => {})
138+
139+
await act(async () => {
140+
screen.getByText('switch-mainnet').click()
141+
})
142+
expect(screen.getByTestId('network').textContent).toBe('mainnet')
143+
144+
await act(async () => {
145+
screen.getByText('switch-testnet').click()
146+
})
147+
expect(screen.getByTestId('network').textContent).toBe('testnet')
148+
})
149+
})
150+
151+
describe('NetworkProvider — localStorage persistence', () => {
152+
beforeEach(() => {
153+
mockContractId = ''
154+
localStorage.clear()
155+
})
156+
157+
it('persists the chosen network to localStorage on setNetwork', async () => {
158+
renderWithProvider(<NetworkConsumer />)
159+
await act(async () => {})
160+
161+
await act(async () => {
162+
screen.getByText('switch-mainnet').click()
163+
})
164+
165+
expect(localStorage.getItem(NETWORK_KEY)).toBe('mainnet')
166+
})
167+
168+
it('reads a previously saved network from localStorage on mount', async () => {
169+
localStorage.setItem(NETWORK_KEY, 'mainnet')
170+
renderWithProvider(<NetworkConsumer />)
171+
172+
// Wait for the useEffect that reads localStorage
173+
await act(async () => {})
174+
175+
expect(screen.getByTestId('network').textContent).toBe('mainnet')
176+
})
177+
178+
it('ignores an invalid value stored in localStorage', async () => {
179+
localStorage.setItem(NETWORK_KEY, 'invalid-network')
180+
renderWithProvider(<NetworkConsumer />)
181+
await act(async () => {})
182+
183+
// Should fall back to the default 'testnet'
184+
expect(screen.getByTestId('network').textContent).toBe('testnet')
185+
})
186+
187+
it('updates localStorage when switching network a second time', async () => {
188+
renderWithProvider(<NetworkConsumer />)
189+
await act(async () => {})
190+
191+
await act(async () => {
192+
screen.getByText('switch-mainnet').click()
193+
})
194+
expect(localStorage.getItem(NETWORK_KEY)).toBe('mainnet')
195+
196+
await act(async () => {
197+
screen.getByText('switch-testnet').click()
198+
})
199+
expect(localStorage.getItem(NETWORK_KEY)).toBe('testnet')
200+
})
201+
})
202+
203+
describe('useNetwork — error boundary (used outside provider)', () => {
204+
// Suppress the expected console.error from React about uncaught errors in tests
205+
beforeEach(() => {
206+
vi.spyOn(console, 'error').mockImplementation(() => {})
207+
})
208+
209+
afterEach(() => {
210+
vi.restoreAllMocks()
211+
})
212+
213+
it('throws when useNetwork is called outside a NetworkProvider', () => {
214+
expect(() => renderHook(() => useNetwork())).toThrow(
215+
'useNetwork must be used within a NetworkProvider',
216+
)
217+
})
218+
219+
it('does NOT throw when useNetwork is called inside a NetworkProvider', () => {
220+
expect(() =>
221+
renderHook(() => useNetwork(), {
222+
wrapper: ({ children }) => <NetworkProvider>{children}</NetworkProvider>,
223+
}),
224+
).not.toThrow()
225+
})
226+
})
227+
228+
describe('useNetwork — hook return shape', () => {
229+
beforeEach(() => {
230+
mockContractId = 'SOME_CONTRACT'
231+
localStorage.clear()
232+
})
233+
234+
it('returns an object with network, setNetwork, config, and isMockMode', async () => {
235+
const { result } = renderHook(() => useNetwork(), {
236+
wrapper: ({ children }) => <NetworkProvider>{children}</NetworkProvider>,
237+
})
238+
239+
await act(async () => {})
240+
241+
expect(result.current).toHaveProperty('network')
242+
expect(result.current).toHaveProperty('setNetwork')
243+
expect(result.current).toHaveProperty('config')
244+
expect(result.current).toHaveProperty('isMockMode')
245+
})
246+
247+
it('setNetwork is a function', async () => {
248+
const { result } = renderHook(() => useNetwork(), {
249+
wrapper: ({ children }) => <NetworkProvider>{children}</NetworkProvider>,
250+
})
251+
252+
await act(async () => {})
253+
254+
expect(typeof result.current.setNetwork).toBe('function')
255+
})
256+
257+
it('config contains the expected shape (rpcUrl, passphrase, etc.)', async () => {
258+
const { result } = renderHook(() => useNetwork(), {
259+
wrapper: ({ children }) => <NetworkProvider>{children}</NetworkProvider>,
260+
})
261+
262+
await act(async () => {})
263+
264+
expect(result.current.config).toMatchObject({
265+
rpcUrl: expect.any(String),
266+
passphrase: expect.any(String),
267+
streamContractId: expect.any(String),
268+
})
269+
})
270+
})

0 commit comments

Comments
 (0)