mirror of
https://github.com/selfxyz/self.git
synced 2026-04-05 03:00:53 -04:00
SELF-483: Enable backup recovery prompts (#834)
* Guard recovery prompts * refactor(app): gate recovery prompts with allow list (#1251) * fix typing * fix header * fix app loading * fix tests * Limit recovery prompts to home allowlist (#1460) * fix test * fix typing pipeline * format and fix linting and tests * tests pass * fix tests * split up testing * save wip * save button fix * fix count * fix modal width * remove consologging * remove depcrecated login count * linting * lint * early return
This commit is contained in:
@@ -2,32 +2,22 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { act, renderHook } from '@testing-library/react-native';
|
||||
|
||||
import { useModal } from '@/hooks/useModal';
|
||||
import { getModalCallbacks } from '@/utils/modalCallbackRegistry';
|
||||
|
||||
jest.mock('@react-navigation/native', () => ({
|
||||
useNavigation: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockNavigate = jest.fn();
|
||||
const mockGoBack = jest.fn();
|
||||
const mockGetState = jest.fn(() => ({
|
||||
routes: [{ name: 'Home' }, { name: 'Modal' }],
|
||||
}));
|
||||
|
||||
describe('useModal', () => {
|
||||
beforeEach(() => {
|
||||
(useNavigation as jest.Mock).mockReturnValue({
|
||||
navigate: mockNavigate,
|
||||
goBack: mockGoBack,
|
||||
getState: mockGetState,
|
||||
// Reset all mocks including the global navigationRef
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Set up the navigation ref mock with proper methods
|
||||
global.mockNavigationRef.isReady.mockReturnValue(true);
|
||||
global.mockNavigationRef.getState.mockReturnValue({
|
||||
routes: [{ name: 'Home' }, { name: 'Modal' }],
|
||||
index: 1,
|
||||
});
|
||||
mockNavigate.mockClear();
|
||||
mockGoBack.mockClear();
|
||||
mockGetState.mockClear();
|
||||
});
|
||||
|
||||
it('should navigate to Modal with callbackId and handle dismissal', () => {
|
||||
@@ -45,8 +35,10 @@ describe('useModal', () => {
|
||||
|
||||
act(() => result.current.showModal());
|
||||
|
||||
expect(mockNavigate).toHaveBeenCalledTimes(1);
|
||||
const params = mockNavigate.mock.calls[0][1];
|
||||
expect(global.mockNavigationRef.navigate).toHaveBeenCalledTimes(1);
|
||||
const [screenName, params] =
|
||||
global.mockNavigationRef.navigate.mock.calls[0];
|
||||
expect(screenName).toBe('Modal');
|
||||
expect(params).toMatchObject({
|
||||
titleText: 'Title',
|
||||
bodyText: 'Body',
|
||||
@@ -58,7 +50,7 @@ describe('useModal', () => {
|
||||
|
||||
act(() => result.current.dismissModal());
|
||||
|
||||
expect(mockGoBack).toHaveBeenCalled();
|
||||
expect(global.mockNavigationRef.goBack).toHaveBeenCalled();
|
||||
expect(onModalDismiss).toHaveBeenCalled();
|
||||
expect(getModalCallbacks(id)).toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import { AppState } from 'react-native';
|
||||
import { act, renderHook, waitFor } from '@testing-library/react-native';
|
||||
|
||||
import { useModal } from '@/hooks/useModal';
|
||||
@@ -9,56 +10,175 @@ import useRecoveryPrompts from '@/hooks/useRecoveryPrompts';
|
||||
import { usePassport } from '@/providers/passportDataProvider';
|
||||
import { useSettingStore } from '@/stores/settingStore';
|
||||
|
||||
const navigationStateListeners: Array<() => void> = [];
|
||||
let isNavigationReady = true;
|
||||
// Use global appStateListeners from jest.setup.js mock
|
||||
const appStateListeners = global.mockAppStateListeners || [];
|
||||
|
||||
jest.mock('@/hooks/useModal');
|
||||
jest.mock('@/providers/passportDataProvider');
|
||||
jest.mock('@/navigation', () => ({
|
||||
navigationRef: {
|
||||
isReady: jest.fn(() => true),
|
||||
navigate: jest.fn(),
|
||||
},
|
||||
navigationRef: global.mockNavigationRef,
|
||||
}));
|
||||
// Use global react-native mock from jest.setup.js - no need to mock here
|
||||
|
||||
const showModal = jest.fn();
|
||||
(useModal as jest.Mock).mockReturnValue({ showModal, visible: false });
|
||||
const getAllDocuments = jest.fn();
|
||||
(usePassport as jest.Mock).mockReturnValue({ getAllDocuments });
|
||||
|
||||
const getAppState = (): {
|
||||
currentState: string;
|
||||
addEventListener: jest.Mock;
|
||||
} =>
|
||||
AppState as unknown as {
|
||||
currentState: string;
|
||||
addEventListener: jest.Mock;
|
||||
};
|
||||
|
||||
describe('useRecoveryPrompts', () => {
|
||||
beforeEach(() => {
|
||||
showModal.mockClear();
|
||||
getAllDocuments.mockResolvedValue({ doc1: {} as any });
|
||||
jest.clearAllMocks();
|
||||
navigationStateListeners.length = 0;
|
||||
appStateListeners.length = 0;
|
||||
isNavigationReady = true;
|
||||
|
||||
// Setup the global navigation ref mock
|
||||
global.mockNavigationRef.isReady.mockImplementation(
|
||||
() => isNavigationReady,
|
||||
);
|
||||
global.mockNavigationRef.getCurrentRoute.mockReturnValue({ name: 'Home' });
|
||||
global.mockNavigationRef.addListener.mockImplementation(
|
||||
(_: string, callback: () => void) => {
|
||||
navigationStateListeners.push(callback);
|
||||
return () => {
|
||||
const index = navigationStateListeners.indexOf(callback);
|
||||
if (index >= 0) {
|
||||
navigationStateListeners.splice(index, 1);
|
||||
}
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
(useModal as jest.Mock).mockReturnValue({ showModal, visible: false });
|
||||
getAllDocuments.mockResolvedValue({
|
||||
doc1: {
|
||||
data: {} as any,
|
||||
metadata: { isRegistered: true } as any,
|
||||
},
|
||||
});
|
||||
const mockAppState = getAppState();
|
||||
mockAppState.currentState = 'active';
|
||||
act(() => {
|
||||
useSettingStore.setState({
|
||||
loginCount: 0,
|
||||
homeScreenViewCount: 0,
|
||||
cloudBackupEnabled: false,
|
||||
hasViewedRecoveryPhrase: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('shows modal on first login', async () => {
|
||||
act(() => {
|
||||
useSettingStore.setState({ loginCount: 1 });
|
||||
});
|
||||
renderHook(() => useRecoveryPrompts());
|
||||
await waitFor(() => {
|
||||
expect(showModal).toHaveBeenCalled();
|
||||
});
|
||||
it('does not show modal before the fifth home view', async () => {
|
||||
for (const count of [1, 2, 3, 4]) {
|
||||
showModal.mockClear();
|
||||
act(() => {
|
||||
useSettingStore.setState({ homeScreenViewCount: count });
|
||||
});
|
||||
renderHook(() => useRecoveryPrompts());
|
||||
await waitFor(() => {
|
||||
expect(showModal).not.toHaveBeenCalled();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('does not show modal when login count is 4', async () => {
|
||||
it('waits for navigation readiness before prompting', async () => {
|
||||
isNavigationReady = false;
|
||||
global.mockNavigationRef.isReady.mockImplementation(
|
||||
() => isNavigationReady,
|
||||
);
|
||||
act(() => {
|
||||
useSettingStore.setState({ loginCount: 4 });
|
||||
useSettingStore.setState({ homeScreenViewCount: 5 });
|
||||
});
|
||||
renderHook(() => useRecoveryPrompts());
|
||||
await waitFor(() => {
|
||||
expect(showModal).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
isNavigationReady = true;
|
||||
navigationStateListeners.forEach(listener => listener());
|
||||
|
||||
await waitFor(() => {
|
||||
expect(showModal).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows modal on eighth login', async () => {
|
||||
it('respects custom allow list overrides', async () => {
|
||||
act(() => {
|
||||
useSettingStore.setState({ loginCount: 8 });
|
||||
useSettingStore.setState({ homeScreenViewCount: 5 });
|
||||
});
|
||||
renderHook(() => useRecoveryPrompts({ allowedRoutes: ['Settings'] }));
|
||||
await waitFor(() => {
|
||||
expect(showModal).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
showModal.mockClear();
|
||||
global.mockNavigationRef.getCurrentRoute.mockReturnValue({
|
||||
name: 'Settings',
|
||||
});
|
||||
|
||||
renderHook(() => useRecoveryPrompts({ allowedRoutes: ['Settings'] }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(showModal).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('prompts when returning from background on eligible route', async () => {
|
||||
// This test verifies that the hook registers an app state listener
|
||||
// and that the prompt logic can be triggered multiple times for different view counts
|
||||
act(() => {
|
||||
useSettingStore.setState({ homeScreenViewCount: 5 });
|
||||
});
|
||||
|
||||
const { rerender, unmount } = renderHook(() => useRecoveryPrompts());
|
||||
|
||||
// Wait for initial prompt
|
||||
await waitFor(() => {
|
||||
expect(showModal).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
// Clear and test with a different login count that should trigger again
|
||||
showModal.mockClear();
|
||||
|
||||
act(() => {
|
||||
useSettingStore.setState({ homeScreenViewCount: 10 }); // next multiple of 5
|
||||
});
|
||||
|
||||
rerender();
|
||||
|
||||
// Wait for second prompt with new login count
|
||||
await waitFor(() => {
|
||||
expect(showModal).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('does not show modal for non-multiple-of-five view counts', async () => {
|
||||
for (const count of [6, 7, 8, 9]) {
|
||||
showModal.mockClear();
|
||||
act(() => {
|
||||
useSettingStore.setState({ homeScreenViewCount: count });
|
||||
});
|
||||
renderHook(() => useRecoveryPrompts());
|
||||
await waitFor(() => {
|
||||
expect(showModal).not.toHaveBeenCalled();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('shows modal on fifth home view', async () => {
|
||||
act(() => {
|
||||
useSettingStore.setState({ homeScreenViewCount: 5 });
|
||||
});
|
||||
renderHook(() => useRecoveryPrompts());
|
||||
await waitFor(() => {
|
||||
@@ -68,7 +188,10 @@ describe('useRecoveryPrompts', () => {
|
||||
|
||||
it('does not show modal if backup already enabled', async () => {
|
||||
act(() => {
|
||||
useSettingStore.setState({ loginCount: 1, cloudBackupEnabled: true });
|
||||
useSettingStore.setState({
|
||||
homeScreenViewCount: 5,
|
||||
cloudBackupEnabled: true,
|
||||
});
|
||||
});
|
||||
renderHook(() => useRecoveryPrompts());
|
||||
await waitFor(() => {
|
||||
@@ -76,12 +199,8 @@ describe('useRecoveryPrompts', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('does not show modal when navigation is not ready', async () => {
|
||||
const navigationRef = require('@/navigation').navigationRef;
|
||||
navigationRef.isReady.mockReturnValueOnce(false);
|
||||
act(() => {
|
||||
useSettingStore.setState({ loginCount: 1 });
|
||||
});
|
||||
it('does not show modal if already visible', async () => {
|
||||
(useModal as jest.Mock).mockReturnValueOnce({ showModal, visible: true });
|
||||
renderHook(() => useRecoveryPrompts());
|
||||
await waitFor(() => {
|
||||
expect(showModal).not.toHaveBeenCalled();
|
||||
@@ -91,7 +210,7 @@ describe('useRecoveryPrompts', () => {
|
||||
it('does not show modal when recovery phrase has been viewed', async () => {
|
||||
act(() => {
|
||||
useSettingStore.setState({
|
||||
loginCount: 1,
|
||||
homeScreenViewCount: 5,
|
||||
hasViewedRecoveryPhrase: true,
|
||||
});
|
||||
});
|
||||
@@ -104,7 +223,7 @@ describe('useRecoveryPrompts', () => {
|
||||
it('does not show modal when no documents exist', async () => {
|
||||
getAllDocuments.mockResolvedValueOnce({});
|
||||
act(() => {
|
||||
useSettingStore.setState({ loginCount: 1 });
|
||||
useSettingStore.setState({ homeScreenViewCount: 5 });
|
||||
});
|
||||
renderHook(() => useRecoveryPrompts());
|
||||
await waitFor(() => {
|
||||
@@ -112,11 +231,51 @@ describe('useRecoveryPrompts', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('shows modal for other valid login counts', async () => {
|
||||
for (const count of [2, 3, 13, 18]) {
|
||||
it('does not show modal when only unregistered documents exist', async () => {
|
||||
getAllDocuments.mockResolvedValueOnce({
|
||||
doc1: {
|
||||
data: {} as any,
|
||||
metadata: { isRegistered: false } as any,
|
||||
},
|
||||
doc2: {
|
||||
data: {} as any,
|
||||
metadata: { isRegistered: undefined } as any,
|
||||
},
|
||||
});
|
||||
act(() => {
|
||||
useSettingStore.setState({ homeScreenViewCount: 5 });
|
||||
});
|
||||
renderHook(() => useRecoveryPrompts());
|
||||
await waitFor(() => {
|
||||
expect(showModal).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows modal when registered documents exist', async () => {
|
||||
getAllDocuments.mockResolvedValueOnce({
|
||||
doc1: {
|
||||
data: {} as any,
|
||||
metadata: { isRegistered: false } as any,
|
||||
},
|
||||
doc2: {
|
||||
data: {} as any,
|
||||
metadata: { isRegistered: true } as any,
|
||||
},
|
||||
});
|
||||
act(() => {
|
||||
useSettingStore.setState({ homeScreenViewCount: 5 });
|
||||
});
|
||||
renderHook(() => useRecoveryPrompts());
|
||||
await waitFor(() => {
|
||||
expect(showModal).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows modal for other valid view counts (multiples of five)', async () => {
|
||||
for (const count of [5, 10, 15]) {
|
||||
showModal.mockClear();
|
||||
act(() => {
|
||||
useSettingStore.setState({ loginCount: count });
|
||||
useSettingStore.setState({ homeScreenViewCount: count });
|
||||
});
|
||||
renderHook(() => useRecoveryPrompts());
|
||||
await waitFor(() => {
|
||||
@@ -125,6 +284,32 @@ describe('useRecoveryPrompts', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('does not show modal again for same login count when state changes', async () => {
|
||||
act(() => {
|
||||
useSettingStore.setState({ homeScreenViewCount: 5 });
|
||||
});
|
||||
renderHook(() => useRecoveryPrompts());
|
||||
await waitFor(() => {
|
||||
expect(showModal).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
showModal.mockClear();
|
||||
|
||||
act(() => {
|
||||
useSettingStore.setState({ hasViewedRecoveryPhrase: true });
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(showModal).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
act(() => {
|
||||
useSettingStore.setState({ hasViewedRecoveryPhrase: false });
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(showModal).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('returns correct visible state', () => {
|
||||
const { result } = renderHook(() => useRecoveryPrompts());
|
||||
expect(result.current.visible).toBe(false);
|
||||
@@ -134,8 +319,9 @@ describe('useRecoveryPrompts', () => {
|
||||
renderHook(() => useRecoveryPrompts());
|
||||
expect(useModal).toHaveBeenCalledWith({
|
||||
titleText: 'Protect your account',
|
||||
bodyText:
|
||||
bodyText: expect.stringContaining(
|
||||
'Enable cloud backup or save your recovery phrase so you can recover your account.',
|
||||
),
|
||||
buttonText: 'Back up now',
|
||||
onButtonPress: expect.any(Function),
|
||||
onModalDismiss: expect.any(Function),
|
||||
|
||||
107
app/tests/src/navigation.test.tsx
Normal file
107
app/tests/src/navigation.test.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react-native';
|
||||
|
||||
jest.mock('@/hooks/useRecoveryPrompts', () => jest.fn());
|
||||
jest.mock('@selfxyz/mobile-sdk-alpha', () => ({
|
||||
useSelfClient: jest.fn(() => ({})),
|
||||
}));
|
||||
jest.mock('@/navigation/deeplinks', () => ({
|
||||
setupUniversalLinkListenerInNavigation: jest.fn(() => jest.fn()),
|
||||
}));
|
||||
jest.mock('@/services/analytics', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(() => ({
|
||||
trackEvent: jest.fn(),
|
||||
trackScreenView: jest.fn(),
|
||||
flush: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('navigation', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should have the correct navigation screens', () => {
|
||||
// Unmock @/navigation for this test to get the real navigationScreens
|
||||
jest.unmock('@/navigation');
|
||||
jest.isolateModules(() => {
|
||||
const navigationScreens = require('@/navigation').navigationScreens;
|
||||
const listOfScreens = Object.keys(navigationScreens).sort();
|
||||
expect(listOfScreens).toEqual([
|
||||
'AadhaarUpload',
|
||||
'AadhaarUploadError',
|
||||
'AadhaarUploadSuccess',
|
||||
'AccountRecovery',
|
||||
'AccountRecoveryChoice',
|
||||
'AccountVerifiedSuccess',
|
||||
'CloudBackupSettings',
|
||||
'ComingSoon',
|
||||
'ConfirmBelonging',
|
||||
'CountryPicker',
|
||||
'CreateMock',
|
||||
'DeferredLinkingInfo',
|
||||
'DevFeatureFlags',
|
||||
'DevHapticFeedback',
|
||||
'DevLoadingScreen',
|
||||
'DevPrivateKey',
|
||||
'DevSettings',
|
||||
'Disclaimer',
|
||||
'DocumentCamera',
|
||||
'DocumentCameraTrouble',
|
||||
'DocumentDataInfo',
|
||||
'DocumentDataNotFound',
|
||||
'DocumentNFCMethodSelection',
|
||||
'DocumentNFCScan',
|
||||
'DocumentNFCTrouble',
|
||||
'DocumentOnboarding',
|
||||
'Gratification',
|
||||
'Home',
|
||||
'IDPicker',
|
||||
'IdDetails',
|
||||
'Loading',
|
||||
'ManageDocuments',
|
||||
'MockDataDeepLink',
|
||||
'Modal',
|
||||
'Points',
|
||||
'PointsInfo',
|
||||
'ProofHistory',
|
||||
'ProofHistoryDetail',
|
||||
'ProofRequestStatus',
|
||||
'Prove',
|
||||
'QRCodeTrouble',
|
||||
'QRCodeViewFinder',
|
||||
'RecoverWithPhrase',
|
||||
'Referral',
|
||||
'SaveRecoveryPhrase',
|
||||
'Settings',
|
||||
'ShowRecoveryPhrase',
|
||||
'Splash',
|
||||
'WebView',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('wires recovery prompts hook into navigation', () => {
|
||||
// Temporarily restore the React mock and unmock @/navigation for this test
|
||||
jest.unmock('@/navigation');
|
||||
const useRecoveryPrompts =
|
||||
require('@/hooks/useRecoveryPrompts') as jest.Mock;
|
||||
|
||||
// Since we're testing the wiring and not the actual rendering,
|
||||
// we can just check if the module exports the default component
|
||||
// and verify the hook is called when the component is imported
|
||||
const navigation = require('@/navigation');
|
||||
expect(navigation.default).toBeDefined();
|
||||
|
||||
// Render the component to trigger the hooks
|
||||
const NavigationWithTracking = navigation.default;
|
||||
render(<NavigationWithTracking />);
|
||||
|
||||
expect(useRecoveryPrompts).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
@@ -1,157 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import { act } from '@testing-library/react-native';
|
||||
|
||||
import { useSettingStore } from '@/stores/settingStore';
|
||||
|
||||
describe('settingStore', () => {
|
||||
beforeEach(() => {
|
||||
act(() => {
|
||||
useSettingStore.setState({
|
||||
loginCount: 0,
|
||||
cloudBackupEnabled: false,
|
||||
hasViewedRecoveryPhrase: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('increments login count', () => {
|
||||
useSettingStore.getState().incrementLoginCount();
|
||||
expect(useSettingStore.getState().loginCount).toBe(1);
|
||||
});
|
||||
|
||||
it('increments login count multiple times', () => {
|
||||
useSettingStore.getState().incrementLoginCount();
|
||||
useSettingStore.getState().incrementLoginCount();
|
||||
useSettingStore.getState().incrementLoginCount();
|
||||
expect(useSettingStore.getState().loginCount).toBe(3);
|
||||
});
|
||||
|
||||
it('increments login count from non-zero initial value', () => {
|
||||
act(() => {
|
||||
useSettingStore.setState({ loginCount: 5 });
|
||||
});
|
||||
useSettingStore.getState().incrementLoginCount();
|
||||
expect(useSettingStore.getState().loginCount).toBe(6);
|
||||
});
|
||||
|
||||
it('resets login count when recovery phrase viewed', () => {
|
||||
act(() => {
|
||||
useSettingStore.setState({ loginCount: 2 });
|
||||
});
|
||||
useSettingStore.getState().setHasViewedRecoveryPhrase(true);
|
||||
expect(useSettingStore.getState().hasViewedRecoveryPhrase).toBe(true);
|
||||
expect(useSettingStore.getState().loginCount).toBe(0);
|
||||
});
|
||||
|
||||
it('does not reset login count when setting recovery phrase viewed to false', () => {
|
||||
act(() => {
|
||||
useSettingStore.setState({
|
||||
loginCount: 3,
|
||||
hasViewedRecoveryPhrase: true,
|
||||
});
|
||||
});
|
||||
useSettingStore.getState().setHasViewedRecoveryPhrase(false);
|
||||
expect(useSettingStore.getState().hasViewedRecoveryPhrase).toBe(false);
|
||||
expect(useSettingStore.getState().loginCount).toBe(3);
|
||||
});
|
||||
|
||||
it('resets login count when enabling cloud backup', () => {
|
||||
act(() => {
|
||||
useSettingStore.setState({ loginCount: 3, cloudBackupEnabled: false });
|
||||
});
|
||||
useSettingStore.getState().toggleCloudBackupEnabled();
|
||||
expect(useSettingStore.getState().cloudBackupEnabled).toBe(true);
|
||||
expect(useSettingStore.getState().loginCount).toBe(0);
|
||||
});
|
||||
|
||||
it('does not reset login count when disabling cloud backup', () => {
|
||||
act(() => {
|
||||
useSettingStore.setState({ loginCount: 4, cloudBackupEnabled: true });
|
||||
});
|
||||
useSettingStore.getState().toggleCloudBackupEnabled();
|
||||
expect(useSettingStore.getState().cloudBackupEnabled).toBe(false);
|
||||
expect(useSettingStore.getState().loginCount).toBe(4);
|
||||
});
|
||||
|
||||
it('handles sequential actions that reset login count', () => {
|
||||
// Increment login count
|
||||
useSettingStore.getState().incrementLoginCount();
|
||||
useSettingStore.getState().incrementLoginCount();
|
||||
expect(useSettingStore.getState().loginCount).toBe(2);
|
||||
|
||||
// Toggle cloud backup (should reset to 0)
|
||||
useSettingStore.getState().toggleCloudBackupEnabled();
|
||||
expect(useSettingStore.getState().cloudBackupEnabled).toBe(true);
|
||||
expect(useSettingStore.getState().loginCount).toBe(0);
|
||||
|
||||
// Increment again
|
||||
useSettingStore.getState().incrementLoginCount();
|
||||
expect(useSettingStore.getState().loginCount).toBe(1);
|
||||
|
||||
// Set recovery phrase viewed (should reset to 0)
|
||||
useSettingStore.getState().setHasViewedRecoveryPhrase(true);
|
||||
expect(useSettingStore.getState().hasViewedRecoveryPhrase).toBe(true);
|
||||
expect(useSettingStore.getState().loginCount).toBe(0);
|
||||
});
|
||||
|
||||
it('does not reset login count when setting recovery phrase viewed to true when already true', () => {
|
||||
act(() => {
|
||||
useSettingStore.setState({
|
||||
loginCount: 5,
|
||||
hasViewedRecoveryPhrase: true,
|
||||
});
|
||||
});
|
||||
useSettingStore.getState().setHasViewedRecoveryPhrase(true);
|
||||
expect(useSettingStore.getState().hasViewedRecoveryPhrase).toBe(true);
|
||||
expect(useSettingStore.getState().loginCount).toBe(5);
|
||||
});
|
||||
|
||||
it('handles complex sequence of mixed operations', () => {
|
||||
// Start with some increments
|
||||
useSettingStore.getState().incrementLoginCount();
|
||||
useSettingStore.getState().incrementLoginCount();
|
||||
useSettingStore.getState().incrementLoginCount();
|
||||
expect(useSettingStore.getState().loginCount).toBe(3);
|
||||
|
||||
// Disable cloud backup (should not reset)
|
||||
act(() => {
|
||||
useSettingStore.setState({ cloudBackupEnabled: true });
|
||||
});
|
||||
useSettingStore.getState().toggleCloudBackupEnabled();
|
||||
expect(useSettingStore.getState().cloudBackupEnabled).toBe(false);
|
||||
expect(useSettingStore.getState().loginCount).toBe(3);
|
||||
|
||||
// Set recovery phrase viewed to false (should not reset)
|
||||
act(() => {
|
||||
useSettingStore.setState({ hasViewedRecoveryPhrase: true });
|
||||
});
|
||||
useSettingStore.getState().setHasViewedRecoveryPhrase(false);
|
||||
expect(useSettingStore.getState().hasViewedRecoveryPhrase).toBe(false);
|
||||
expect(useSettingStore.getState().loginCount).toBe(3);
|
||||
|
||||
// Enable cloud backup (should reset)
|
||||
useSettingStore.getState().toggleCloudBackupEnabled();
|
||||
expect(useSettingStore.getState().cloudBackupEnabled).toBe(true);
|
||||
expect(useSettingStore.getState().loginCount).toBe(0);
|
||||
});
|
||||
|
||||
it('maintains login count when toggling cloud backup from true to false then back to true', () => {
|
||||
// Start with cloud backup enabled and some login count
|
||||
act(() => {
|
||||
useSettingStore.setState({ loginCount: 2, cloudBackupEnabled: true });
|
||||
});
|
||||
|
||||
// Toggle to disable (should not reset)
|
||||
useSettingStore.getState().toggleCloudBackupEnabled();
|
||||
expect(useSettingStore.getState().cloudBackupEnabled).toBe(false);
|
||||
expect(useSettingStore.getState().loginCount).toBe(2);
|
||||
|
||||
// Toggle to enable (should reset)
|
||||
useSettingStore.getState().toggleCloudBackupEnabled();
|
||||
expect(useSettingStore.getState().cloudBackupEnabled).toBe(true);
|
||||
expect(useSettingStore.getState().loginCount).toBe(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user