Skip to content

Commit 14e04e2

Browse files
committed
test: update existing backup and onboarding tests
1 parent 9ed2034 commit 14e04e2

15 files changed

Lines changed: 336 additions & 85 deletions
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import { renderHook, act } from "@testing-library/react-native"
2+
3+
import { useBackupNudgeState } from "@app/hooks/use-backup-nudge-state"
4+
5+
const mockBackupState = jest.fn()
6+
const mockActiveWallet = jest.fn()
7+
const mockRemoteConfig = jest.fn()
8+
const mockGetItem = jest.fn()
9+
const mockSetItem = jest.fn()
10+
11+
jest.mock("@app/self-custodial/providers/backup-state-provider", () => ({
12+
BackupStatus: { None: "none", Completed: "completed" },
13+
useBackupState: () => mockBackupState(),
14+
}))
15+
16+
jest.mock("@app/hooks/use-active-wallet", () => ({
17+
useActiveWallet: () => mockActiveWallet(),
18+
}))
19+
20+
jest.mock("@app/config/feature-flags-context", () => ({
21+
useRemoteConfig: () => mockRemoteConfig(),
22+
}))
23+
24+
jest.mock("@react-native-async-storage/async-storage", () => ({
25+
getItem: (...args: string[]) => mockGetItem(...args),
26+
setItem: (...args: string[]) => mockSetItem(...args),
27+
}))
28+
29+
const defaultBackupState = { backupState: { status: "none", method: null } }
30+
const completedBackupState = { backupState: { status: "completed", method: "manual" } }
31+
const selfCustodialWallet = {
32+
accountType: "self-custodial",
33+
wallets: [{ walletCurrency: "BTC", balance: { amount: 3000 } }],
34+
}
35+
const custodialWallet = {
36+
accountType: "custodial",
37+
wallets: [{ walletCurrency: "BTC", balance: { amount: 50000 } }],
38+
}
39+
const defaultConfig = {
40+
backupNudgeBannerThreshold: 2100,
41+
backupNudgeModalThreshold: 21000,
42+
}
43+
44+
describe("useBackupNudgeState", () => {
45+
beforeEach(() => {
46+
jest.clearAllMocks()
47+
mockBackupState.mockReturnValue(defaultBackupState)
48+
mockActiveWallet.mockReturnValue(selfCustodialWallet)
49+
mockRemoteConfig.mockReturnValue(defaultConfig)
50+
mockGetItem.mockResolvedValue(null)
51+
mockSetItem.mockResolvedValue(undefined)
52+
})
53+
54+
it("shows banner when balance >= banner threshold and not backed up", async () => {
55+
const { result } = renderHook(() => useBackupNudgeState())
56+
57+
await act(async () => {})
58+
59+
expect(result.current.shouldShowBanner).toBe(true)
60+
expect(result.current.shouldShowModal).toBe(false)
61+
})
62+
63+
it("shows modal when balance >= modal threshold", async () => {
64+
mockActiveWallet.mockReturnValue({
65+
accountType: "self-custodial",
66+
wallets: [{ walletCurrency: "BTC", balance: { amount: 22000 } }],
67+
})
68+
69+
const { result } = renderHook(() => useBackupNudgeState())
70+
71+
await act(async () => {})
72+
73+
expect(result.current.shouldShowModal).toBe(true)
74+
expect(result.current.shouldShowBanner).toBe(false)
75+
})
76+
77+
it("shows nothing when backed up", async () => {
78+
mockBackupState.mockReturnValue(completedBackupState)
79+
80+
const { result } = renderHook(() => useBackupNudgeState())
81+
82+
await act(async () => {})
83+
84+
expect(result.current.shouldShowBanner).toBe(false)
85+
expect(result.current.shouldShowModal).toBe(false)
86+
expect(result.current.shouldShowSettingsBanner).toBe(false)
87+
})
88+
89+
it("shows nothing for custodial users", async () => {
90+
mockActiveWallet.mockReturnValue(custodialWallet)
91+
92+
const { result } = renderHook(() => useBackupNudgeState())
93+
94+
await act(async () => {})
95+
96+
expect(result.current.shouldShowBanner).toBe(false)
97+
expect(result.current.shouldShowModal).toBe(false)
98+
})
99+
100+
it("shows settings banner for unbacked self-custodial", async () => {
101+
const { result } = renderHook(() => useBackupNudgeState())
102+
103+
await act(async () => {})
104+
105+
expect(result.current.shouldShowSettingsBanner).toBe(true)
106+
})
107+
108+
it("dismisses banner and persists to AsyncStorage", async () => {
109+
const { result } = renderHook(() => useBackupNudgeState())
110+
111+
await act(async () => {})
112+
113+
act(() => {
114+
result.current.dismissBanner()
115+
})
116+
117+
expect(result.current.shouldShowBanner).toBe(false)
118+
expect(mockSetItem).toHaveBeenCalledWith("backupNudgeDismissedAt", expect.any(String))
119+
})
120+
121+
it("loads dismissed state from AsyncStorage", async () => {
122+
mockGetItem.mockResolvedValue(String(Date.now()))
123+
124+
const { result } = renderHook(() => useBackupNudgeState())
125+
126+
await act(async () => {})
127+
128+
expect(result.current.shouldShowBanner).toBe(false)
129+
})
130+
131+
it("shows banner again after 24h cooldown", async () => {
132+
mockGetItem.mockResolvedValue(String(Date.now() - 25 * 60 * 60 * 1000))
133+
134+
const { result } = renderHook(() => useBackupNudgeState())
135+
136+
await act(async () => {})
137+
138+
expect(result.current.shouldShowBanner).toBe(true)
139+
})
140+
})

