Skip to content

Commit 53f027e

Browse files
Add Custom Fee Rate for Withdrawal
Closes #36
1 parent 3cdbaae commit 53f027e

3 files changed

Lines changed: 502 additions & 125 deletions

File tree

Lines changed: 308 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,318 @@
1-
import { fireEvent, screen, waitFor } from '@testing-library/react';
1+
import { act, fireEvent, screen, waitFor } from '@testing-library/react';
2+
import { CLNService } from '../../../services/http.service';
23
import { mockAppStore } from '../../../utilities/test-utilities/mockData';
34
import { renderWithProviders } from '../../../utilities/test-utilities/mockStore';
45
import BTCWithdraw from './BTCWithdraw';
56

6-
describe('BTCWithdraw component ', () => {
7-
it('should show withdraw card when clicking withdraw action from BTC card', async () => {
8-
await renderWithProviders(<BTCWithdraw />, { preloadedState: mockAppStore, initialRoute: ['/cln'] });
7+
const mockOnClose = jest.fn();
98

10-
// Initial state
11-
expect(screen.getByTestId('btc-wallet-balance-card')).toBeInTheDocument();
9+
const renderBTCWithdraw = async () => {
10+
await act(async () => {
11+
renderWithProviders(<BTCWithdraw onClose={mockOnClose} />, {
12+
preloadedState: mockAppStore,
13+
initialRoute: ['/cln'],
14+
});
15+
});
16+
17+
const withdrawBtn = await screen.findByTestId('withdraw-button');
18+
await act(async () => {
19+
fireEvent.click(withdrawBtn);
20+
});
21+
22+
await waitFor(() => {
23+
expect(screen.getByTestId('btc-withdraw-card')).toBeInTheDocument();
24+
});
25+
};
26+
27+
describe('BTCWithdraw component', () => {
28+
29+
beforeEach(async () => {
30+
jest.useFakeTimers({ advanceTimers: true });
31+
await renderBTCWithdraw();
32+
});
33+
34+
afterEach(() => {
35+
mockOnClose.mockClear();
36+
jest.clearAllTimers();
37+
jest.useRealTimers();
38+
});
39+
40+
it('should render the withdraw card with all fields', () => {
41+
expect(screen.getByTestId('btc-withdraw-card')).toBeInTheDocument();
42+
expect(screen.getByLabelText('amount')).toBeInTheDocument();
43+
expect(screen.getByLabelText('address')).toBeInTheDocument();
44+
expect(screen.getByTestId('show-custom-fee-rate')).toBeInTheDocument();
45+
expect(screen.getByTestId('show-custom-fee-rate')).not.toBeChecked();
46+
expect(screen.getByTestId('button-withdraw')).toBeInTheDocument();
47+
expect(screen.getByTestId('button-withdraw')).not.toBeDisabled();
48+
});
49+
50+
it('should render the FeerateRange slider by default with no custom fee rate input', async () => {
51+
await waitFor(() => {
52+
expect(screen.getByTestId('feerate-range')).toBeInTheDocument();
53+
});
54+
expect(screen.queryByLabelText('feeRate')).not.toBeInTheDocument();
55+
expect(screen.queryByTestId('fee-rate-unit')).not.toBeInTheDocument();
56+
});
57+
58+
it('should show custom fee rate input when checkbox is checked', async () => {
59+
const checkbox = screen.getByTestId('show-custom-fee-rate');
60+
expect(checkbox).not.toBeChecked();
61+
62+
await act(async () => {
63+
fireEvent.click(checkbox);
64+
});
1265

13-
// Click the withdraw button
14-
const withdrawButton = screen.getByTestId('withdraw-button');
15-
fireEvent.click(withdrawButton);
1666
await waitFor(() => {
17-
expect(screen.getByTestId('btc-withdraw')).toBeInTheDocument();
18-
expect(screen.getByTestId('button-withdraw')).toBeInTheDocument();
67+
expect(checkbox).toBeChecked();
68+
expect(screen.getByLabelText('feeRate')).toBeInTheDocument();
69+
expect(screen.getByTestId('fee-rate-unit')).toHaveTextContent('perkw');
70+
});
71+
});
72+
73+
it('should hide custom fee rate input and show slider when checkbox is unchecked again', async () => {
74+
const checkbox = screen.getByTestId('show-custom-fee-rate');
75+
76+
await act(async () => { fireEvent.click(checkbox); });
77+
await waitFor(() => expect(screen.getByLabelText('feeRate')).toBeInTheDocument());
78+
79+
await act(async () => { fireEvent.click(checkbox); });
80+
await waitFor(() => {
81+
expect(screen.queryByLabelText('feeRate')).not.toBeInTheDocument();
82+
expect(screen.getByTestId('feerate-range')).toBeInTheDocument();
83+
});
84+
});
85+
86+
it('should not submit when form is empty', async () => {
87+
const mockBtcWithdraw = jest.spyOn(CLNService, 'btcWithdraw').mockResolvedValue({ txid: 'tx123' });
88+
89+
await act(async () => {
90+
fireEvent.click(screen.getByTestId('button-withdraw'));
91+
});
92+
93+
expect(mockBtcWithdraw).not.toHaveBeenCalled();
94+
mockBtcWithdraw.mockRestore();
95+
});
96+
97+
it('should show invalid address error when address is blurred empty', async () => {
98+
fireEvent.change(screen.getByLabelText('address'), { target: { value: '' } });
99+
fireEvent.blur(screen.getByLabelText('address'));
100+
101+
await waitFor(() => {
102+
expect(screen.getByText('Invalid Address')).toBeInTheDocument();
103+
});
104+
});
105+
106+
it('should show invalid amount error when amount is 0', async () => {
107+
fireEvent.change(screen.getByLabelText('amount'), { target: { value: '0' } });
108+
fireEvent.blur(screen.getByLabelText('amount'));
109+
110+
await waitFor(() => {
111+
expect(screen.getByText('Amount should be greater than 0')).toBeInTheDocument();
112+
});
113+
});
114+
115+
it('should show invalid fee rate error when custom fee rate is 0', async () => {
116+
await act(async () => {
117+
fireEvent.click(screen.getByTestId('show-custom-fee-rate'));
118+
});
119+
120+
const feeRateInput = await screen.findByLabelText('feeRate');
121+
fireEvent.change(feeRateInput, { target: { value: '0' } });
122+
fireEvent.blur(feeRateInput);
123+
124+
await waitFor(() => {
125+
expect(screen.getByText('Fee Rate should be greater than 0')).toBeInTheDocument();
126+
});
127+
});
128+
129+
it('should set amount to "All" when Send All is clicked', async () => {
130+
await act(async () => {
131+
fireEvent.click(screen.getByText('Send All'));
132+
});
133+
134+
await waitFor(() => {
135+
expect(screen.getByLabelText('amount')).toHaveValue('All');
136+
expect(screen.getByLabelText('amount')).toBeDisabled();
137+
});
138+
});
139+
140+
it('should clear amount when the close button on Send All is clicked', async () => {
141+
await act(async () => {
142+
fireEvent.click(screen.getByText('Send All'));
143+
});
144+
145+
await waitFor(() => expect(screen.getByLabelText('amount')).toHaveValue('All'));
146+
147+
await act(async () => {
148+
fireEvent.click(document.querySelector('.btn-addon-close') as HTMLElement);
149+
});
150+
151+
await waitFor(() => {
152+
expect(screen.getByLabelText('amount')).toHaveValue(null);
153+
});
154+
});
155+
156+
it('should disable submit button while submission is pending', async () => {
157+
jest.spyOn(CLNService, 'btcWithdraw').mockImplementation(() => new Promise(() => {}));
158+
159+
fireEvent.change(screen.getByLabelText('amount'), { target: { value: '100000' } });
160+
fireEvent.change(screen.getByLabelText('address'), { target: { value: 'bc1qaddress' } });
161+
162+
await act(async () => {
163+
fireEvent.click(screen.getByTestId('button-withdraw'));
164+
});
165+
166+
await waitFor(() => {
167+
expect(screen.getByTestId('button-withdraw')).toBeDisabled();
168+
});
169+
});
170+
171+
it('should show success message after withdraw', async () => {
172+
jest.spyOn(CLNService, 'btcWithdraw').mockResolvedValue({ txid: 'tx123' });
173+
174+
fireEvent.change(screen.getByLabelText('amount'), { target: { value: '100000' } });
175+
fireEvent.change(screen.getByLabelText('address'), { target: { value: 'bc1qaddress' } });
176+
177+
await act(async () => {
178+
fireEvent.click(screen.getByTestId('button-withdraw'));
179+
});
180+
181+
await waitFor(() => {
182+
expect(screen.getByText(/transaction sent with transaction id tx123/i)).toBeInTheDocument();
183+
});
184+
});
185+
186+
it('should show error message when btcWithdraw fails', async () => {
187+
jest.spyOn(CLNService, 'btcWithdraw').mockRejectedValue('Insufficient funds');
188+
189+
fireEvent.change(screen.getByLabelText('amount'), { target: { value: '100000' } });
190+
fireEvent.change(screen.getByLabelText('address'), { target: { value: 'bc1qaddress' } });
191+
192+
await act(async () => {
193+
fireEvent.click(screen.getByTestId('button-withdraw'));
194+
});
195+
196+
await waitFor(() => {
197+
expect(screen.getByTestId('status-alert-message')).toHaveTextContent('Insufficient Funds');
198+
});
199+
});
200+
201+
describe('btcWithdraw fee rate argument', () => {
202+
const fillRequiredFields = () => {
203+
fireEvent.change(screen.getByLabelText('amount'), { target: { value: '100000' } });
204+
fireEvent.change(screen.getByLabelText('address'), { target: { value: 'bc1qaddress' } });
205+
};
206+
207+
describe('when showCustomFeeRate is false (default)', () => {
208+
it('should call btcWithdraw with selFeeRate "normal" by default', async () => {
209+
const mockBtcWithdraw = jest.spyOn(CLNService, 'btcWithdraw').mockResolvedValue({ txid: 'tx123' });
210+
fillRequiredFields();
211+
212+
await act(async () => {
213+
fireEvent.click(screen.getByTestId('button-withdraw'));
214+
});
215+
216+
await waitFor(() => {
217+
expect(mockBtcWithdraw).toHaveBeenCalledWith('bc1qaddress', '100000', 'normal');
218+
});
219+
mockBtcWithdraw.mockRestore();
220+
});
221+
222+
it('should call btcWithdraw with selFeeRate "slow" when slider is set to slow', async () => {
223+
const mockBtcWithdraw = jest.spyOn(CLNService, 'btcWithdraw').mockResolvedValue({ txid: 'tx123' });
224+
fillRequiredFields();
225+
226+
fireEvent.change(screen.getByTestId('feerate-range'), { target: { value: '0' } });
227+
228+
await act(async () => {
229+
fireEvent.click(screen.getByTestId('button-withdraw'));
230+
});
231+
232+
await waitFor(() => {
233+
expect(mockBtcWithdraw).toHaveBeenCalledWith('bc1qaddress', '100000', 'slow');
234+
});
235+
mockBtcWithdraw.mockRestore();
236+
});
237+
238+
it('should call btcWithdraw with selFeeRate "urgent" when slider is set to urgent', async () => {
239+
const mockBtcWithdraw = jest.spyOn(CLNService, 'btcWithdraw').mockResolvedValue({ txid: 'tx123' });
240+
fillRequiredFields();
241+
242+
fireEvent.change(screen.getByTestId('feerate-range'), { target: { value: '2' } });
243+
244+
await act(async () => {
245+
fireEvent.click(screen.getByTestId('button-withdraw'));
246+
});
247+
248+
await waitFor(() => {
249+
expect(mockBtcWithdraw).toHaveBeenCalledWith('bc1qaddress', '100000', 'urgent');
250+
});
251+
mockBtcWithdraw.mockRestore();
252+
});
253+
});
254+
255+
describe('when showCustomFeeRate is true', () => {
256+
it('should call btcWithdraw with feeRateValue + "perkw"', async () => {
257+
const mockBtcWithdraw = jest.spyOn(CLNService, 'btcWithdraw').mockResolvedValue({ txid: 'tx123' });
258+
fillRequiredFields();
259+
260+
await act(async () => {
261+
fireEvent.click(screen.getByTestId('show-custom-fee-rate'));
262+
});
263+
264+
const feeRateInput = await screen.findByLabelText('feeRate');
265+
fireEvent.change(feeRateInput, { target: { value: '500' } });
266+
267+
await act(async () => {
268+
fireEvent.click(screen.getByTestId('button-withdraw'));
269+
});
270+
271+
await waitFor(() => {
272+
expect(mockBtcWithdraw).toHaveBeenCalledWith('bc1qaddress', '100000', '500perkw');
273+
});
274+
mockBtcWithdraw.mockRestore();
275+
});
276+
277+
it('should not call btcWithdraw if custom fee rate value is empty', async () => {
278+
const mockBtcWithdraw = jest.spyOn(CLNService, 'btcWithdraw').mockResolvedValue({ txid: 'tx123' });
279+
fillRequiredFields();
280+
281+
await act(async () => {
282+
fireEvent.click(screen.getByTestId('show-custom-fee-rate'));
283+
});
284+
285+
await act(async () => {
286+
fireEvent.click(screen.getByTestId('button-withdraw'));
287+
});
288+
289+
expect(mockBtcWithdraw).not.toHaveBeenCalled();
290+
mockBtcWithdraw.mockRestore();
291+
});
292+
293+
it('should not use selFeeRate when showCustomFeeRate is true', async () => {
294+
const mockBtcWithdraw = jest.spyOn(CLNService, 'btcWithdraw').mockResolvedValue({ txid: 'tx123' });
295+
fillRequiredFields();
296+
297+
fireEvent.change(screen.getByTestId('feerate-range'), { target: { value: '2' } });
298+
299+
await act(async () => {
300+
fireEvent.click(screen.getByTestId('show-custom-fee-rate'));
301+
});
302+
303+
const feeRateInput = await screen.findByLabelText('feeRate');
304+
fireEvent.change(feeRateInput, { target: { value: '300' } });
305+
306+
await act(async () => {
307+
fireEvent.click(screen.getByTestId('button-withdraw'));
308+
});
309+
310+
await waitFor(() => {
311+
expect(mockBtcWithdraw).toHaveBeenCalledWith('bc1qaddress', '100000', '300perkw');
312+
expect(mockBtcWithdraw).not.toHaveBeenCalledWith(expect.anything(), expect.anything(), 'urgent');
313+
});
314+
mockBtcWithdraw.mockRestore();
315+
});
19316
});
20317
});
21318
});

0 commit comments

Comments
 (0)