forked from StellarSend/frontend
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathuseTransactions.test.tsx
More file actions
249 lines (212 loc) · 9.6 KB
/
Copy pathuseTransactions.test.tsx
File metadata and controls
249 lines (212 loc) · 9.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
import React from 'react'
import { act, renderHook, waitFor } from '@testing-library/react'
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import type { Transaction, TransactionPage } from '@/types'
const apiMocks = vi.hoisted(() => ({
fetchTransactionsFromHorizon: vi.fn(),
}))
vi.mock('@/lib/api', () => apiMocks)
const walletMocks = vi.hoisted(() => ({
publicKey: 'GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5',
network: 'testnet' as const,
isConnected: true,
}))
vi.mock('./useWallet', () => ({ useWallet: () => walletMocks }))
import { txKeys, useRecentTransactions, useInvalidateTransactions } from './useTransactions'
function makeTransaction(id: string): Transaction {
return {
id,
hash: id,
createdAt: new Date().toISOString(),
type: 'payment',
status: 'success',
sourceAccount: 'GSOURCE',
destinationAccount: 'GDEST',
amount: '10',
assetCode: 'XLM',
assetIssuer: null,
fee: '0.00001',
ledger: 1,
direction: 'received',
counterparty: 'GSOURCE',
}
}
function makeTransactions(limit: number): Transaction[] {
return Array.from({ length: limit }, (_, i) => makeTransaction(`tx${i}`))
}
function makePage(overrides: Partial<TransactionPage> = {}): TransactionPage {
return {
transactions: [],
page: 1,
pageSize: 5,
total: 0,
hasMore: false,
...overrides,
}
}
function wrapper({ children }: { children: React.ReactNode }) {
const client = new QueryClient({
defaultOptions: { queries: { retry: false } },
})
return <QueryClientProvider client={client}>{children}</QueryClientProvider>
}
// Renders two hooks under one shared QueryClient (mirroring how multiple
// Dashboard/History components mount together under the app's single
// QueryClientProvider).
function useTwoRecentTransactions(limitA: number, limitB: number) {
const a = useRecentTransactions(limitA)
const b = useRecentTransactions(limitB)
return { a, b }
}
// Mirrors Dashboard.tsx exactly: RecentTransactions(5), QuickStats(50),
// ActivityChart(50) all mounted together for the same wallet.
function useDashboardRecentTransactions() {
const recentTransactions = useRecentTransactions(5)
const quickStats = useRecentTransactions(50)
const activityChart = useRecentTransactions(50)
return { recentTransactions, quickStats, activityChart }
}
// Mirrors History.tsx exactly: HistoryChart(100), HistorySummary(50).
function useHistoryRecentTransactions() {
const historyChart = useRecentTransactions(100)
const historySummary = useRecentTransactions(50)
return { historyChart, historySummary }
}
function useTwoLimitsPlusInvalidate(limitA: number, limitB: number) {
const a = useRecentTransactions(limitA)
const b = useRecentTransactions(limitB)
const invalidate = useInvalidateTransactions()
return { a, b, invalidate }
}
beforeEach(() => {
apiMocks.fetchTransactionsFromHorizon.mockReset()
walletMocks.publicKey = 'GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5'
walletMocks.isConnected = true
})
describe('txKeys.list', () => {
it('includes limit in the returned key tuple', () => {
expect(txKeys.list('GPUBKEY', 5, { direction: undefined })).toEqual([
'transactions',
'list',
'GPUBKEY',
5,
{ direction: undefined },
])
})
it('produces distinct keys for the same wallet at different limits', () => {
const key5 = txKeys.list('GPUBKEY', 5)
const key50 = txKeys.list('GPUBKEY', 50)
expect(key5).not.toEqual(key50)
})
})
describe('useRecentTransactions', () => {
// Regression test for #51: mirrors Dashboard.tsx, where RecentTransactions
// calls useRecentTransactions(5) and QuickStats/ActivityChart call
// useRecentTransactions(50) for the same connected wallet at the same
// time. Before the fix, both resolved to one shared cache entry — whoever
// fetched first "won" and every subscriber received that dataset size
// regardless of the limit it actually asked for.
it('two callers with different limits for the same wallet each fetch and receive their own limit-sized dataset', async () => {
apiMocks.fetchTransactionsFromHorizon.mockImplementation(
async (_pubKey: string, _network: string, limit: number) =>
makePage({ transactions: makeTransactions(limit), pageSize: limit }),
)
const { result } = renderHook(() => useTwoRecentTransactions(5, 50), { wrapper })
await waitFor(() => {
expect(result.current.a.isSuccess).toBe(true)
expect(result.current.b.isSuccess).toBe(true)
})
// Each call site independently invoked the fetcher with its own limit...
expect(apiMocks.fetchTransactionsFromHorizon).toHaveBeenCalledWith(
walletMocks.publicKey,
walletMocks.network,
5,
)
expect(apiMocks.fetchTransactionsFromHorizon).toHaveBeenCalledWith(
walletMocks.publicKey,
walletMocks.network,
50,
)
// ...and each received the dataset sized for the limit it asked for,
// not whichever one happened to resolve first for a shared cache slot.
expect(result.current.a.data?.transactions).toHaveLength(5)
expect(result.current.b.data?.transactions).toHaveLength(50)
})
// Dashboard.tsx's exact three call sites: RecentTransactions(5),
// QuickStats(50), ActivityChart(50). The two limit=50 callers should
// still correctly dedupe to a single shared fetch (same wallet, same
// limit really is the same query) — only the limit=5 caller needs its
// own. Before the fix all three collapsed into one shared query
// regardless of limit; the fix must not overcorrect into never sharing.
it("Dashboard's three simultaneous callers (5, 50, 50) produce exactly two fetches, and both limit=50 callers share one result", async () => {
apiMocks.fetchTransactionsFromHorizon.mockImplementation(
async (_pubKey: string, _network: string, limit: number) =>
makePage({ transactions: makeTransactions(limit), pageSize: limit }),
)
const { result } = renderHook(() => useDashboardRecentTransactions(), { wrapper })
await waitFor(() => {
expect(result.current.recentTransactions.isSuccess).toBe(true)
expect(result.current.quickStats.isSuccess).toBe(true)
expect(result.current.activityChart.isSuccess).toBe(true)
})
// Two distinct limits -> two fetches, not three (the shared limit=50
// pair dedupes) and not one (limit=5 doesn't collide with them).
expect(apiMocks.fetchTransactionsFromHorizon).toHaveBeenCalledTimes(2)
expect(result.current.recentTransactions.data?.transactions).toHaveLength(5)
expect(result.current.quickStats.data?.transactions).toHaveLength(50)
expect(result.current.activityChart.data?.transactions).toHaveLength(50)
// QuickStats and ActivityChart share the exact same underlying data
// reference — same query, same cache entry, as intended for equal limits.
expect(result.current.quickStats.data).toBe(result.current.activityChart.data)
})
// History.tsx's independent collision: HistoryChart(100) and
// HistorySummary(50). A separate page from Dashboard, but the same class
// of bug — worth its own explicit coverage per the issue.
it("History's two simultaneous callers (100, 50) each fetch and receive their own limit-sized dataset", async () => {
apiMocks.fetchTransactionsFromHorizon.mockImplementation(
async (_pubKey: string, _network: string, limit: number) =>
makePage({ transactions: makeTransactions(limit), pageSize: limit }),
)
const { result } = renderHook(() => useHistoryRecentTransactions(), { wrapper })
await waitFor(() => {
expect(result.current.historyChart.isSuccess).toBe(true)
expect(result.current.historySummary.isSuccess).toBe(true)
})
expect(apiMocks.fetchTransactionsFromHorizon).toHaveBeenCalledTimes(2)
expect(result.current.historyChart.data?.transactions).toHaveLength(100)
expect(result.current.historySummary.data?.transactions).toHaveLength(50)
})
it('two callers with the identical limit for the same wallet still share one fetch', async () => {
apiMocks.fetchTransactionsFromHorizon.mockResolvedValue(makePage({ pageSize: 50 }))
const { result } = renderHook(() => useTwoRecentTransactions(50, 50), { wrapper })
await waitFor(() => {
expect(result.current.a.isSuccess).toBe(true)
expect(result.current.b.isSuccess).toBe(true)
})
expect(apiMocks.fetchTransactionsFromHorizon).toHaveBeenCalledTimes(1)
expect(result.current.a.data).toBe(result.current.b.data)
})
// useInvalidateTransactions invalidates via the broad txKeys.all prefix,
// not a specific list()/limit key. Now that list() includes limit in its
// key, this confirms invalidation still reaches every limit variant
// instead of accidentally scoping to just one.
it('useInvalidateTransactions still invalidates every distinct-limit query under one wallet', async () => {
apiMocks.fetchTransactionsFromHorizon.mockResolvedValue(makePage())
const { result } = renderHook(() => useTwoLimitsPlusInvalidate(5, 50), { wrapper })
await waitFor(() => {
expect(result.current.a.isSuccess).toBe(true)
expect(result.current.b.isSuccess).toBe(true)
})
expect(apiMocks.fetchTransactionsFromHorizon).toHaveBeenCalledTimes(2)
await act(async () => {
result.current.invalidate()
})
await waitFor(() => {
// Both the limit=5 and limit=50 queries refetched — one additional
// call each, four total — proving the broader txKeys.all invalidation
// still reaches both now-more-specific list() keys.
expect(apiMocks.fetchTransactionsFromHorizon).toHaveBeenCalledTimes(4)
})
})
})