__tests__/hooks/use-keychain-backup.spec.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ import { renderHook, act } from "@testing-library/react-native"
33
import { useKeychainBackup } from "@app/hooks/use-keychain-backup"
44

55
const mockSetGenericPassword = jest.fn()
6+
const mockGetGenericPassword = jest.fn()
67

78
jest.mock("react-native-keychain", () => ({
89
setGenericPassword: (...args: readonly unknown[]) => mockSetGenericPassword(...args),
10+
getGenericPassword: (...args: readonly unknown[]) => mockGetGenericPassword(...args),
911
ACCESSIBLE: {
1012
WHEN_UNLOCKED_THIS_DEVICE_ONLY: "AccessibleWhenUnlockedThisDeviceOnly",
1113
},
@@ -37,7 +39,7 @@ describe("useKeychainBackup", () => {
3739
}),
3840
)
3941
expect(result.current.loading).toBe(false)
40-
expect(result.current.error).toBeUndefined()
42+
expect(result.current.error).toBeNull()
4143
})
4244

4345
it("returns false when keychain save fails", async () => {
@@ -82,4 +84,46 @@ describe("useKeychainBackup", () => {
8284
expect(result.current.error).toBe("Unknown error")
8385
})
8486
})
87+
88+
describe("read", () => {
89+
it("returns password from keychain", async () => {
90+
mockGetGenericPassword.mockResolvedValue({ password: "test mnemonic" })
91+
92+
const { result } = renderHook(() => useKeychainBackup("test-service"))
93+
94+
let value: string | null = null
95+
await act(async () => {
96+
value = await result.current.read()
97+
})
98+
99+
expect(value).toBe("test mnemonic")
100+
expect(mockGetGenericPassword).toHaveBeenCalledWith({ service: "test-service" })
101+
})
102+
103+
it("returns null when no credentials found", async () => {
104+
mockGetGenericPassword.mockResolvedValue(false)
105+
106+
const { result } = renderHook(() => useKeychainBackup("test-service"))
107+
108+
let value: string | null = null
109+
await act(async () => {
110+
value = await result.current.read()
111+
})
112+
113+
expect(value).toBeNull()
114+
})
115+
116+
it("returns null on error", async () => {
117+
mockGetGenericPassword.mockRejectedValue(new Error("keychain error"))
118+
119+
const { result } = renderHook(() => useKeychainBackup("test-service"))
120+
121+
let value: string | null = null
122+
await act(async () => {
123+
value = await result.current.read()
124+
})
125+
126+
expect(value).toBeNull()
127+
})
128+
})
85129
})

