Skip to content

Commit 729801b

Browse files
authored
test: add tests for frontend internal-accounts and transactions features (#1863)
* test: add tests for frontend internal-accounts hooks, detail page, and transactions page - use-internal-accounts.test.ts: 13 tests covering useInternalAccountsTable and useInternalAccountDetail hooks including null tenant, NotFound handling, and error propagation - [accountId].test.tsx: 15 tests covering loading states, account details display, action buttons (suspend/reactivate), and transactions tab rendering - index.test.tsx (transactions): 10 tests covering TransactionsPage heading, data display, direction badges, empty state, loading skeletons, and filters * fix: replace require() with dynamic import to satisfy no-require-imports lint rule * test: assert on detail-skeleton testid for loading state test --------- Co-authored-by: Ben Coombs <bjcoombs@users.noreply.github.com>
1 parent b3044a9 commit 729801b

3 files changed

Lines changed: 766 additions & 0 deletions

File tree

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest'
2+
import { renderHook, waitFor } from '@testing-library/react'
3+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
4+
import * as React from 'react'
5+
6+
const mockListInternalAccounts = vi.fn()
7+
const mockRetrieveInternalAccount = vi.fn()
8+
9+
vi.mock('@/api/context', () => ({
10+
useApiClients: vi.fn(() => ({
11+
internalAccount: {
12+
listInternalAccounts: mockListInternalAccounts,
13+
retrieveInternalAccount: mockRetrieveInternalAccount,
14+
},
15+
})),
16+
}))
17+
18+
vi.mock('@/hooks/use-tenant-context', () => ({
19+
useTenantSlug: vi.fn(() => 'test-tenant'),
20+
}))
21+
22+
import { useInternalAccountsTable, useInternalAccountDetail } from './use-internal-accounts'
23+
import { useTenantSlug } from '@/hooks/use-tenant-context'
24+
25+
function createWrapper() {
26+
const queryClient = new QueryClient({
27+
defaultOptions: { queries: { retry: false, gcTime: 0 } },
28+
logger: { log: () => {}, warn: () => {}, error: () => {} },
29+
})
30+
return function Wrapper({ children }: { children: React.ReactNode }) {
31+
return React.createElement(QueryClientProvider, { client: queryClient }, children)
32+
}
33+
}
34+
35+
describe('useInternalAccountsTable', () => {
36+
beforeEach(() => {
37+
vi.clearAllMocks()
38+
vi.mocked(useTenantSlug).mockReturnValue('test-tenant')
39+
})
40+
41+
it('returns queryKey, queryFn, and tenantSlug', () => {
42+
const { result } = renderHook(() => useInternalAccountsTable(), {
43+
wrapper: createWrapper(),
44+
})
45+
expect(result.current.queryKey).toBeDefined()
46+
expect(result.current.queryFn).toBeTypeOf('function')
47+
expect(result.current.tenantSlug).toBe('test-tenant')
48+
})
49+
50+
it('queryKey includes tenantSlug', () => {
51+
const { result } = renderHook(() => useInternalAccountsTable(), {
52+
wrapper: createWrapper(),
53+
})
54+
expect(result.current.queryKey).toEqual(
55+
expect.arrayContaining(['test-tenant']),
56+
)
57+
})
58+
59+
it('queryFn returns empty items when tenantSlug is null', async () => {
60+
vi.mocked(useTenantSlug).mockReturnValue(null)
61+
const { result } = renderHook(() => useInternalAccountsTable(), {
62+
wrapper: createWrapper(),
63+
})
64+
const data = await result.current.queryFn({ pageSize: 10 })
65+
expect(data).toEqual({ items: [] })
66+
})
67+
68+
it('queryFn maps facilities to InternalAccountRow shape', async () => {
69+
mockListInternalAccounts.mockResolvedValue({
70+
facilities: [
71+
{
72+
accountId: 'acc-001',
73+
accountCode: 'CLR-GBP-001',
74+
name: 'GBP Clearing',
75+
behaviorClass: 'CLEARING',
76+
accountStatus: 1,
77+
instrumentCode: 'GBP',
78+
createdAt: { seconds: BigInt(1700000000), nanos: 0 },
79+
},
80+
],
81+
pagination: { nextPageToken: 'tok-next', totalCount: BigInt(1) },
82+
})
83+
84+
const { result } = renderHook(() => useInternalAccountsTable(), {
85+
wrapper: createWrapper(),
86+
})
87+
88+
const data = await result.current.queryFn({ pageSize: 10 })
89+
90+
expect(data.items).toHaveLength(1)
91+
expect(data.items[0]).toMatchObject({
92+
accountId: 'acc-001',
93+
accountCode: 'CLR-GBP-001',
94+
name: 'GBP Clearing',
95+
behaviorClass: 'CLEARING',
96+
accountStatus: 1,
97+
instrumentCode: 'GBP',
98+
})
99+
expect(data.nextPageToken).toBe('tok-next')
100+
})
101+
102+
it('queryFn passes statusFilter parsed from filters', async () => {
103+
mockListInternalAccounts.mockResolvedValue({
104+
facilities: [],
105+
pagination: { nextPageToken: '' },
106+
})
107+
108+
const { result } = renderHook(() => useInternalAccountsTable(), {
109+
wrapper: createWrapper(),
110+
})
111+
112+
await result.current.queryFn({ pageSize: 10, filters: { status: '2' } })
113+
114+
expect(mockListInternalAccounts).toHaveBeenCalledWith(
115+
expect.objectContaining({ statusFilter: 2 }),
116+
)
117+
})
118+
119+
it('queryFn passes behaviorClassFilter from filters', async () => {
120+
mockListInternalAccounts.mockResolvedValue({
121+
facilities: [],
122+
pagination: { nextPageToken: '' },
123+
})
124+
125+
const { result } = renderHook(() => useInternalAccountsTable(), {
126+
wrapper: createWrapper(),
127+
})
128+
129+
await result.current.queryFn({ pageSize: 10, filters: { behaviorClass: 'CLEARING' } })
130+
131+
expect(mockListInternalAccounts).toHaveBeenCalledWith(
132+
expect.objectContaining({ behaviorClassFilter: 'CLEARING' }),
133+
)
134+
})
135+
136+
it('queryFn returns undefined nextPageToken when pagination is empty', async () => {
137+
mockListInternalAccounts.mockResolvedValue({
138+
facilities: [],
139+
pagination: { nextPageToken: '' },
140+
})
141+
142+
const { result } = renderHook(() => useInternalAccountsTable(), {
143+
wrapper: createWrapper(),
144+
})
145+
146+
const data = await result.current.queryFn({ pageSize: 10 })
147+
expect(data.nextPageToken).toBeUndefined()
148+
})
149+
})
150+
151+
describe('useInternalAccountDetail', () => {
152+
beforeEach(() => {
153+
vi.clearAllMocks()
154+
vi.mocked(useTenantSlug).mockReturnValue('test-tenant')
155+
})
156+
157+
it('returns account data on success', async () => {
158+
mockRetrieveInternalAccount.mockResolvedValue({
159+
facility: {
160+
accountId: 'acc-001',
161+
accountCode: 'CLR-GBP-001',
162+
name: 'GBP Clearing',
163+
behaviorClass: 'CLEARING',
164+
instrumentCode: 'GBP',
165+
accountStatus: 1,
166+
description: 'Test account',
167+
createdAt: { seconds: BigInt(1700000000), nanos: 0 },
168+
updatedAt: { seconds: BigInt(1700000001), nanos: 0 },
169+
},
170+
})
171+
172+
const { result } = renderHook(() => useInternalAccountDetail('acc-001'), {
173+
wrapper: createWrapper(),
174+
})
175+
176+
await waitFor(() => expect(result.current.isSuccess).toBe(true))
177+
178+
expect(result.current.data).toMatchObject({
179+
accountId: 'acc-001',
180+
accountCode: 'CLR-GBP-001',
181+
name: 'GBP Clearing',
182+
behaviorClass: 'CLEARING',
183+
instrumentCode: 'GBP',
184+
accountStatus: 1,
185+
description: 'Test account',
186+
})
187+
})
188+
189+
it('returns null when facility is missing in response', async () => {
190+
mockRetrieveInternalAccount.mockResolvedValue({ facility: null })
191+
192+
const { result } = renderHook(() => useInternalAccountDetail('acc-001'), {
193+
wrapper: createWrapper(),
194+
})
195+
196+
await waitFor(() => expect(result.current.isSuccess).toBe(true))
197+
expect(result.current.data).toBeNull()
198+
})
199+
200+
it('returns null for NotFound error', async () => {
201+
const { ConnectError, Code } = await import('@connectrpc/connect')
202+
mockRetrieveInternalAccount.mockRejectedValue(
203+
new ConnectError('not found', Code.NotFound),
204+
)
205+
206+
const { result } = renderHook(() => useInternalAccountDetail('acc-missing'), {
207+
wrapper: createWrapper(),
208+
})
209+
210+
await waitFor(() => expect(result.current.isSuccess).toBe(true))
211+
expect(result.current.data).toBeNull()
212+
})
213+
214+
it('propagates non-NotFound errors', async () => {
215+
const { ConnectError, Code } = await import('@connectrpc/connect')
216+
mockRetrieveInternalAccount.mockRejectedValue(
217+
new ConnectError('internal error', Code.Internal),
218+
)
219+
220+
const { result } = renderHook(() => useInternalAccountDetail('acc-001'), {
221+
wrapper: createWrapper(),
222+
})
223+
224+
await waitFor(() => expect(result.current.isError).toBe(true))
225+
expect(result.current.error).toBeDefined()
226+
})
227+
228+
it('is disabled when accountId is undefined', () => {
229+
const { result } = renderHook(() => useInternalAccountDetail(undefined), {
230+
wrapper: createWrapper(),
231+
})
232+
233+
expect(result.current.isFetching).toBe(false)
234+
expect(mockRetrieveInternalAccount).not.toHaveBeenCalled()
235+
})
236+
237+
it('is disabled when tenantSlug is null', () => {
238+
vi.mocked(useTenantSlug).mockReturnValue(null)
239+
240+
const { result } = renderHook(() => useInternalAccountDetail('acc-001'), {
241+
wrapper: createWrapper(),
242+
})
243+
244+
expect(result.current.isFetching).toBe(false)
245+
expect(mockRetrieveInternalAccount).not.toHaveBeenCalled()
246+
})
247+
})

0 commit comments

Comments
 (0)