Skip to content

Commit 794dfd4

Browse files
committed
tests: providers
1 parent ac692d4 commit 794dfd4

File tree

4 files changed

+765
-0
lines changed

4 files changed

+765
-0
lines changed
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import React from 'react';
2+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
3+
import { cleanup, render, screen, waitFor } from '@testing-library/react';
4+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5+
import { AuthenticationProvider } from '@lib/contexts/authentication/authentication-provider';
6+
import { useAuthenticationContext } from '@lib/contexts/authentication/hook';
7+
import { ApiAccount } from '@typedefs/blockchain';
8+
import { KycStatus } from '@/typedefs/profile';
9+
import { config } from '@lib/config';
10+
11+
const apiMocks = vi.hoisted(() => ({
12+
getAccount: vi.fn(),
13+
}));
14+
15+
const accountMocks = vi.hoisted(() => ({
16+
state: {
17+
address: '0xabc',
18+
isConnected: true,
19+
},
20+
}));
21+
22+
const connectKitMocks = vi.hoisted(() => ({
23+
isSignedIn: false,
24+
modalOpen: false,
25+
openSIWE: vi.fn(),
26+
}));
27+
28+
vi.mock('@lib/api/backend', () => ({
29+
getAccount: apiMocks.getAccount,
30+
}));
31+
32+
vi.mock('wagmi', () => ({
33+
useAccount: () => accountMocks.state,
34+
}));
35+
36+
vi.mock('connectkit', () => ({
37+
useSIWE: () => ({
38+
isSignedIn: connectKitMocks.isSignedIn,
39+
}),
40+
useModal: () => ({
41+
open: connectKitMocks.modalOpen,
42+
openSIWE: connectKitMocks.openSIWE,
43+
}),
44+
}));
45+
46+
const Consumer = () => {
47+
const context = useAuthenticationContext();
48+
49+
if (!context) {
50+
return <div>missing</div>;
51+
}
52+
53+
return (
54+
<div>
55+
<div data-testid="account-email">{context.account?.email ?? 'none'}</div>
56+
<div data-testid="account-error">{context.accountFetchError?.message ?? 'none'}</div>
57+
</div>
58+
);
59+
};
60+
61+
const renderWithClient = () => {
62+
const client = new QueryClient({
63+
defaultOptions: {
64+
queries: {
65+
retry: false,
66+
},
67+
},
68+
});
69+
70+
return render(
71+
<QueryClientProvider client={client}>
72+
<AuthenticationProvider>
73+
<Consumer />
74+
</AuthenticationProvider>
75+
</QueryClientProvider>,
76+
);
77+
};
78+
79+
describe('AuthenticationProvider', () => {
80+
beforeEach(() => {
81+
connectKitMocks.isSignedIn = false;
82+
connectKitMocks.modalOpen = false;
83+
accountMocks.state = {
84+
address: '0xabc',
85+
isConnected: true,
86+
};
87+
apiMocks.getAccount.mockReset();
88+
connectKitMocks.openSIWE.mockReset();
89+
});
90+
91+
afterEach(() => {
92+
cleanup();
93+
});
94+
95+
it('opens SIWE when connected but signed out', async () => {
96+
renderWithClient();
97+
98+
await waitFor(() => {
99+
expect(connectKitMocks.openSIWE).toHaveBeenCalled();
100+
});
101+
});
102+
103+
it('fetches the account after sign-in', async () => {
104+
const account: ApiAccount = {
105+
email: 'user@example.com',
106+
emailConfirmed: true,
107+
pendingEmail: '',
108+
address: '0xabc',
109+
applicantType: 'individual',
110+
uuid: 'uuid',
111+
kycStatus: KycStatus.Approved,
112+
isActive: true,
113+
isBlacklisted: false,
114+
blacklistedReason: '',
115+
receiveUpdates: false,
116+
referral: null,
117+
usdBuyLimit: 0,
118+
vatPercentage: 0,
119+
viesRegistered: false,
120+
};
121+
122+
connectKitMocks.isSignedIn = true;
123+
apiMocks.getAccount.mockResolvedValue(account);
124+
125+
renderWithClient();
126+
127+
await waitFor(() => {
128+
expect(apiMocks.getAccount).toHaveBeenCalled();
129+
});
130+
131+
expect(screen.getByTestId('account-email').textContent).toBe('user@example.com');
132+
});
133+
134+
it('skips SIWE when connected to the safe address', async () => {
135+
accountMocks.state = {
136+
address: config.safeAddress,
137+
isConnected: true,
138+
};
139+
140+
renderWithClient();
141+
142+
await waitFor(() => {
143+
expect(connectKitMocks.openSIWE).not.toHaveBeenCalled();
144+
});
145+
});
146+
147+
it('exposes errors when fetching the account fails', async () => {
148+
connectKitMocks.isSignedIn = true;
149+
apiMocks.getAccount.mockRejectedValue(new Error('Account fetch failed'));
150+
151+
renderWithClient();
152+
153+
await waitFor(() => {
154+
expect(screen.getByTestId('account-error').textContent).toBe('Account fetch failed');
155+
});
156+
});
157+
});
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import React, { useState } from 'react';
2+
import { cleanup, render, screen, waitFor } from '@testing-library/react';
3+
import userEvent from '@testing-library/user-event';
4+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5+
import { BlockchainProvider } from '@lib/contexts/blockchain/blockchain-provider';
6+
import { useBlockchainContext } from '@lib/contexts/blockchain/hook';
7+
8+
const wagmiMocks = vi.hoisted(() => ({
9+
account: {
10+
address: undefined as string | undefined,
11+
},
12+
publicClient: undefined as any,
13+
}));
14+
15+
const toastMocks = vi.hoisted(() => ({
16+
promise: vi.fn(),
17+
}));
18+
19+
vi.mock('wagmi', () => ({
20+
useAccount: () => wagmiMocks.account,
21+
usePublicClient: () => wagmiMocks.publicClient,
22+
}));
23+
24+
vi.mock('react-hot-toast', () => ({
25+
default: toastMocks,
26+
}));
27+
28+
const Consumer = ({ tokenAddress }: { tokenAddress: string }) => {
29+
const context = useBlockchainContext();
30+
const [balance, setBalance] = useState('none');
31+
const [licenses, setLicenses] = useState('none');
32+
const [txStatus, setTxStatus] = useState('idle');
33+
34+
if (!context) {
35+
return <div>missing</div>;
36+
}
37+
38+
return (
39+
<div>
40+
<div data-testid="balance">{balance}</div>
41+
<div data-testid="licenses">{licenses}</div>
42+
<div data-testid="txStatus">{txStatus}</div>
43+
44+
<button
45+
type="button"
46+
onClick={async () => {
47+
const value = await context.fetchErc20Balance(tokenAddress as any);
48+
setBalance(value.toString());
49+
}}
50+
>
51+
FetchBalance
52+
</button>
53+
<button
54+
type="button"
55+
onClick={async () => {
56+
const result = await context.fetchLicenses();
57+
setLicenses(String(result.length));
58+
}}
59+
>
60+
FetchLicenses
61+
</button>
62+
<button
63+
type="button"
64+
onClick={async () => {
65+
try {
66+
await context.watchTx('0xtx', {
67+
waitForTransactionReceipt: vi.fn().mockResolvedValue({
68+
status: 'success',
69+
transactionHash: '0xtx',
70+
}),
71+
});
72+
setTxStatus('success');
73+
} catch {
74+
setTxStatus('failed');
75+
}
76+
}}
77+
>
78+
WatchTx
79+
</button>
80+
<button
81+
type="button"
82+
onClick={async () => {
83+
try {
84+
await context.watchTx('0xtx', {
85+
waitForTransactionReceipt: vi.fn().mockResolvedValue({
86+
status: 'reverted',
87+
transactionHash: '0xtx',
88+
}),
89+
});
90+
setTxStatus('success');
91+
} catch {
92+
setTxStatus('failed');
93+
}
94+
}}
95+
>
96+
WatchTxFail
97+
</button>
98+
</div>
99+
);
100+
};
101+
102+
describe('BlockchainProvider', () => {
103+
beforeEach(() => {
104+
wagmiMocks.account = { address: undefined };
105+
wagmiMocks.publicClient = undefined;
106+
toastMocks.promise.mockReset();
107+
});
108+
109+
afterEach(() => {
110+
cleanup();
111+
});
112+
113+
it('returns defaults without a connected wallet', async () => {
114+
const user = userEvent.setup();
115+
116+
render(
117+
<BlockchainProvider>
118+
<Consumer tokenAddress="0xToken" />
119+
</BlockchainProvider>,
120+
);
121+
122+
await user.click(screen.getByRole('button', { name: 'FetchBalance' }));
123+
await user.click(screen.getByRole('button', { name: 'FetchLicenses' }));
124+
125+
await waitFor(() => {
126+
expect(screen.getByTestId('balance').textContent).toBe('0');
127+
expect(screen.getByTestId('licenses').textContent).toBe('0');
128+
});
129+
130+
expect(toastMocks.promise).not.toHaveBeenCalled();
131+
});
132+
133+
it('fetches balances and licenses with a public client', async () => {
134+
const user = userEvent.setup();
135+
const readContract = vi.fn();
136+
137+
readContract
138+
.mockResolvedValueOnce(123n)
139+
.mockResolvedValueOnce([{ licenseId: 1n, nodeAddress: '0xnode' }])
140+
.mockResolvedValueOnce([{ licenseId: 2n, nodeAddress: '0xnode2' }]);
141+
142+
wagmiMocks.account = { address: '0xabc' };
143+
wagmiMocks.publicClient = {
144+
readContract,
145+
};
146+
147+
render(
148+
<BlockchainProvider>
149+
<Consumer tokenAddress="0xToken" />
150+
</BlockchainProvider>,
151+
);
152+
153+
await user.click(screen.getByRole('button', { name: 'FetchBalance' }));
154+
await user.click(screen.getByRole('button', { name: 'FetchLicenses' }));
155+
156+
await waitFor(() => {
157+
expect(readContract).toHaveBeenCalled();
158+
});
159+
160+
expect(readContract).toHaveBeenCalledTimes(3);
161+
});
162+
163+
it('wraps transaction watching with toast feedback', async () => {
164+
const user = userEvent.setup();
165+
166+
render(
167+
<BlockchainProvider>
168+
<Consumer tokenAddress="0xToken" />
169+
</BlockchainProvider>,
170+
);
171+
172+
await user.click(screen.getByRole('button', { name: 'WatchTx' }));
173+
174+
expect(toastMocks.promise).toHaveBeenCalledWith(expect.any(Promise), expect.any(Object), expect.any(Object));
175+
});
176+
177+
it('surfaces failures when a transaction is reverted', async () => {
178+
const user = userEvent.setup();
179+
180+
render(
181+
<BlockchainProvider>
182+
<Consumer tokenAddress="0xToken" />
183+
</BlockchainProvider>,
184+
);
185+
186+
await user.click(screen.getByRole('button', { name: 'WatchTxFail' }));
187+
188+
await waitFor(() => {
189+
expect(screen.getByTestId('txStatus').textContent).toBe('failed');
190+
});
191+
192+
expect(toastMocks.promise).toHaveBeenCalledWith(expect.any(Promise), expect.any(Object), expect.any(Object));
193+
});
194+
});

0 commit comments

Comments
 (0)