__tests__/screens/account-type-selection/account-type-selection.spec.tsx

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import React from "react"
2-
import { Alert } from "react-native"
32
import { fireEvent, render } from "@testing-library/react-native"
43

54
import { AccountTypeSelectionScreen } from "@app/screens/account-type-selection"
@@ -149,19 +148,15 @@ describe("AccountTypeSelectionScreen", () => {
149148
})
150149
})
151150

152-
it("shows alert for self-custodial restore (not yet implemented)", () => {
151+
it("navigates to restore method screen for self-custodial restore", () => {
153152
mockMode.mockReturnValue("restore")
154-
const alertSpy = jest.spyOn(Alert, "alert")
155153

156154
const { getByTestId } = render(<AccountTypeSelectionScreen />)
157155

158156
fireEvent.press(getByTestId("self-custodial-option"))
159157
fireEvent.press(getByTestId("continue-button"))
160158

161-
expect(alertSpy).toHaveBeenCalledWith(
162-
"Coming soon",
163-
"Restore flow will be available in a future update.",
164-
)
159+
expect(mockNavigate).toHaveBeenCalledWith("sparkRestoreMethodScreen")
165160
})
166161

167162
it("does not navigate when nothing selected", () => {

__tests__/screens/home.spec.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,20 @@ import {
1414

1515
let currentMocks: MockedResponse[] = []
1616

17+
jest.mock("@app/hooks/use-backup-nudge-state", () => ({
18+
useBackupNudgeState: () => ({
19+
shouldShowBanner: false,
20+
shouldShowModal: false,
21+
shouldShowSettingsBanner: false,
22+
dismissBanner: jest.fn(),
23+
}),
24+
}))
25+
26+
jest.mock("@app/screens/spark-onboarding/trust-model-screen", () => ({
27+
SparkTrustModelScreen: () => null,
28+
useTrustModelSeen: () => ({ seen: true, markAsSeen: jest.fn() }),
29+
}))
30+
1731
// eslint-disable-next-line prefer-const
1832
let mockActiveWalletOverride: Record<string, unknown> | null = null
1933

__tests__/screens/settings-screen/settings-screen.spec.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
jest.mock("@app/hooks/use-backup-nudge-state", () => ({
2+
useBackupNudgeState: () => ({
3+
shouldShowBanner: false,
4+
shouldShowModal: false,
5+
shouldShowSettingsBanner: false,
6+
dismissBanner: jest.fn(),
7+
}),
8+
}))
9+
110
import React from "react"
211
import { TouchableOpacity, View } from "react-native"
312
import { act, fireEvent, render, screen, within } from "@testing-library/react-native"

__tests__/screens/spark-onboarding/backup-alerts-screen.spec.tsx

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,10 @@ describe("SparkBackupAlertsScreen", () => {
4343
</ContextForScreen>,
4444
)
4545

46-
expect(getByText(LL.SparkOnboarding.ManualBackup.Alerts.title())).toBeTruthy()
47-
expect(getByText(LL.SparkOnboarding.ManualBackup.Alerts.check1())).toBeTruthy()
48-
expect(queryByText(LL.SparkOnboarding.ManualBackup.Alerts.check2())).toBeNull()
49-
expect(queryByText(LL.SparkOnboarding.ManualBackup.Alerts.check3())).toBeNull()
46+
expect(getByText(LL.BackupScreen.ManualBackup.Alerts.title())).toBeTruthy()
47+
expect(getByText(LL.BackupScreen.ManualBackup.Alerts.check1())).toBeTruthy()
48+
expect(queryByText(LL.BackupScreen.ManualBackup.Alerts.check2())).toBeNull()
49+
expect(queryByText(LL.BackupScreen.ManualBackup.Alerts.check3())).toBeNull()
5050
})
5151

5252
it("reveals second checkbox after checking first", () => {
@@ -56,8 +56,8 @@ describe("SparkBackupAlertsScreen", () => {
5656
</ContextForScreen>,
5757
)
5858

59-
fireEvent.press(getByText(LL.SparkOnboarding.ManualBackup.Alerts.check1()))
60-
expect(getByText(LL.SparkOnboarding.ManualBackup.Alerts.check2())).toBeTruthy()
59+
fireEvent.press(getByText(LL.BackupScreen.ManualBackup.Alerts.check1()))
60+
expect(getByText(LL.BackupScreen.ManualBackup.Alerts.check2())).toBeTruthy()
6161
})
6262

6363
it("reveals third checkbox after checking second", () => {
@@ -67,9 +67,9 @@ describe("SparkBackupAlertsScreen", () => {
6767
</ContextForScreen>,
6868
)
6969

70-
fireEvent.press(getByText(LL.SparkOnboarding.ManualBackup.Alerts.check1()))
71-
fireEvent.press(getByText(LL.SparkOnboarding.ManualBackup.Alerts.check2()))
72-
expect(getByText(LL.SparkOnboarding.ManualBackup.Alerts.check3())).toBeTruthy()
70+
fireEvent.press(getByText(LL.BackupScreen.ManualBackup.Alerts.check1()))
71+
fireEvent.press(getByText(LL.BackupScreen.ManualBackup.Alerts.check2()))
72+
expect(getByText(LL.BackupScreen.ManualBackup.Alerts.check3())).toBeTruthy()
7373
})
7474

7575
it("continue button is disabled when not all checkboxes are checked", () => {
@@ -90,9 +90,9 @@ describe("SparkBackupAlertsScreen", () => {
9090
</ContextForScreen>,
9191
)
9292

93-
fireEvent.press(getByText(LL.SparkOnboarding.ManualBackup.Alerts.check1()))
94-
fireEvent.press(getByText(LL.SparkOnboarding.ManualBackup.Alerts.check2()))
95-
fireEvent.press(getByText(LL.SparkOnboarding.ManualBackup.Alerts.check3()))
93+
fireEvent.press(getByText(LL.BackupScreen.ManualBackup.Alerts.check1()))
94+
fireEvent.press(getByText(LL.BackupScreen.ManualBackup.Alerts.check2()))
95+
fireEvent.press(getByText(LL.BackupScreen.ManualBackup.Alerts.check3()))
9696

9797
fireEvent.press(getByText(LL.common.continue()))
9898
expect(mockNavigate).toHaveBeenCalledWith("sparkBackupPhraseScreen", { step: 1 })
@@ -105,14 +105,14 @@ describe("SparkBackupAlertsScreen", () => {
105105
</ContextForScreen>,
106106
)
107107

108-
fireEvent.press(getByText(LL.SparkOnboarding.ManualBackup.Alerts.check1()))
109-
fireEvent.press(getByText(LL.SparkOnboarding.ManualBackup.Alerts.check2()))
108+
fireEvent.press(getByText(LL.BackupScreen.ManualBackup.Alerts.check1()))
109+
fireEvent.press(getByText(LL.BackupScreen.ManualBackup.Alerts.check2()))
110110

111111
// Uncheck first — second and third should remain visible
112-
fireEvent.press(getByText(LL.SparkOnboarding.ManualBackup.Alerts.check1()))
112+
fireEvent.press(getByText(LL.BackupScreen.ManualBackup.Alerts.check1()))
113113

114-
expect(getByText(LL.SparkOnboarding.ManualBackup.Alerts.check2())).toBeTruthy()
115-
expect(getByText(LL.SparkOnboarding.ManualBackup.Alerts.check3())).toBeTruthy()
114+
expect(getByText(LL.BackupScreen.ManualBackup.Alerts.check2())).toBeTruthy()
115+
expect(getByText(LL.BackupScreen.ManualBackup.Alerts.check3())).toBeTruthy()
116116
})
117117

118118
it("saves BackupAlerts checkpoint on mount", () => {

__tests__/screens/spark-onboarding/backup-confirm-screen.spec.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -68,15 +68,15 @@ describe("SparkBackupConfirmScreen", () => {
6868
</ContextForScreen>,
6969
)
7070

71-
expect(getByText(LL.SparkOnboarding.ManualBackup.Confirm.subtitle())).toBeTruthy()
71+
expect(getByText(LL.BackupScreen.ManualBackup.Confirm.subtitle())).toBeTruthy()
7272
expect(
73-
getByPlaceholderText(`${LL.SparkOnboarding.ManualBackup.Confirm.enterWord()} 1`),
73+
getByPlaceholderText(`${LL.BackupScreen.ManualBackup.Confirm.enterWord()} 1`),
7474
).toBeTruthy()
7575
expect(
76-
getByPlaceholderText(`${LL.SparkOnboarding.ManualBackup.Confirm.enterWord()} 5`),
76+
getByPlaceholderText(`${LL.BackupScreen.ManualBackup.Confirm.enterWord()} 5`),
7777
).toBeTruthy()
7878
expect(
79-
getByPlaceholderText(`${LL.SparkOnboarding.ManualBackup.Confirm.enterWord()} 9`),
79+
getByPlaceholderText(`${LL.BackupScreen.ManualBackup.Confirm.enterWord()} 9`),
8080
).toBeTruthy()
8181
})
8282

@@ -87,7 +87,7 @@ describe("SparkBackupConfirmScreen", () => {
8787
</ContextForScreen>,
8888
)
8989

90-
expect(getByText(LL.SparkOnboarding.ManualBackup.Confirm.enterWords())).toBeTruthy()
90+
expect(getByText(LL.BackupScreen.ManualBackup.Confirm.enterWords())).toBeTruthy()
9191
})
9292

9393
it("shows autocomplete suggestions when typing 3+ characters", () => {
@@ -98,7 +98,7 @@ describe("SparkBackupConfirmScreen", () => {
9898
)
9999

100100
fireEvent.changeText(
101-
getByPlaceholderText(`${LL.SparkOnboarding.ManualBackup.Confirm.enterWord()} 1`),
101+
getByPlaceholderText(`${LL.BackupScreen.ManualBackup.Confirm.enterWord()} 1`),
102102
"you",
103103
)
104104

@@ -114,7 +114,7 @@ describe("SparkBackupConfirmScreen", () => {
114114
)
115115

116116
const input = getByPlaceholderText(
117-
`${LL.SparkOnboarding.ManualBackup.Confirm.enterWord()} 1`,
117+
`${LL.BackupScreen.ManualBackup.Confirm.enterWord()} 1`,
118118
)
119119
fireEvent.changeText(input, "you")
120120
fireEvent.press(getByText("youth"))
@@ -130,7 +130,7 @@ describe("SparkBackupConfirmScreen", () => {
130130
)
131131

132132
fireEvent.changeText(
133-
getByPlaceholderText(`${LL.SparkOnboarding.ManualBackup.Confirm.enterWord()} 1`),
133+
getByPlaceholderText(`${LL.BackupScreen.ManualBackup.Confirm.enterWord()} 1`),
134134
"you",
135135
)
136136
fireEvent.press(getByText("youth"))

0 commit comments

Comments
 (0)