Skip to content

Commit bf8ddd4

Browse files
committed
test: usePortfolioBalanceSync
1 parent 4587592 commit bf8ddd4

File tree

1 file changed

+269
-0
lines changed

1 file changed

+269
-0
lines changed
Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
import { act, renderHook } from "tests/testSetup";
2+
import { usePortfolioBalanceSync } from "../usePortfolioBalanceSync";
3+
import { BTC_ACCOUNT, ETH_ACCOUNT } from "LLD/features/__mocks__/accounts.mock";
4+
import * as walletSyncContext from "LLD/features/WalletSync/components/WalletSyncContext";
5+
import { INITIAL_STATE } from "~/renderer/reducers/settings";
6+
import { DEFAULT_PORTFOLIO_RANGE } from "LLD/utils/constants";
7+
import type { PortfolioRange } from "@ledgerhq/types-live";
8+
import * as portfolioReact from "@ledgerhq/live-countervalues-react/portfolio";
9+
import * as countervaluesReact from "@ledgerhq/live-countervalues-react";
10+
import * as bridgeReact from "@ledgerhq/live-common/bridge/react/index";
11+
12+
const dayRange: PortfolioRange = "day";
13+
14+
const mockPoll = jest.fn();
15+
const mockOnUserRefresh = jest.fn();
16+
const mockBridgeSync = jest.fn();
17+
18+
// Bridge and WalletSync: spyOn fails (Cannot redefine property), so we override only the hooks we need
19+
jest.mock("@ledgerhq/live-common/bridge/react/index", () => ({
20+
...jest.requireActual("@ledgerhq/live-common/bridge/react/index"),
21+
useBridgeSync: jest.fn(),
22+
useGlobalSyncState: jest.fn(),
23+
}));
24+
25+
jest.mock("LLD/features/WalletSync/components/WalletSyncContext", () => ({
26+
...jest.requireActual("LLD/features/WalletSync/components/WalletSyncContext"),
27+
useWalletSyncUserState: jest.fn(),
28+
}));
29+
30+
const mockUseBridgeSync = jest.mocked(bridgeReact.useBridgeSync);
31+
const mockUseGlobalSyncState = jest.mocked(bridgeReact.useGlobalSyncState);
32+
const mockUseWalletSyncUserState = jest.mocked(walletSyncContext.useWalletSyncUserState);
33+
34+
const defaultPollingReturn = {
35+
poll: mockPoll,
36+
pending: false,
37+
error: null,
38+
wipe: jest.fn(),
39+
start: jest.fn(),
40+
stop: jest.fn(),
41+
};
42+
43+
const defaultPortfolio = {
44+
balanceHistory: [],
45+
balanceAvailable: true,
46+
availableAccounts: [],
47+
unavailableCurrencies: [],
48+
accounts: [],
49+
range: dayRange,
50+
histories: [],
51+
countervalueReceiveSum: 0,
52+
countervalueSendSum: 0,
53+
countervalueChange: { percentage: 0, value: 0 },
54+
};
55+
56+
const defaultInitialState = {
57+
accounts: [],
58+
settings: {
59+
...INITIAL_STATE,
60+
counterValue: "USD",
61+
selectedTimeRange: dayRange,
62+
},
63+
};
64+
65+
describe("usePortfolioBalanceSync", () => {
66+
beforeEach(() => {
67+
jest.clearAllMocks();
68+
jest.spyOn(portfolioReact, "usePortfolioThrottled").mockReturnValue({
69+
...defaultPortfolio,
70+
balanceAvailable: true,
71+
});
72+
jest.spyOn(countervaluesReact, "useCountervaluesPolling").mockReturnValue({
73+
...defaultPollingReturn,
74+
});
75+
mockUseGlobalSyncState.mockReturnValue({ pending: false, error: null });
76+
mockUseBridgeSync.mockReturnValue(mockBridgeSync);
77+
mockUseWalletSyncUserState.mockReturnValue({
78+
onUserRefresh: mockOnUserRefresh,
79+
visualPending: false,
80+
walletSyncError: null,
81+
});
82+
});
83+
84+
afterEach(() => {
85+
jest.restoreAllMocks();
86+
});
87+
88+
it("returns portfolio, counterValue, balance state, and triggerRefresh", () => {
89+
const { result } = renderHook(() => usePortfolioBalanceSync(), {
90+
initialState: defaultInitialState,
91+
});
92+
93+
expect(result.current).toMatchObject({
94+
portfolio: expect.any(Object),
95+
counterValue: expect.anything(),
96+
balanceAvailable: true,
97+
isColdStart: false,
98+
isBalanceLoading: false,
99+
stableSyncPending: false,
100+
hasCvOrBridgeError: false,
101+
hasWalletSyncError: false,
102+
});
103+
expect(typeof result.current.triggerRefresh).toBe("function");
104+
});
105+
106+
it("uses DEFAULT_PORTFOLIO_RANGE when legacyRange is false", () => {
107+
const usePortfolioThrottledSpy = jest.spyOn(portfolioReact, "usePortfolioThrottled");
108+
109+
renderHook(() => usePortfolioBalanceSync({ legacyRange: false }), {
110+
initialState: defaultInitialState,
111+
});
112+
113+
expect(usePortfolioThrottledSpy).toHaveBeenCalledWith(
114+
expect.objectContaining({
115+
range: DEFAULT_PORTFOLIO_RANGE,
116+
}),
117+
);
118+
});
119+
120+
it("uses selectedTimeRange from store when legacyRange is true", () => {
121+
const usePortfolioThrottledSpy = jest.spyOn(portfolioReact, "usePortfolioThrottled");
122+
123+
renderHook(() => usePortfolioBalanceSync({ legacyRange: true }), {
124+
initialState: {
125+
...defaultInitialState,
126+
settings: {
127+
...defaultInitialState.settings,
128+
selectedTimeRange: "week",
129+
},
130+
},
131+
});
132+
133+
expect(usePortfolioThrottledSpy).toHaveBeenCalledWith(
134+
expect.objectContaining({
135+
range: "week",
136+
}),
137+
);
138+
});
139+
140+
it("returns isColdStart true when hasAccounts and balance is not yet available", () => {
141+
jest.spyOn(portfolioReact, "usePortfolioThrottled").mockReturnValue({
142+
...defaultPortfolio,
143+
balanceAvailable: false,
144+
});
145+
146+
const { result } = renderHook(() => usePortfolioBalanceSync(), {
147+
initialState: { ...defaultInitialState, accounts: [BTC_ACCOUNT] },
148+
});
149+
150+
expect(result.current.isColdStart).toBe(true);
151+
expect(result.current.balanceAvailable).toBe(false);
152+
});
153+
154+
it("returns isColdStart false when no accounts", () => {
155+
jest.spyOn(portfolioReact, "usePortfolioThrottled").mockReturnValue({
156+
...defaultPortfolio,
157+
balanceAvailable: false,
158+
});
159+
160+
const { result } = renderHook(() => usePortfolioBalanceSync(), {
161+
initialState: defaultInitialState,
162+
});
163+
164+
expect(result.current.isColdStart).toBe(false);
165+
});
166+
167+
it("returns isBalanceLoading true when sync is pending", () => {
168+
jest.spyOn(countervaluesReact, "useCountervaluesPolling").mockReturnValue({
169+
...defaultPollingReturn,
170+
pending: true,
171+
});
172+
173+
const { result } = renderHook(() => usePortfolioBalanceSync(), {
174+
initialState: defaultInitialState,
175+
});
176+
177+
expect(result.current.stableSyncPending).toBe(true);
178+
expect(result.current.isBalanceLoading).toBe(true);
179+
});
180+
181+
it("returns hasCvOrBridgeError true when not pending and cvPolling has error", () => {
182+
jest.spyOn(countervaluesReact, "useCountervaluesPolling").mockReturnValue({
183+
...defaultPollingReturn,
184+
error: new Error("CV error"),
185+
});
186+
187+
const { result } = renderHook(() => usePortfolioBalanceSync(), {
188+
initialState: defaultInitialState,
189+
});
190+
191+
expect(result.current.hasCvOrBridgeError).toBe(true);
192+
});
193+
194+
it("returns hasCvOrBridgeError true when not pending and globalSyncState has error", () => {
195+
mockUseGlobalSyncState.mockReturnValue({
196+
pending: false,
197+
error: new Error("Sync error"),
198+
});
199+
200+
const { result } = renderHook(() => usePortfolioBalanceSync(), {
201+
initialState: defaultInitialState,
202+
});
203+
204+
expect(result.current.hasCvOrBridgeError).toBe(true);
205+
});
206+
207+
it("returns hasCvOrBridgeError false when sync is still pending", () => {
208+
jest.spyOn(countervaluesReact, "useCountervaluesPolling").mockReturnValue({
209+
...defaultPollingReturn,
210+
pending: true,
211+
error: new Error("CV error"),
212+
});
213+
214+
const { result } = renderHook(() => usePortfolioBalanceSync(), {
215+
initialState: defaultInitialState,
216+
});
217+
218+
expect(result.current.hasCvOrBridgeError).toBe(false);
219+
});
220+
221+
it("returns hasWalletSyncError true when wallet sync has error", () => {
222+
mockUseWalletSyncUserState.mockReturnValue({
223+
onUserRefresh: mockOnUserRefresh,
224+
visualPending: false,
225+
walletSyncError: new Error("Wallet sync failed"),
226+
});
227+
228+
const { result } = renderHook(() => usePortfolioBalanceSync(), {
229+
initialState: defaultInitialState,
230+
});
231+
232+
expect(result.current.hasWalletSyncError).toBe(true);
233+
});
234+
235+
it("triggerRefresh calls onUserRefresh, poll, and bridgeSync with SYNC_ALL_ACCOUNTS", () => {
236+
const { result } = renderHook(() => usePortfolioBalanceSync(), {
237+
initialState: defaultInitialState,
238+
});
239+
240+
act(() => {
241+
result.current.triggerRefresh();
242+
});
243+
244+
expect(mockOnUserRefresh).toHaveBeenCalledTimes(1);
245+
expect(mockPoll).toHaveBeenCalledTimes(1);
246+
expect(mockBridgeSync).toHaveBeenCalledWith({
247+
type: "SYNC_ALL_ACCOUNTS",
248+
priority: 5,
249+
reason: "user-click",
250+
});
251+
});
252+
253+
it("passes accounts from store to portfolio hook", () => {
254+
const usePortfolioThrottledSpy = jest.spyOn(portfolioReact, "usePortfolioThrottled");
255+
256+
renderHook(() => usePortfolioBalanceSync(), {
257+
initialState: {
258+
...defaultInitialState,
259+
accounts: [BTC_ACCOUNT, ETH_ACCOUNT],
260+
},
261+
});
262+
263+
expect(usePortfolioThrottledSpy).toHaveBeenCalledWith(
264+
expect.objectContaining({
265+
accounts: [BTC_ACCOUNT, ETH_ACCOUNT],
266+
}),
267+
);
268+
});
269+
});

0 commit comments

Comments
 (0)