diff --git a/components/AccountInformation.test.tsx b/components/AccountInformation.test.tsx
new file mode 100644
index 00000000..2d4ad0db
--- /dev/null
+++ b/components/AccountInformation.test.tsx
@@ -0,0 +1,42 @@
+import React from 'react';
+import {render} from '@testing-library/react-native';
+import {AccountInformation} from './AccountInformation';
+
+describe('AccountInformation Component', () => {
+ const defaultProps = {
+ email: 'test@example.com',
+ picture: 'https://example.com/avatar.jpg',
+ };
+
+ it('should match snapshot with email and picture', () => {
+ const {toJSON} = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with different email', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with different picture URL', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with long email', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+});
diff --git a/components/ActivityLogEvent.test.ts b/components/ActivityLogEvent.test.ts
index 9fd2f71d..bb050fc6 100644
--- a/components/ActivityLogEvent.test.ts
+++ b/components/ActivityLogEvent.test.ts
@@ -19,7 +19,7 @@ describe('ActivityLog', () => {
describe('getActionText', () => {
let activityLog;
let mockIl18nfn;
- let wellknown = {
+ const wellknown = {
credential_configurations_supported: {
mockId: {
display: [
@@ -67,6 +67,7 @@ describe('getActionText', () => {
activityLog.getActionText(mockIl18nfn, wellknown);
expect(mockIl18nfn).toHaveBeenCalledWith('mockType', {
idType: 'fake VC',
+ vcStatus: '',
});
expect(mockIl18nfn).toHaveBeenCalledTimes(1);
// TODO: assert the returned string
@@ -81,3 +82,79 @@ describe('getActionText', () => {
expect(mockIl18nfn).toHaveBeenCalledTimes(1);
});
});
+
+describe('VCActivityLog.getLogFromObject', () => {
+ it('should create VCActivityLog instance from object', () => {
+ const mockData = {
+ id: 'test-id',
+ type: 'VC_ADDED',
+ timestamp: 1234567890,
+ deviceName: 'Test Device',
+ };
+
+ const log = VCActivityLog.getLogFromObject(mockData);
+
+ expect(log).toBeInstanceOf(VCActivityLog);
+ expect(log.id).toBe('test-id');
+ expect(log.type).toBe('VC_ADDED');
+ expect(log.timestamp).toBe(1234567890);
+ expect(log.deviceName).toBe('Test Device');
+ });
+
+ it('should create VCActivityLog from empty object', () => {
+ const log = VCActivityLog.getLogFromObject({});
+
+ expect(log).toBeInstanceOf(VCActivityLog);
+ expect(log.timestamp).toBeDefined();
+ });
+});
+
+describe('VCActivityLog.getActionLabel', () => {
+ it('should return formatted action label with device name and time', () => {
+ const mockLog = new VCActivityLog({
+ deviceName: 'iPhone 12',
+ timestamp: Date.now() - 60000, // 1 minute ago
+ });
+
+ const label = mockLog.getActionLabel('en');
+
+ expect(label).toContain('iPhone 12');
+ expect(label).toContain('·');
+ expect(label).toContain('ago');
+ });
+
+ it('should return only time when device name is empty', () => {
+ const mockLog = new VCActivityLog({
+ deviceName: '',
+ timestamp: Date.now() - 120000, // 2 minutes ago
+ });
+
+ const label = mockLog.getActionLabel('en');
+
+ expect(label).not.toContain('·');
+ expect(label).toContain('ago');
+ });
+
+ it('should filter out empty labels', () => {
+ const mockLog = new VCActivityLog({
+ deviceName: ' ', // whitespace only
+ timestamp: Date.now(),
+ });
+
+ const label = mockLog.getActionLabel('en');
+
+ expect(label).not.toContain('·');
+ expect(label).toBeTruthy();
+ });
+
+ it('should format time with device name in English locale', () => {
+ const mockLog = new VCActivityLog({
+ deviceName: 'Test Device',
+ timestamp: Date.now() - 300000, // 5 minutes ago
+ });
+
+ const labelEn = mockLog.getActionLabel('en');
+ expect(labelEn).toBeTruthy();
+ expect(labelEn).toContain('Test Device');
+ });
+});
diff --git a/components/ActivityLogText.test.tsx b/components/ActivityLogText.test.tsx
new file mode 100644
index 00000000..612a41fd
--- /dev/null
+++ b/components/ActivityLogText.test.tsx
@@ -0,0 +1,65 @@
+import React from 'react';
+import {render} from '@testing-library/react-native';
+import {ActivityLogText} from './ActivityLogText';
+import {VCItemContainerFlowType} from '../shared/Utils';
+import {VCActivityLog} from './ActivityLogEvent';
+import {VPShareActivityLog} from './VPShareActivityLogEvent';
+
+// Mock TextItem
+jest.mock('./ui/TextItem', () => ({
+ TextItem: jest.fn(() => null),
+}));
+
+// Mock HistoryScreenController
+jest.mock('../screens/History/HistoryScreenController', () => ({
+ useHistoryTab: jest.fn(() => ({
+ getWellKnownIssuerMap: jest.fn(() => ({display: [{name: 'Test Issuer'}]})),
+ })),
+}));
+
+// Mock ActivityLogEvent
+jest.mock('./ActivityLogEvent', () => ({
+ VCActivityLog: {
+ getLogFromObject: jest.fn(obj => ({
+ ...obj,
+ getActionLabel: jest.fn(() => 'Shared'),
+ getActionText: jest.fn(() => 'Shared with Test Device'),
+ })),
+ },
+}));
+
+// Mock VPShareActivityLogEvent
+jest.mock('./VPShareActivityLogEvent', () => ({
+ VPShareActivityLog: {
+ getLogFromObject: jest.fn(obj => ({
+ ...obj,
+ getActionLabel: jest.fn(() => 'Verified'),
+ getActionText: jest.fn(() => 'Verified by Test Device'),
+ })),
+ },
+}));
+
+describe('ActivityLogText Component', () => {
+ const mockActivity = {
+ vcLabel: 'Test VC',
+ timestamp: new Date().toISOString(),
+ deviceName: 'Test Device',
+ vcIdType: 'NationalID',
+ flow: VCItemContainerFlowType.VC_SHARE,
+ issuer: 'test-issuer',
+ } as unknown as VCActivityLog;
+
+ it('should match snapshot with VC activity', () => {
+ const {toJSON} = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with VP activity', () => {
+ const vpActivity = {
+ ...mockActivity,
+ flow: VCItemContainerFlowType.VP_SHARE,
+ } as unknown as VPShareActivityLog;
+ const {toJSON} = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+});
diff --git a/components/BackupAndRestoreBannerNotification.test.tsx b/components/BackupAndRestoreBannerNotification.test.tsx
new file mode 100644
index 00000000..559232fa
--- /dev/null
+++ b/components/BackupAndRestoreBannerNotification.test.tsx
@@ -0,0 +1,43 @@
+import React from 'react';
+import {render} from '@testing-library/react-native';
+import {BackupAndRestoreBannerNotification} from './BackupAndRestoreBannerNotification';
+
+// Mock controllers
+jest.mock('../screens/backupAndRestore/BackupController', () => ({
+ useBackupScreen: jest.fn(() => ({
+ showBackupInProgress: false,
+ isBackingUpSuccess: false,
+ isBackingUpFailure: false,
+ backupErrorReason: '',
+ DISMISS: jest.fn(),
+ DISMISS_SHOW_BACKUP_IN_PROGRESS: jest.fn(),
+ })),
+}));
+
+jest.mock('../screens/Settings/BackupRestoreController', () => ({
+ useBackupRestoreScreen: jest.fn(() => ({
+ showRestoreInProgress: false,
+ isBackUpRestoreSuccess: false,
+ isBackUpRestoreFailure: false,
+ restoreErrorReason: '',
+ DISMISS: jest.fn(),
+ DISMISS_SHOW_RESTORE_IN_PROGRESS: jest.fn(),
+ })),
+}));
+
+// Mock BannerNotification
+jest.mock('./BannerNotification', () => ({
+ BannerNotification: jest.fn(() => null),
+ BannerStatusType: {
+ IN_PROGRESS: 'inProgress',
+ SUCCESS: 'success',
+ ERROR: 'error',
+ },
+}));
+
+describe('BackupAndRestoreBannerNotification Component', () => {
+ it('should match snapshot with no banners', () => {
+ const {toJSON} = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+});
diff --git a/components/BannerNotification.test.tsx b/components/BannerNotification.test.tsx
new file mode 100644
index 00000000..4c7c2acf
--- /dev/null
+++ b/components/BannerNotification.test.tsx
@@ -0,0 +1,51 @@
+import React from 'react';
+import {render} from '@testing-library/react-native';
+import {BannerNotification, BannerStatusType} from './BannerNotification';
+
+describe('BannerNotification Component', () => {
+ const defaultProps = {
+ message: 'Test notification message',
+ onClosePress: jest.fn(),
+ testId: 'bannerTest',
+ type: BannerStatusType.SUCCESS,
+ };
+
+ it('should match snapshot with success status', () => {
+ const {toJSON} = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with error status', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with in progress status', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with long message', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with different testId', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+});
diff --git a/components/BannerNotificationContainer.test.tsx b/components/BannerNotificationContainer.test.tsx
new file mode 100644
index 00000000..79724068
--- /dev/null
+++ b/components/BannerNotificationContainer.test.tsx
@@ -0,0 +1,72 @@
+import React from 'react';
+import {render} from '@testing-library/react-native';
+import {BannerNotificationContainer} from './BannerNotificationContainer';
+
+// Mock all controllers
+jest.mock('./BannerNotificationController', () => ({
+ UseBannerNotification: jest.fn(() => ({
+ isBindingSuccess: false,
+ verificationStatus: null,
+ isPasscodeUnlock: false,
+ isBiometricUnlock: false,
+ isDownloadingFailed: false,
+ isDownloadingSuccess: false,
+ isReverificationSuccess: {status: false},
+ isReverificationFailed: {status: false},
+ RESET_WALLET_BINDING_SUCCESS: jest.fn(),
+ RESET_VERIFICATION_STATUS: jest.fn(),
+ RESET_DOWNLOADING_FAILED: jest.fn(),
+ RESET_DOWNLOADING_SUCCESS: jest.fn(),
+ RESET_REVIRIFICATION_SUCCESS: jest.fn(),
+ RESET_REVERIFICATION_FAILURE: jest.fn(),
+ DISMISS: jest.fn(),
+ })),
+}));
+
+jest.mock('../screens/Scan/ScanScreenController', () => ({
+ useScanScreen: jest.fn(() => ({
+ showQuickShareSuccessBanner: false,
+ DISMISS_QUICK_SHARE_BANNER: jest.fn(),
+ })),
+}));
+
+jest.mock('../screens/Settings/SettingScreenController', () => ({
+ useSettingsScreen: jest.fn(() => ({
+ isKeyOrderSet: null,
+ RESET_KEY_ORDER_RESPONSE: jest.fn(),
+ })),
+}));
+
+jest.mock('./BackupAndRestoreBannerNotification', () => ({
+ BackupAndRestoreBannerNotification: jest.fn(() => null),
+}));
+
+jest.mock('./BannerNotification', () => ({
+ BannerNotification: jest.fn(() => null),
+ BannerStatusType: {
+ IN_PROGRESS: 'inProgress',
+ SUCCESS: 'success',
+ ERROR: 'error',
+ },
+}));
+
+describe('BannerNotificationContainer Component', () => {
+ it('should match snapshot with no banners visible', () => {
+ const {toJSON} = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with verification banner enabled', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with verification banner disabled', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+});
diff --git a/components/CopilotTooltip.test.tsx b/components/CopilotTooltip.test.tsx
new file mode 100644
index 00000000..6ccb3a10
--- /dev/null
+++ b/components/CopilotTooltip.test.tsx
@@ -0,0 +1,51 @@
+import React from 'react';
+import {render} from '@testing-library/react-native';
+import {CopilotTooltip} from './CopilotTooltip';
+
+// Mock ui components
+jest.mock('./ui', () => ({
+ Button: jest.fn(() => null),
+ Column: ({children}: {children: React.ReactNode}) => <>{children}>,
+ Row: ({children}: {children: React.ReactNode}) => <>{children}>,
+ Text: ({children}: {children: React.ReactNode}) => <>{children}>,
+}));
+
+// Mock controller
+jest.mock('./CopilotTooltipController', () => ({
+ UseCopilotTooltip: jest.fn(() => ({
+ copilotEvents: {
+ on: jest.fn(),
+ },
+ SET_TOUR_GUIDE: jest.fn(),
+ ONBOARDING_DONE: jest.fn(),
+ INITIAL_DOWNLOAD_DONE: jest.fn(),
+ CURRENT_STEP: 1,
+ currentStepTitle: 'Step 1 Title',
+ currentStepDescription: 'Step 1 Description',
+ titleTestID: 'stepTitle',
+ descriptionTestID: 'stepDescription',
+ stepCount: '1/5',
+ isFirstStep: true,
+ isLastStep: false,
+ isFinalStep: false,
+ isOnboarding: true,
+ isInitialDownloading: false,
+ goToPrev: jest.fn(),
+ goToNext: jest.fn(),
+ stop: jest.fn(),
+ })),
+}));
+
+// Mock settings controller
+jest.mock('../screens/Settings/SettingScreenController', () => ({
+ useSettingsScreen: jest.fn(() => ({
+ BACK: jest.fn(),
+ })),
+}));
+
+describe('CopilotTooltip Component', () => {
+ it('should match snapshot with first step', () => {
+ const {toJSON} = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+});
diff --git a/components/CopyButton.test.tsx b/components/CopyButton.test.tsx
new file mode 100644
index 00000000..a263a723
--- /dev/null
+++ b/components/CopyButton.test.tsx
@@ -0,0 +1,39 @@
+import React from 'react';
+import {render} from '@testing-library/react-native';
+import {CopyButton} from './CopyButton';
+
+// Mock Clipboard
+jest.mock('@react-native-clipboard/clipboard', () => ({
+ setString: jest.fn(),
+}));
+
+// Mock SvgImage
+jest.mock('./ui/svg', () => ({
+ SvgImage: {
+ copyIcon: jest.fn(() => null),
+ },
+}));
+
+describe('CopyButton Component', () => {
+ const defaultProps = {
+ content: 'Test content to copy',
+ };
+
+ it('should match snapshot with default props', () => {
+ const {toJSON} = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with long content', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with special characters', () => {
+ const specialContent = 'Special: @#$%^&*(){}[]';
+ const {toJSON} = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+});
diff --git a/components/DeviceInfoList.test.tsx b/components/DeviceInfoList.test.tsx
new file mode 100644
index 00000000..f27db219
--- /dev/null
+++ b/components/DeviceInfoList.test.tsx
@@ -0,0 +1,85 @@
+import React from 'react';
+import {render} from '@testing-library/react-native';
+import {DeviceInfoList, DeviceInfo} from './DeviceInfoList';
+
+describe('DeviceInfoList Component', () => {
+ const mockDeviceInfo: DeviceInfo = {
+ deviceName: 'Samsung Galaxy S21',
+ name: 'John Doe',
+ deviceId: 'device123',
+ };
+
+ it('should render DeviceInfoList component', () => {
+ const {toJSON} = render();
+ expect(toJSON()).toBeTruthy();
+ });
+
+ it('should render with receiver mode', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toBeTruthy();
+ });
+
+ it('should render with sender mode', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toBeTruthy();
+ });
+
+ it('should render without of prop', () => {
+ const {toJSON} = render();
+ expect(toJSON()).toBeTruthy();
+ });
+
+ it('should handle different device names', () => {
+ const deviceNames = [
+ 'iPhone 14 Pro',
+ 'Google Pixel 7',
+ 'OnePlus 11',
+ 'Samsung Galaxy S23',
+ ];
+
+ deviceNames.forEach(deviceName => {
+ const deviceInfo = {...mockDeviceInfo, deviceName};
+ const {toJSON} = render();
+ expect(toJSON()).toBeTruthy();
+ });
+ });
+
+ it('should handle different user names', () => {
+ const names = ['Alice Smith', 'Bob Johnson', 'Charlie Brown'];
+
+ names.forEach(name => {
+ const deviceInfo = {...mockDeviceInfo, name};
+ const {toJSON} = render();
+ expect(toJSON()).toBeTruthy();
+ });
+ });
+
+ it('should handle different device IDs', () => {
+ const deviceIds = ['device001', 'device002', 'device003'];
+
+ deviceIds.forEach(deviceId => {
+ const deviceInfo = {...mockDeviceInfo, deviceId};
+ const {toJSON} = render();
+ expect(toJSON()).toBeTruthy();
+ });
+ });
+
+ it('should handle empty device name', () => {
+ const deviceInfo = {...mockDeviceInfo, deviceName: ''};
+ const {toJSON} = render();
+ expect(toJSON()).toBeTruthy();
+ });
+
+ it('should handle long device names', () => {
+ const deviceInfo = {
+ ...mockDeviceInfo,
+ deviceName: 'Very Long Device Name With Many Characters',
+ };
+ const {toJSON} = render();
+ expect(toJSON()).toBeTruthy();
+ });
+});
diff --git a/components/DropdownIcon.test.tsx b/components/DropdownIcon.test.tsx
new file mode 100644
index 00000000..c08d6560
--- /dev/null
+++ b/components/DropdownIcon.test.tsx
@@ -0,0 +1,50 @@
+import React from 'react';
+import {render} from '@testing-library/react-native';
+import {DropdownIcon} from './DropdownIcon';
+
+// Mock Popable
+jest.mock('react-native-popable', () => ({
+ Popable: ({children}: {children: React.ReactNode}) => <>{children}>,
+}));
+
+describe('DropdownIcon Component', () => {
+ const mockItems = [
+ {label: 'Item 1', onPress: jest.fn(), icon: 'account'},
+ {label: 'Item 2', onPress: jest.fn(), icon: 'settings'},
+ {label: 'Item 3', onPress: jest.fn(), icon: 'delete', idType: 'type1'},
+ ];
+
+ const defaultProps = {
+ idType: 'type1',
+ icon: 'dots-vertical',
+ items: mockItems,
+ };
+
+ it('should match snapshot with default props', () => {
+ const {toJSON} = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with different icon', () => {
+ const {toJSON} = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with empty items', () => {
+ const {toJSON} = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with multiple items', () => {
+ const manyItems = [
+ {label: 'Action 1', onPress: jest.fn(), icon: 'edit'},
+ {label: 'Action 2', onPress: jest.fn(), icon: 'share'},
+ {label: 'Action 3', onPress: jest.fn(), icon: 'download'},
+ {label: 'Action 4', onPress: jest.fn(), icon: 'upload'},
+ ];
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+});
diff --git a/components/DualMessageOverlay.test.tsx b/components/DualMessageOverlay.test.tsx
new file mode 100644
index 00000000..7d200649
--- /dev/null
+++ b/components/DualMessageOverlay.test.tsx
@@ -0,0 +1,83 @@
+import React from 'react';
+import {render} from '@testing-library/react-native';
+import {DualMessageOverlay} from './DualMessageOverlay';
+import {Text} from 'react-native';
+
+// Mock react-native-elements
+jest.mock('react-native-elements', () => ({
+ Overlay: ({children}: {children: React.ReactNode}) => <>{children}>,
+ Button: jest.fn(({title}) => <>{title}>),
+}));
+
+// Mock ui components
+jest.mock('./ui', () => ({
+ Button: jest.fn(
+ ({title, children}: {title?: string; children?: React.ReactNode}) => (
+ <>{title || children}>
+ ),
+ ),
+ Column: ({children}: {children: React.ReactNode}) => <>{children}>,
+ Row: ({children}: {children: React.ReactNode}) => <>{children}>,
+ Text: ({children}: {children: React.ReactNode}) => <>{children}>,
+}));
+
+describe('DualMessageOverlay Component', () => {
+ const defaultProps = {
+ isVisible: true,
+ title: 'Confirm Action',
+ message: 'Are you sure you want to proceed?',
+ };
+
+ it('should match snapshot with title and message', () => {
+ const {toJSON} = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with both buttons', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with only try again button', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with only ignore button', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with hint text', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with custom height', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with children', () => {
+ const {toJSON} = render(
+
+ Custom content here
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+});
diff --git a/components/EditableListItem.test.tsx b/components/EditableListItem.test.tsx
new file mode 100644
index 00000000..4536bd9e
--- /dev/null
+++ b/components/EditableListItem.test.tsx
@@ -0,0 +1,96 @@
+import React from 'react';
+import {render} from '@testing-library/react-native';
+import {EditableListItem} from './EditableListItem';
+
+// Mock SvgImage
+jest.mock('./ui/svg', () => ({
+ SvgImage: {
+ starIcon: jest.fn(() => null),
+ },
+}));
+
+// Mock react-native-elements
+jest.mock('react-native-elements', () => ({
+ Icon: jest.fn(() => null),
+ ListItem: ({children}: {children: React.ReactNode}) => <>{children}>,
+ Overlay: ({children}: {children: React.ReactNode}) => <>{children}>,
+ Input: jest.fn(() => null),
+}));
+
+// Mock ui components
+jest.mock('./ui', () => ({
+ Button: jest.fn(() => null),
+ Column: ({children}: {children: React.ReactNode}) => <>{children}>,
+ Row: ({children}: {children: React.ReactNode}) => <>{children}>,
+ Text: ({children}: {children: React.ReactNode}) => <>{children}>,
+}));
+
+// Add mock for ListItem.Content and ListItem.Title
+const ListItem = require('react-native-elements').ListItem;
+ListItem.Content = ({children}: {children: React.ReactNode}) => <>{children}>;
+ListItem.Title = ({children}: {children: React.ReactNode}) => <>{children}>;
+
+describe('EditableListItem Component', () => {
+ const mockItems = [
+ {label: 'Email', value: 'test@example.com', testID: 'emailItem'},
+ {label: 'Phone', value: '1234567890', testID: 'phoneItem'},
+ ];
+
+ const defaultProps = {
+ testID: 'editableItem',
+ title: 'Contact Information',
+ content: 'Edit your details',
+ items: mockItems,
+ Icon: 'edit',
+ onEdit: jest.fn(),
+ onCancel: jest.fn(),
+ titleColor: '#000000',
+ };
+
+ it('should match snapshot with default props', () => {
+ const {toJSON} = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with custom title color', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with progress indicator', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with error state', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with success response', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with single item', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+});
diff --git a/components/GlobalContextProvider.test.tsx b/components/GlobalContextProvider.test.tsx
new file mode 100644
index 00000000..eac28774
--- /dev/null
+++ b/components/GlobalContextProvider.test.tsx
@@ -0,0 +1,49 @@
+import React from 'react';
+import {render} from '@testing-library/react-native';
+import {GlobalContextProvider} from './GlobalContextProvider';
+import {Text} from 'react-native';
+
+// Mock xstate
+jest.mock('@xstate/react', () => ({
+ useInterpret: jest.fn(() => ({
+ subscribe: jest.fn(),
+ })),
+}));
+
+// Mock appMachine
+jest.mock('../machines/app', () => ({
+ appMachine: {},
+}));
+
+// Mock GlobalContext
+jest.mock('../shared/GlobalContext', () => ({
+ GlobalContext: {
+ Provider: ({children}: {children: React.ReactNode}) => <>{children}>,
+ },
+}));
+
+// Mock commonUtil
+jest.mock('../shared/commonUtil', () => ({
+ logState: jest.fn(),
+}));
+
+describe('GlobalContextProvider Component', () => {
+ it('should match snapshot with children', () => {
+ const {toJSON} = render(
+
+ Test Child
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with multiple children', () => {
+ const {toJSON} = render(
+
+ Child 1
+ Child 2
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+});
diff --git a/components/HelpScreen.test.tsx b/components/HelpScreen.test.tsx
new file mode 100644
index 00000000..102db38a
--- /dev/null
+++ b/components/HelpScreen.test.tsx
@@ -0,0 +1,60 @@
+import React from 'react';
+import {render} from '@testing-library/react-native';
+import {HelpScreen} from './HelpScreen';
+import {Text} from 'react-native';
+
+// Mock Modal
+jest.mock('./ui/Modal', () => ({
+ Modal: ({children}: {children: React.ReactNode}) => <>{children}>,
+}));
+
+// Mock BannerNotificationContainer
+jest.mock('./BannerNotificationContainer', () => ({
+ BannerNotificationContainer: jest.fn(() => null),
+}));
+
+// Mock API
+jest.mock('../shared/api', () => ({
+ __esModule: true,
+ default: jest.fn(() =>
+ Promise.resolve({
+ aboutInjiUrl: 'https://docs.inji.io',
+ }),
+ ),
+}));
+
+describe('HelpScreen Component', () => {
+ const triggerComponent = Help;
+
+ it('should match snapshot with Inji source', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with BackUp source', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with keyManagement source', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot when disabled', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+});
diff --git a/components/KebabPopUp.test.tsx b/components/KebabPopUp.test.tsx
new file mode 100644
index 00000000..5153df4d
--- /dev/null
+++ b/components/KebabPopUp.test.tsx
@@ -0,0 +1,88 @@
+import React from 'react';
+import {render} from '@testing-library/react-native';
+import {KebabPopUp} from './KebabPopUp';
+import {Text} from 'react-native';
+
+// Mock controller
+jest.mock('./KebabPopUpController', () => ({
+ useKebabPopUp: jest.fn(() => ({
+ isScanning: false,
+ })),
+}));
+
+// Mock kebabMenuUtils
+jest.mock('./kebabMenuUtils', () => ({
+ getKebabMenuOptions: jest.fn(() => [
+ {
+ testID: 'pinCard',
+ label: 'Pin Card',
+ onPress: jest.fn(),
+ icon: null,
+ },
+ {
+ testID: 'removeFromWallet',
+ label: 'Remove',
+ onPress: jest.fn(),
+ icon: null,
+ },
+ ]),
+}));
+
+// Mock react-native-elements
+jest.mock('react-native-elements', () => ({
+ Icon: jest.fn(() => null),
+ ListItem: ({children}: {children: React.ReactNode}) => <>{children}>,
+ Overlay: ({children}: {children: React.ReactNode}) => <>{children}>,
+}));
+
+// Mock FlatList
+jest.mock('react-native-gesture-handler', () => ({
+ FlatList: ({renderItem, data}: any) => (
+ <>{data.map((item: any, index: number) => renderItem({item, index}))}>
+ ),
+}));
+
+describe('KebabPopUp Component', () => {
+ const mockService = {} as any;
+ const mockVcMetadata = {
+ id: 'test-vc',
+ vcLabel: 'Test VC',
+ };
+
+ const defaultProps = {
+ iconName: 'ellipsis-vertical',
+ vcMetadata: mockVcMetadata as any,
+ isVisible: true,
+ onDismiss: jest.fn(),
+ service: mockService,
+ vcHasImage: false,
+ };
+
+ it('should match snapshot with default icon', () => {
+ const {toJSON} = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with custom icon component', () => {
+ const CustomIcon = Custom;
+ const {toJSON} = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with custom icon color', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot when not visible', () => {
+ const {toJSON} = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with VC that has image', () => {
+ const {toJSON} = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+});
diff --git a/components/LanguageSelector.test.tsx b/components/LanguageSelector.test.tsx
new file mode 100644
index 00000000..ed165e40
--- /dev/null
+++ b/components/LanguageSelector.test.tsx
@@ -0,0 +1,32 @@
+import React from 'react';
+import {render} from '@testing-library/react-native';
+import {LanguageSelector} from './LanguageSelector';
+import {Text} from 'react-native';
+
+// Mock dependencies
+jest.mock('react-native-restart', () => ({
+ Restart: jest.fn(),
+}));
+
+jest.mock('./ui/Picker', () => ({
+ Picker: jest.fn(() => null),
+}));
+
+describe('LanguageSelector Component', () => {
+ const defaultTrigger = Select Language;
+
+ it('should match snapshot with default trigger', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with custom trigger component', () => {
+ const customTrigger = Choose Language;
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+});
diff --git a/components/Logo.test.tsx b/components/Logo.test.tsx
new file mode 100644
index 00000000..d26728c5
--- /dev/null
+++ b/components/Logo.test.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import {render} from '@testing-library/react-native';
+import {Logo} from './Logo';
+
+describe('Logo Component', () => {
+ it('should render Logo component', () => {
+ const {toJSON} = render();
+ expect(toJSON()).toBeTruthy();
+ });
+
+ it('should render Logo with width and height props', () => {
+ const {toJSON} = render();
+ expect(toJSON()).toBeTruthy();
+ });
+
+ it('should render Logo with string width and height', () => {
+ const {toJSON} = render();
+ expect(toJSON()).toBeTruthy();
+ });
+});
diff --git a/components/Message.test.tsx b/components/Message.test.tsx
new file mode 100644
index 00000000..a856bc53
--- /dev/null
+++ b/components/Message.test.tsx
@@ -0,0 +1,43 @@
+import React from 'react';
+import {render} from '@testing-library/react-native';
+import {Message} from './Message';
+
+// Mock LinearProgress
+jest.mock('react-native-elements', () => ({
+ LinearProgress: jest.fn(() => null),
+}));
+
+// Mock Button from ui
+jest.mock('./ui', () => ({
+ Button: jest.fn(() => null),
+ Centered: ({children}: {children: React.ReactNode}) => <>{children}>,
+ Column: ({children}: {children: React.ReactNode}) => <>{children}>,
+ Text: ({children}: {children: React.ReactNode}) => <>{children}>,
+}));
+
+describe('Message Component', () => {
+ it('should match snapshot with title only', () => {
+ const {toJSON} = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with message only', () => {
+ const {toJSON} = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with title and message', () => {
+ const {toJSON} = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with hint text', () => {
+ const {toJSON} = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with cancel button', () => {
+ const {toJSON} = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+});
diff --git a/components/MessageOverlay.test.tsx b/components/MessageOverlay.test.tsx
new file mode 100644
index 00000000..09e24aaf
--- /dev/null
+++ b/components/MessageOverlay.test.tsx
@@ -0,0 +1,99 @@
+import React from 'react';
+import {render} from '@testing-library/react-native';
+import {MessageOverlay, ErrorMessageOverlay} from './MessageOverlay';
+import {Text} from 'react-native';
+
+// Mock react-native-elements
+jest.mock('react-native-elements', () => ({
+ Overlay: ({children}: {children: React.ReactNode}) => <>{children}>,
+ LinearProgress: jest.fn(() => null),
+}));
+
+// Mock ui components
+jest.mock('./ui', () => ({
+ Button: jest.fn(() => null),
+ Column: ({children}: {children: React.ReactNode}) => <>{children}>,
+ Text: ({children}: {children: React.ReactNode}) => <>{children}>,
+}));
+
+describe('MessageOverlay Component', () => {
+ const defaultProps = {
+ isVisible: true,
+ title: 'Test Title',
+ message: 'Test Message',
+ };
+
+ it('should match snapshot with title and message', () => {
+ const {toJSON} = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with progress indicator', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with numeric progress', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with button', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with custom children', () => {
+ const {toJSON} = render(
+
+ Custom Content
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with hint text', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with custom minHeight', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+});
+
+describe('ErrorMessageOverlay Component', () => {
+ const errorProps = {
+ isVisible: true,
+ error: 'network',
+ translationPath: 'errors',
+ onDismiss: jest.fn(),
+ };
+
+ it('should match snapshot with error', () => {
+ const {toJSON} = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with testID', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+});
diff --git a/components/Passcode.test.tsx b/components/Passcode.test.tsx
new file mode 100644
index 00000000..b73eb9db
--- /dev/null
+++ b/components/Passcode.test.tsx
@@ -0,0 +1,58 @@
+import React from 'react';
+import {render} from '@testing-library/react-native';
+import {Passcode} from './Passcode';
+
+// Mock PasscodeVerify
+jest.mock('./PasscodeVerify', () => ({
+ PasscodeVerify: jest.fn(() => null),
+}));
+
+// Mock telemetry
+jest.mock('../shared/telemetry/TelemetryUtils', () => ({
+ getImpressionEventData: jest.fn(),
+ sendImpressionEvent: jest.fn(),
+}));
+
+describe('Passcode Component', () => {
+ const defaultProps = {
+ error: '',
+ storedPasscode: 'hashed-passcode',
+ salt: 'salt-value',
+ onSuccess: jest.fn(),
+ onError: jest.fn(),
+ onDismiss: jest.fn(),
+ };
+
+ it('should match snapshot with default props', () => {
+ const {toJSON} = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with custom message', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with error message', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with both message and error', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+});
diff --git a/components/PasscodeVerify.test.tsx b/components/PasscodeVerify.test.tsx
new file mode 100644
index 00000000..51b56c3b
--- /dev/null
+++ b/components/PasscodeVerify.test.tsx
@@ -0,0 +1,54 @@
+import React from 'react';
+import {render} from '@testing-library/react-native';
+import {PasscodeVerify} from './PasscodeVerify';
+
+// Mock PinInput
+jest.mock('./PinInput', () => ({
+ PinInput: jest.fn(() => null),
+}));
+
+// Mock commonUtil
+jest.mock('../shared/commonUtil', () => ({
+ hashData: jest.fn(() => Promise.resolve('hashed-value')),
+}));
+
+// Mock telemetry
+jest.mock('../shared/telemetry/TelemetryUtils', () => ({
+ getErrorEventData: jest.fn(),
+ sendErrorEvent: jest.fn(),
+}));
+
+describe('PasscodeVerify Component', () => {
+ const defaultProps = {
+ passcode: 'stored-hashed-passcode',
+ onSuccess: jest.fn(),
+ onError: jest.fn(),
+ salt: 'test-salt',
+ testID: 'passcodeVerify',
+ };
+
+ it('should match snapshot with default props', () => {
+ const {toJSON} = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with different testID', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot without onError handler', () => {
+ const {passcode, onSuccess, salt, testID} = defaultProps;
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+});
diff --git a/components/PendingIcon.test.tsx b/components/PendingIcon.test.tsx
new file mode 100644
index 00000000..5eccf0fc
--- /dev/null
+++ b/components/PendingIcon.test.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import {render} from '@testing-library/react-native';
+import PendingIcon from './PendingIcon';
+
+describe('PendingIcon', () => {
+ it('should render PendingIcon component', () => {
+ const {toJSON} = render();
+ expect(toJSON()).toBeTruthy();
+ });
+
+ it('should match snapshot', () => {
+ const {toJSON} = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should render with custom color', () => {
+ const {toJSON} = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+});
diff --git a/components/PinInput.test.tsx b/components/PinInput.test.tsx
new file mode 100644
index 00000000..63c38d40
--- /dev/null
+++ b/components/PinInput.test.tsx
@@ -0,0 +1,54 @@
+import React from 'react';
+import {render} from '@testing-library/react-native';
+import {PinInput} from './PinInput';
+
+// Mock usePinInput
+jest.mock('../machines/pinInput', () => ({
+ usePinInput: jest.fn(length => ({
+ state: {
+ context: {
+ inputRefs: Array(length).fill({current: null}),
+ values: Array(length).fill(''),
+ },
+ },
+ send: jest.fn(),
+ events: {
+ UPDATE_INPUT: jest.fn((value, index) => ({
+ type: 'UPDATE_INPUT',
+ value,
+ index,
+ })),
+ FOCUS_INPUT: jest.fn(index => ({type: 'FOCUS_INPUT', index})),
+ KEY_PRESS: jest.fn(key => ({type: 'KEY_PRESS', key})),
+ },
+ })),
+}));
+
+describe('PinInput Component', () => {
+ it('should match snapshot with 4 digit PIN', () => {
+ const {toJSON} = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with 6 digit PIN', () => {
+ const {toJSON} = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with onChange handler', () => {
+ const {toJSON} = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with onDone and autosubmit', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with custom testID', () => {
+ const {toJSON} = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+});
diff --git a/components/ProfileIcon.test.tsx b/components/ProfileIcon.test.tsx
new file mode 100644
index 00000000..54f93509
--- /dev/null
+++ b/components/ProfileIcon.test.tsx
@@ -0,0 +1,50 @@
+import React from 'react';
+import {render} from '@testing-library/react-native';
+import {ProfileIcon} from './ProfileIcon';
+import {View} from 'react-native';
+
+// Mock SvgImage
+jest.mock('./ui/svg', () => ({
+ SvgImage: {
+ pinIcon: jest.fn(() => ),
+ },
+}));
+
+describe('ProfileIcon Component', () => {
+ const defaultProps = {
+ profileIconContainerStyles: {},
+ profileIconSize: 40,
+ };
+
+ it('should match snapshot without pinned icon', () => {
+ const {toJSON} = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with pinned icon', () => {
+ const {toJSON} = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with custom icon size', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with custom container styles', () => {
+ const customStyles = {
+ backgroundColor: 'blue',
+ borderRadius: 10,
+ padding: 5,
+ };
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+});
diff --git a/components/ProgressingModal.test.tsx b/components/ProgressingModal.test.tsx
new file mode 100644
index 00000000..cb9c2c58
--- /dev/null
+++ b/components/ProgressingModal.test.tsx
@@ -0,0 +1,100 @@
+import React from 'react';
+import {render} from '@testing-library/react-native';
+import {ProgressingModal} from './ProgressingModal';
+
+// Mock Modal
+jest.mock('./ui/Modal', () => ({
+ Modal: ({children}: {children: React.ReactNode}) => <>{children}>,
+}));
+
+// Mock Spinner
+jest.mock('react-native-spinkit', () => 'Spinner');
+
+// Mock SvgImage
+jest.mock('./ui/svg', () => ({
+ SvgImage: {
+ ProgressIcon: jest.fn(() => null),
+ },
+}));
+
+// Mock ui components
+jest.mock('./ui', () => ({
+ Button: jest.fn(() => null),
+ Centered: ({children}: {children: React.ReactNode}) => <>{children}>,
+ Column: ({children}: {children: React.ReactNode}) => <>{children}>,
+ Text: ({children}: {children: React.ReactNode}) => <>{children}>,
+}));
+
+describe('ProgressingModal Component', () => {
+ const defaultProps = {
+ isVisible: true,
+ isHintVisible: false,
+ title: 'Processing',
+ };
+
+ it('should match snapshot with default props', () => {
+ const {toJSON} = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with progress spinner', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with hint visible', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with retry button', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with stay in progress button', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with BLE error visible', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot as requester', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+});
diff --git a/components/QrCodeOverlay.test.tsx b/components/QrCodeOverlay.test.tsx
new file mode 100644
index 00000000..e05fddb8
--- /dev/null
+++ b/components/QrCodeOverlay.test.tsx
@@ -0,0 +1,94 @@
+import React from 'react';
+import {render} from '@testing-library/react-native';
+import {QrCodeOverlay} from './QrCodeOverlay';
+import {NativeModules} from 'react-native';
+
+// Mock QRCode
+jest.mock('react-native-qrcode-svg', () => 'QRCode');
+
+// Mock SvgImage
+jest.mock('./ui/svg', () => ({
+ SvgImage: {
+ MagnifierZoom: jest.fn(() => null),
+ },
+}));
+
+// Mock sharing utils
+jest.mock('../shared/sharing/imageUtils', () => ({
+ shareImageToAllSupportedApps: jest.fn(() => Promise.resolve(true)),
+}));
+
+describe('QrCodeOverlay Component', () => {
+ // Setup mocks for native modules
+ beforeAll(() => {
+ // Mock RNSecureKeystoreModule methods
+ NativeModules.RNSecureKeystoreModule.getData = jest.fn(() =>
+ Promise.resolve(['key', 'mocked-qr-data']),
+ );
+ NativeModules.RNSecureKeystoreModule.storeData = jest.fn(() =>
+ Promise.resolve(),
+ );
+
+ // Mock RNPixelpassModule
+ NativeModules.RNPixelpassModule = {
+ generateQRData: jest.fn(() => Promise.resolve('mocked-qr-data')),
+ };
+ });
+
+ // Silence console warnings during tests
+ beforeAll(() => {
+ jest.spyOn(console, 'warn').mockImplementation();
+ jest.spyOn(console, 'error').mockImplementation();
+ });
+
+ afterAll(() => {
+ jest.restoreAllMocks();
+ });
+
+ const mockVC = {
+ credential: {id: 'test-credential'},
+ generatedOn: new Date().toISOString(),
+ };
+
+ const mockMeta = {
+ id: 'test-vc-id',
+ vcLabel: 'Test VC',
+ };
+
+ const defaultProps = {
+ verifiableCredential: mockVC as any,
+ meta: mockMeta as any,
+ };
+
+ // NOTE: CodeRabbit suggested making these tests async to wait for QR data loading.
+ // However, the component requires native module mocks (RNSecureKeystoreModule.getData)
+ // that are not properly initialized in the test environment, causing the component
+ // to always return null. These tests currently capture empty snapshots.
+ // TODO: Fix native module mocking to properly test the async QR data loading behavior.
+
+ it('should match snapshot with default props', () => {
+ const {toJSON} = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with inline QR disabled', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with force visible', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with onClose handler', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+});
diff --git a/components/QrScanner.test.tsx b/components/QrScanner.test.tsx
new file mode 100644
index 00000000..8b62d5d4
--- /dev/null
+++ b/components/QrScanner.test.tsx
@@ -0,0 +1,106 @@
+import React from 'react';
+import {render} from '@testing-library/react-native';
+import {QrScanner} from './QrScanner';
+
+// Mock useContext
+const mockUseContext = jest.fn();
+jest.spyOn(React, 'useContext').mockImplementation(mockUseContext);
+
+// Mock GlobalContext
+jest.mock('../shared/GlobalContext', () => ({
+ GlobalContext: {},
+}));
+
+// Mock xstate with a mutable mock function
+const mockUseSelector = jest.fn();
+jest.mock('@xstate/react', () => ({
+ useSelector: jest.fn((...args) => mockUseSelector(...args)),
+}));
+
+// Before each test, set up the context mock
+beforeEach(() => {
+ mockUseContext.mockReturnValue({
+ appService: {send: jest.fn()},
+ });
+ mockUseSelector.mockReturnValue(true);
+});
+
+// Mock app machine
+jest.mock('../machines/app', () => ({
+ selectIsActive: jest.fn(),
+}));
+
+// Mock SvgImage
+jest.mock('./ui/svg', () => ({
+ SvgImage: {
+ FlipCameraIcon: jest.fn(() => null),
+ },
+}));
+
+// Mock ui components
+jest.mock('./ui', () => ({
+ Column: ({children}: {children: React.ReactNode}) => <>{children}>,
+ Row: ({children}: {children: React.ReactNode}) => <>{children}>,
+ Text: ({children}: {children: React.ReactNode}) => <>{children}>,
+}));
+
+// Mock expo-camera
+jest.mock('expo-camera', () => ({
+ CameraView: jest.fn(() => null),
+ useCameraPermissions: jest.fn(() => [
+ null,
+ jest.fn(() => Promise.resolve({granted: true})),
+ jest.fn(() => Promise.resolve({status: 'granted'})),
+ ]),
+ PermissionStatus: {
+ UNDETERMINED: 'undetermined',
+ GRANTED: 'granted',
+ DENIED: 'denied',
+ },
+ CameraType: {
+ BACK: 'back',
+ FRONT: 'front',
+ },
+}));
+
+describe('QrScanner Component', () => {
+ const defaultProps = {
+ onQrFound: jest.fn(),
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should match snapshot with default props', () => {
+ const {toJSON} = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with title', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with custom title', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should render with onQrFound callback', () => {
+ const onQrFound = jest.fn();
+ const {toJSON} = render();
+ expect(toJSON()).toBeTruthy();
+ expect(onQrFound).not.toHaveBeenCalled(); // Callback not called until QR is scanned
+ });
+
+ it('should render when isActive is false', () => {
+ mockUseSelector.mockReturnValue(false);
+ const {toJSON} = render();
+ expect(toJSON()).toBeTruthy();
+ });
+});
diff --git a/components/RotatingIcon.test.tsx b/components/RotatingIcon.test.tsx
new file mode 100644
index 00000000..0539c671
--- /dev/null
+++ b/components/RotatingIcon.test.tsx
@@ -0,0 +1,99 @@
+import React from 'react';
+import {render} from '@testing-library/react-native';
+import {RotatingIcon} from './RotatingIcon';
+
+describe('RotatingIcon Component', () => {
+ it('should render RotatingIcon component', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toBeTruthy();
+ });
+
+ it('should render with clockwise rotation', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toBeTruthy();
+ });
+
+ it('should render with counter-clockwise rotation', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toBeTruthy();
+ });
+
+ it('should render with custom duration', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toBeTruthy();
+ });
+
+ it('should render with default duration', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toBeTruthy();
+ });
+
+ it('should handle different icon names', () => {
+ const iconNames = ['sync', 'refresh', 'loading', 'autorenew'];
+
+ iconNames.forEach(name => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toBeTruthy();
+ });
+ });
+
+ it('should handle different sizes', () => {
+ const sizes = [16, 24, 32, 48];
+
+ sizes.forEach(size => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toBeTruthy();
+ });
+ });
+
+ it('should handle different colors', () => {
+ const colors = ['#FF0000', '#00FF00', '#0000FF', '#FFFFFF'];
+
+ colors.forEach(color => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toBeTruthy();
+ });
+ });
+});
diff --git a/components/SectionLayout.test.tsx b/components/SectionLayout.test.tsx
new file mode 100644
index 00000000..93c7882b
--- /dev/null
+++ b/components/SectionLayout.test.tsx
@@ -0,0 +1,70 @@
+import React from 'react';
+import {render} from '@testing-library/react-native';
+import {SectionLayout} from './SectionLayout';
+import {Text, View} from 'react-native';
+
+// Mock ui components
+jest.mock('./ui', () => ({
+ Column: ({children}: {children: React.ReactNode}) => <>{children}>,
+ Row: ({children}: {children: React.ReactNode}) => <>{children}>,
+ Text: ({children}: {children: React.ReactNode}) => <>{children}>,
+}));
+
+describe('SectionLayout Component', () => {
+ const defaultProps = {
+ headerIcon: Icon,
+ headerText: 'Section Header',
+ children: Section Content,
+ testId: 'testSection',
+ };
+
+ it('should match snapshot with default props', () => {
+ const {toJSON} = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with custom marginBottom', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with different header text', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with complex children', () => {
+ const {toJSON} = render(
+
+ Line 1
+ Line 2
+
+ Nested content
+
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with different icon', () => {
+ const customIcon = 🔍;
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with zero marginBottom', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+});
diff --git a/components/TextEditOverlay.test.tsx b/components/TextEditOverlay.test.tsx
new file mode 100644
index 00000000..0c16b74e
--- /dev/null
+++ b/components/TextEditOverlay.test.tsx
@@ -0,0 +1,150 @@
+import React from 'react';
+import {render, fireEvent} from '@testing-library/react-native';
+import {TextEditOverlay} from './TextEditOverlay';
+
+// Mock react-native-elements with a more realistic Input
+jest.mock('react-native-elements', () => {
+ const RN = jest.requireActual('react-native');
+ return {
+ Input: (props: {value: string; onChangeText: (text: string) => void}) => (
+
+ ),
+ };
+});
+
+// Mock ui components with more realistic Button
+jest.mock('./ui', () => {
+ const RN = jest.requireActual('react-native');
+ const React = jest.requireActual('react');
+ return {
+ Button: (props: {title: string; onPress: () => void}) => (
+
+ {props.title}
+
+ ),
+ Centered: ({children}: {children: React.ReactNode}) => <>{children}>,
+ Column: ({children}: {children: React.ReactNode}) => <>{children}>,
+ Row: ({children}: {children: React.ReactNode}) => <>{children}>,
+ Text: ({children}: {children: React.ReactNode}) => <>{children}>,
+ };
+});
+
+describe('TextEditOverlay Component', () => {
+ const defaultProps = {
+ isVisible: true,
+ label: 'Edit Name',
+ value: 'John Doe',
+ onSave: jest.fn(),
+ onDismiss: jest.fn(),
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should match snapshot with default props', () => {
+ const {toJSON} = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with different label', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with maxLength', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with empty value', () => {
+ const {toJSON} = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with long value', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should call onSave with value when save button is pressed', () => {
+ const onSave = jest.fn();
+ const {getByTestId} = render(
+ ,
+ );
+
+ const saveButton = getByTestId('button-save');
+ fireEvent.press(saveButton);
+
+ // onSave should be called with the current value
+ expect(onSave).toHaveBeenCalledTimes(1);
+ expect(typeof onSave.mock.calls[0][0]).toBe('string');
+ });
+
+ it('should call onSave with original value when save is pressed without changes', () => {
+ const onSave = jest.fn();
+ const {getByTestId} = render(
+ ,
+ );
+
+ const saveButton = getByTestId('button-save');
+ fireEvent.press(saveButton);
+
+ expect(onSave).toHaveBeenCalledWith('John Doe');
+ });
+
+ it('should call onDismiss and reset value when cancel button is pressed', () => {
+ const onDismiss = jest.fn();
+ const {getByTestId} = render(
+ ,
+ );
+
+ const input = getByTestId('text-input');
+ // Simulate text change
+ input.props.onChangeText('Modified Text');
+
+ const cancelButton = getByTestId('button-cancel');
+ fireEvent.press(cancelButton);
+
+ expect(onDismiss).toHaveBeenCalled();
+ });
+
+ it('should handle text input changes', () => {
+ const {getByTestId} = render();
+
+ const input = getByTestId('text-input');
+
+ // Verify input has onChangeText handler
+ expect(input.props.onChangeText).toBeDefined();
+ expect(typeof input.props.onChangeText).toBe('function');
+ });
+
+ it('should not use isVisible prop', () => {
+ // Note: isVisible is defined in the interface but not used in the component
+ // This test documents that the prop exists but has no effect
+ const {toJSON: jsonVisible} = render(
+ ,
+ );
+ const {toJSON: jsonHidden} = render(
+ ,
+ );
+
+ // Both render the same output regardless of isVisible value
+ expect(JSON.stringify(jsonVisible())).toEqual(JSON.stringify(jsonHidden()));
+ });
+});
diff --git a/components/TrustModal.test.tsx b/components/TrustModal.test.tsx
new file mode 100644
index 00000000..1e44fda9
--- /dev/null
+++ b/components/TrustModal.test.tsx
@@ -0,0 +1,105 @@
+import React from 'react';
+import {render} from '@testing-library/react-native';
+import {TrustModal} from './TrustModal';
+
+// Mock useTranslation hook
+const mockT = jest.fn((key: string) => {
+ if (key === 'infoPoints' || key === 'verifierInfoPoints') {
+ return ['Point 1', 'Point 2', 'Point 3'];
+ }
+ return key;
+});
+
+jest.mock('react-i18next', () => ({
+ ...jest.requireActual('react-i18next'),
+ useTranslation: () => ({
+ t: mockT,
+ i18n: {changeLanguage: jest.fn()},
+ }),
+}));
+
+// Mock ui components
+jest.mock('./ui', () => ({
+ Button: jest.fn(() => null),
+ Column: ({children}: {children: React.ReactNode}) => <>{children}>,
+ Row: ({children}: {children: React.ReactNode}) => <>{children}>,
+ Text: ({children}: {children: React.ReactNode}) => <>{children}>,
+}));
+
+// Mock react-native components
+jest.mock('react-native', () => {
+ const ReactNative = jest.requireActual('react-native');
+ return {
+ ...ReactNative,
+ Modal: ({children}: {children: React.ReactNode}) => <>{children}>,
+ View: ({children}: {children: React.ReactNode}) => <>{children}>,
+ ScrollView: ({children}: {children: React.ReactNode}) => <>{children}>,
+ Image: jest.fn(() => null),
+ };
+});
+
+describe('TrustModal Component', () => {
+ const defaultProps = {
+ isVisible: true,
+ logo: 'https://example.com/logo.png',
+ name: 'Test Issuer',
+ onConfirm: jest.fn(),
+ onCancel: jest.fn(),
+ };
+
+ it('should match snapshot with issuer flow', () => {
+ const {toJSON} = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with verifier flow', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot when not visible', () => {
+ const {toJSON} = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot without logo', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot without name', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot without logo and name', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with long name', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+});
diff --git a/components/VCVerification.test.tsx b/components/VCVerification.test.tsx
new file mode 100644
index 00000000..2650a64f
--- /dev/null
+++ b/components/VCVerification.test.tsx
@@ -0,0 +1,75 @@
+import React from 'react';
+import {render} from '@testing-library/react-native';
+import {VCVerification} from './VCVerification';
+import {VCMetadata} from '../shared/VCMetadata';
+import {Display} from './VC/common/VCUtils';
+
+// Mock the Display class
+const mockDisplay = {
+ getTextColor: jest.fn((defaultColor: string) => defaultColor),
+} as unknown as Display;
+
+describe('VCVerification Component', () => {
+ it('should render for verified and valid credential', () => {
+ const vcMetadata = new VCMetadata({
+ isVerified: true,
+ isExpired: false,
+ });
+
+ const {toJSON} = render(
+ ,
+ );
+
+ expect(toJSON()).toBeTruthy();
+ });
+
+ it('should render for verified but expired credential', () => {
+ const vcMetadata = new VCMetadata({
+ isVerified: true,
+ isExpired: true,
+ });
+
+ const {toJSON} = render(
+ ,
+ );
+
+ expect(toJSON()).toBeTruthy();
+ });
+
+ it('should render for pending/unverified credential', () => {
+ const vcMetadata = new VCMetadata({
+ isVerified: false,
+ isExpired: false,
+ });
+
+ const {toJSON} = render(
+ ,
+ );
+
+ expect(toJSON()).toBeTruthy();
+ });
+
+ it('should render verification status text', () => {
+ const vcMetadata = new VCMetadata({
+ isVerified: true,
+ isExpired: false,
+ });
+
+ const {getByText} = render(
+ ,
+ );
+
+ expect(getByText('valid')).toBeTruthy();
+ });
+
+ it('should call getTextColor from display prop', () => {
+ const vcMetadata = new VCMetadata({
+ isVerified: true,
+ isExpired: false,
+ });
+
+ render();
+
+ expect(mockDisplay.getTextColor).toHaveBeenCalled();
+ });
+});
diff --git a/components/VPShareActivityLogEvent.test.ts b/components/VPShareActivityLogEvent.test.ts
new file mode 100644
index 00000000..f22edb71
--- /dev/null
+++ b/components/VPShareActivityLogEvent.test.ts
@@ -0,0 +1,234 @@
+import {VPShareActivityLog} from './VPShareActivityLogEvent';
+import {VCItemContainerFlowType} from '../shared/Utils';
+
+describe('VPShareActivityLog', () => {
+ describe('constructor', () => {
+ it('should create instance with default values', () => {
+ const log = new VPShareActivityLog({});
+
+ expect(log.type).toBe('');
+ expect(log.timestamp).toBeDefined();
+ expect(log.flow).toBe(VCItemContainerFlowType.VP_SHARE);
+ expect(log.info).toBe('');
+ });
+
+ it('should create instance with provided values', () => {
+ const timestamp = Date.now();
+ const log = new VPShareActivityLog({
+ type: 'SHARED_SUCCESSFULLY',
+ timestamp,
+ flow: VCItemContainerFlowType.QR_LOGIN,
+ info: 'Test info',
+ });
+
+ expect(log.type).toBe('SHARED_SUCCESSFULLY');
+ expect(log.timestamp).toBe(timestamp);
+ expect(log.flow).toBe(VCItemContainerFlowType.QR_LOGIN);
+ expect(log.info).toBe('Test info');
+ });
+
+ it('should handle different activity types', () => {
+ const types: Array<
+ | 'SHARED_SUCCESSFULLY'
+ | 'SHARED_WITH_FACE_VERIFIACTION'
+ | 'VERIFIER_AUTHENTICATION_FAILED'
+ | 'INVALID_AUTH_REQUEST'
+ | 'USER_DECLINED_CONSENT'
+ > = [
+ 'SHARED_SUCCESSFULLY',
+ 'SHARED_WITH_FACE_VERIFIACTION',
+ 'VERIFIER_AUTHENTICATION_FAILED',
+ 'INVALID_AUTH_REQUEST',
+ 'USER_DECLINED_CONSENT',
+ ];
+
+ types.forEach(type => {
+ const log = new VPShareActivityLog({type});
+ expect(log.type).toBe(type);
+ });
+ });
+ });
+
+ describe('getActionText', () => {
+ it('should return formatted action text', () => {
+ const log = new VPShareActivityLog({
+ type: 'SHARED_SUCCESSFULLY',
+ info: 'Test info',
+ });
+
+ const mockT = jest.fn(key => `Translated: ${key}`);
+ const result = log.getActionText(mockT);
+
+ expect(mockT).toHaveBeenCalledWith(
+ 'ActivityLogText:vpSharing:SHARED_SUCCESSFULLY',
+ {info: 'Test info'},
+ );
+ expect(result).toContain('Translated:');
+ });
+
+ it('should handle empty type', () => {
+ const log = new VPShareActivityLog({type: ''});
+ const mockT = jest.fn(key => key);
+
+ log.getActionText(mockT);
+ expect(mockT).toHaveBeenCalled();
+ });
+
+ it('should pass info to translation function', () => {
+ const log = new VPShareActivityLog({
+ type: 'TECHNICAL_ERROR',
+ info: 'Error details',
+ });
+
+ const mockT = jest.fn();
+ log.getActionText(mockT);
+
+ expect(mockT).toHaveBeenCalledWith(
+ expect.any(String),
+ expect.objectContaining({info: 'Error details'}),
+ );
+ });
+ });
+
+ describe('getLogFromObject', () => {
+ it('should create log from object', () => {
+ const data = {
+ type: 'SHARED_SUCCESSFULLY',
+ timestamp: 1234567890,
+ flow: VCItemContainerFlowType.VP_SHARE,
+ info: 'Test',
+ };
+
+ const log = VPShareActivityLog.getLogFromObject(data);
+
+ expect(log).toBeInstanceOf(VPShareActivityLog);
+ expect(log.type).toBe('SHARED_SUCCESSFULLY');
+ expect(log.timestamp).toBe(1234567890);
+ });
+
+ it('should handle empty object', () => {
+ const log = VPShareActivityLog.getLogFromObject({});
+
+ expect(log).toBeInstanceOf(VPShareActivityLog);
+ expect(log.type).toBe('');
+ });
+
+ it('should handle partial object', () => {
+ const log = VPShareActivityLog.getLogFromObject({
+ type: 'USER_DECLINED_CONSENT',
+ });
+
+ expect(log).toBeInstanceOf(VPShareActivityLog);
+ expect(log.type).toBe('USER_DECLINED_CONSENT');
+ });
+ });
+
+ describe('getActionLabel', () => {
+ it('should return formatted action label in English', () => {
+ const log = new VPShareActivityLog({
+ timestamp: Date.now() - 60000, // 1 minute ago
+ });
+
+ const label = log.getActionLabel('enUS');
+
+ expect(label).toBeDefined();
+ expect(typeof label).toBe('string');
+ expect(label.length).toBeGreaterThan(0);
+ });
+
+ it('should handle different languages', () => {
+ const log = new VPShareActivityLog({
+ timestamp: Date.now() - 3600000, // 1 hour ago
+ });
+
+ const languages = ['enUS', 'hi', 'kn', 'ta', 'ar'];
+
+ languages.forEach(lang => {
+ const label = log.getActionLabel(lang);
+ expect(label).toBeDefined();
+ expect(typeof label).toBe('string');
+ });
+ });
+
+ it('should show relative time', () => {
+ const recentTimestamp = Date.now() - 5000; // 5 seconds ago
+ const log = new VPShareActivityLog({timestamp: recentTimestamp});
+
+ const label = log.getActionLabel('enUS');
+
+ expect(label).toBeDefined();
+ expect(label.length).toBeGreaterThan(0);
+ });
+
+ it('should handle old timestamps', () => {
+ const oldTimestamp = Date.now() - 86400000; // 1 day ago
+ const log = new VPShareActivityLog({timestamp: oldTimestamp});
+
+ const label = log.getActionLabel('enUS');
+
+ expect(label).toBeDefined();
+ expect(label).toContain('ago');
+ });
+ });
+
+ describe('Activity Log Types', () => {
+ it('should handle all success types', () => {
+ const successTypes: Array<
+ | 'SHARED_SUCCESSFULLY'
+ | 'SHARED_WITH_FACE_VERIFIACTION'
+ | 'SHARED_AFTER_RETRY'
+ | 'SHARED_WITH_FACE_VERIFICATION_AFTER_RETRY'
+ > = [
+ 'SHARED_SUCCESSFULLY',
+ 'SHARED_WITH_FACE_VERIFIACTION',
+ 'SHARED_AFTER_RETRY',
+ 'SHARED_WITH_FACE_VERIFICATION_AFTER_RETRY',
+ ];
+
+ successTypes.forEach(type => {
+ const log = new VPShareActivityLog({type});
+ expect(log.type).toBe(type);
+ });
+ });
+
+ it('should handle all error types', () => {
+ const errorTypes: Array<
+ | 'VERIFIER_AUTHENTICATION_FAILED'
+ | 'INVALID_AUTH_REQUEST'
+ | 'RETRY_ATTEMPT_FAILED'
+ | 'MAX_RETRY_ATTEMPT_FAILED'
+ | 'FACE_VERIFICATION_FAILED'
+ | 'TECHNICAL_ERROR'
+ > = [
+ 'VERIFIER_AUTHENTICATION_FAILED',
+ 'INVALID_AUTH_REQUEST',
+ 'RETRY_ATTEMPT_FAILED',
+ 'MAX_RETRY_ATTEMPT_FAILED',
+ 'FACE_VERIFICATION_FAILED',
+ 'TECHNICAL_ERROR',
+ ];
+
+ errorTypes.forEach(type => {
+ const log = new VPShareActivityLog({type});
+ expect(log.type).toBe(type);
+ });
+ });
+
+ it('should handle credential-related types', () => {
+ const credentialTypes: Array<
+ | 'NO_SELECTED_VC_HAS_IMAGE'
+ | 'CREDENTIAL_MISMATCH_FROM_KEBAB'
+ | 'NO_CREDENTIAL_MATCHING_REQUEST'
+ > = [
+ 'NO_SELECTED_VC_HAS_IMAGE',
+ 'CREDENTIAL_MISMATCH_FROM_KEBAB',
+ 'NO_CREDENTIAL_MATCHING_REQUEST',
+ ];
+
+ credentialTypes.forEach(type => {
+ const log = new VPShareActivityLog({type});
+ expect(log.type).toBe(type);
+ });
+ });
+ });
+});
diff --git a/components/VcItemContainerProfileImage.test.tsx b/components/VcItemContainerProfileImage.test.tsx
new file mode 100644
index 00000000..da90b178
--- /dev/null
+++ b/components/VcItemContainerProfileImage.test.tsx
@@ -0,0 +1,71 @@
+import React from 'react';
+import {render} from '@testing-library/react-native';
+import {VcItemContainerProfileImage} from './VcItemContainerProfileImage';
+import {View} from 'react-native';
+
+// Mock SvgImage
+jest.mock('./ui/svg', () => ({
+ SvgImage: {
+ pinIcon: jest.fn(() => ),
+ },
+}));
+
+// Mock ProfileIcon
+jest.mock('./ProfileIcon', () => ({
+ ProfileIcon: jest.fn(() => ),
+}));
+
+describe('VcItemContainerProfileImage Component', () => {
+ const vcDataWithImage = {
+ face: 'https://example.com/avatar.jpg',
+ };
+
+ const vcDataWithoutImage = {
+ face: null,
+ };
+
+ it('should match snapshot with face image', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with face image and pinned', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot without face image', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot without face image and pinned', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should match snapshot with empty string face', () => {
+ const {toJSON} = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+});
diff --git a/components/VerifiedIcon.test.tsx b/components/VerifiedIcon.test.tsx
new file mode 100644
index 00000000..50fafc67
--- /dev/null
+++ b/components/VerifiedIcon.test.tsx
@@ -0,0 +1,30 @@
+import React from 'react';
+import {render} from '@testing-library/react-native';
+import VerifiedIcon from './VerifiedIcon';
+
+describe('VerifiedIcon', () => {
+ it('should render VerifiedIcon component', () => {
+ const {toJSON} = render();
+ expect(toJSON()).toBeTruthy();
+ });
+
+ it('should match snapshot', () => {
+ const {toJSON} = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should have proper styling structure', () => {
+ const {toJSON} = render();
+ const tree = toJSON();
+
+ // Verify component structure exists
+ expect(tree).toBeTruthy();
+ expect(tree.children).toBeTruthy();
+ });
+
+ it('should render with check-circle icon', () => {
+ const {toJSON} = render();
+ const tree = toJSON();
+ expect(tree).toBeTruthy();
+ });
+});
diff --git a/components/__snapshots__/AccountInformation.test.tsx.snap b/components/__snapshots__/AccountInformation.test.tsx.snap
new file mode 100644
index 00000000..35ba47fe
--- /dev/null
+++ b/components/__snapshots__/AccountInformation.test.tsx.snap
@@ -0,0 +1,821 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`AccountInformation Component should match snapshot with different email 1`] = `
+
+
+
+
+
+
+
+ associatedAccount
+
+
+
+
+ another@test.com
+
+
+
+
+`;
+
+exports[`AccountInformation Component should match snapshot with different picture URL 1`] = `
+
+
+
+
+
+
+
+ associatedAccount
+
+
+
+
+ test@example.com
+
+
+
+
+`;
+
+exports[`AccountInformation Component should match snapshot with email and picture 1`] = `
+
+
+
+
+
+
+
+ associatedAccount
+
+
+
+
+ test@example.com
+
+
+
+
+`;
+
+exports[`AccountInformation Component should match snapshot with long email 1`] = `
+
+
+
+
+
+
+
+ associatedAccount
+
+
+
+
+ very.long.email.address@example-domain.com
+
+
+
+
+`;
diff --git a/components/__snapshots__/ActivityLogText.test.tsx.snap b/components/__snapshots__/ActivityLogText.test.tsx.snap
new file mode 100644
index 00000000..7de89d8a
--- /dev/null
+++ b/components/__snapshots__/ActivityLogText.test.tsx.snap
@@ -0,0 +1,5 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ActivityLogText Component should match snapshot with VC activity 1`] = `null`;
+
+exports[`ActivityLogText Component should match snapshot with VP activity 1`] = `null`;
diff --git a/components/__snapshots__/BackupAndRestoreBannerNotification.test.tsx.snap b/components/__snapshots__/BackupAndRestoreBannerNotification.test.tsx.snap
new file mode 100644
index 00000000..c694cafc
--- /dev/null
+++ b/components/__snapshots__/BackupAndRestoreBannerNotification.test.tsx.snap
@@ -0,0 +1,3 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`BackupAndRestoreBannerNotification Component should match snapshot with no banners 1`] = `null`;
diff --git a/components/__snapshots__/BannerNotification.test.tsx.snap b/components/__snapshots__/BannerNotification.test.tsx.snap
new file mode 100644
index 00000000..9f49314c
--- /dev/null
+++ b/components/__snapshots__/BannerNotification.test.tsx.snap
@@ -0,0 +1,851 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`BannerNotification Component should match snapshot with different testId 1`] = `
+
+
+
+
+ Test notification message
+
+
+
+
+
+
+
+`;
+
+exports[`BannerNotification Component should match snapshot with error status 1`] = `
+
+
+
+
+ Test notification message
+
+
+
+
+
+
+
+`;
+
+exports[`BannerNotification Component should match snapshot with in progress status 1`] = `
+
+
+
+
+ Test notification message
+
+
+
+
+
+
+
+`;
+
+exports[`BannerNotification Component should match snapshot with long message 1`] = `
+
+
+
+
+ This is a very long notification message that should wrap to multiple lines and still be displayed correctly
+
+
+
+
+
+
+
+`;
+
+exports[`BannerNotification Component should match snapshot with success status 1`] = `
+
+
+
+
+ Test notification message
+
+
+
+
+
+
+
+`;
diff --git a/components/__snapshots__/BannerNotificationContainer.test.tsx.snap b/components/__snapshots__/BannerNotificationContainer.test.tsx.snap
new file mode 100644
index 00000000..ced6e873
--- /dev/null
+++ b/components/__snapshots__/BannerNotificationContainer.test.tsx.snap
@@ -0,0 +1,7 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`BannerNotificationContainer Component should match snapshot with no banners visible 1`] = `null`;
+
+exports[`BannerNotificationContainer Component should match snapshot with verification banner disabled 1`] = `null`;
+
+exports[`BannerNotificationContainer Component should match snapshot with verification banner enabled 1`] = `null`;
diff --git a/components/__snapshots__/CopilotTooltip.test.tsx.snap b/components/__snapshots__/CopilotTooltip.test.tsx.snap
new file mode 100644
index 00000000..6622b3e0
--- /dev/null
+++ b/components/__snapshots__/CopilotTooltip.test.tsx.snap
@@ -0,0 +1,9 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`CopilotTooltip Component should match snapshot with first step 1`] = `
+[
+ "Step 1 Title",
+ "Step 1 Description",
+ "1/5",
+]
+`;
diff --git a/components/__snapshots__/CopyButton.test.tsx.snap b/components/__snapshots__/CopyButton.test.tsx.snap
new file mode 100644
index 00000000..7f320b76
--- /dev/null
+++ b/components/__snapshots__/CopyButton.test.tsx.snap
@@ -0,0 +1,268 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`CopyButton Component should match snapshot with default props 1`] = `
+
+
+
+ clipboard.copy
+
+
+
+`;
+
+exports[`CopyButton Component should match snapshot with long content 1`] = `
+
+
+
+ clipboard.copy
+
+
+
+`;
+
+exports[`CopyButton Component should match snapshot with special characters 1`] = `
+
+
+
+ clipboard.copy
+
+
+
+`;
diff --git a/components/__snapshots__/DropdownIcon.test.tsx.snap b/components/__snapshots__/DropdownIcon.test.tsx.snap
new file mode 100644
index 00000000..dd600c8e
--- /dev/null
+++ b/components/__snapshots__/DropdownIcon.test.tsx.snap
@@ -0,0 +1,9 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`DropdownIcon Component should match snapshot with default props 1`] = ``;
+
+exports[`DropdownIcon Component should match snapshot with different icon 1`] = ``;
+
+exports[`DropdownIcon Component should match snapshot with empty items 1`] = ``;
+
+exports[`DropdownIcon Component should match snapshot with multiple items 1`] = ``;
diff --git a/components/__snapshots__/DualMessageOverlay.test.tsx.snap b/components/__snapshots__/DualMessageOverlay.test.tsx.snap
new file mode 100644
index 00000000..5a0b98fe
--- /dev/null
+++ b/components/__snapshots__/DualMessageOverlay.test.tsx.snap
@@ -0,0 +1,170 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`DualMessageOverlay Component should match snapshot with both buttons 1`] = `
+[
+ "Confirm Action",
+ "Are you sure you want to proceed?",
+
+ tryAgain
+ ,
+
+ ignore
+ ,
+]
+`;
+
+exports[`DualMessageOverlay Component should match snapshot with children 1`] = `
+[
+ "Confirm Action",
+ "Are you sure you want to proceed?",
+
+ Custom content here
+ ,
+]
+`;
+
+exports[`DualMessageOverlay Component should match snapshot with custom height 1`] = `
+[
+ "Confirm Action",
+ "Are you sure you want to proceed?",
+]
+`;
+
+exports[`DualMessageOverlay Component should match snapshot with hint text 1`] = `
+[
+ "Confirm Action",
+ "Are you sure you want to proceed?",
+ "Additional information",
+]
+`;
+
+exports[`DualMessageOverlay Component should match snapshot with only ignore button 1`] = `
+[
+ "Confirm Action",
+ "Are you sure you want to proceed?",
+
+ ignore
+ ,
+]
+`;
+
+exports[`DualMessageOverlay Component should match snapshot with only try again button 1`] = `
+[
+ "Confirm Action",
+ "Are you sure you want to proceed?",
+
+ tryAgain
+ ,
+]
+`;
+
+exports[`DualMessageOverlay Component should match snapshot with title and message 1`] = `
+[
+ "Confirm Action",
+ "Are you sure you want to proceed?",
+]
+`;
diff --git a/components/__snapshots__/EditableListItem.test.tsx.snap b/components/__snapshots__/EditableListItem.test.tsx.snap
new file mode 100644
index 00000000..02ee9054
--- /dev/null
+++ b/components/__snapshots__/EditableListItem.test.tsx.snap
@@ -0,0 +1,55 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EditableListItem Component should match snapshot with custom title color 1`] = `
+[
+ "Contact Information",
+ "Edit your details",
+ "editLabel",
+ "editLabel",
+]
+`;
+
+exports[`EditableListItem Component should match snapshot with default props 1`] = `
+[
+ "Contact Information",
+ "Edit your details",
+ "editLabel",
+ "editLabel",
+]
+`;
+
+exports[`EditableListItem Component should match snapshot with error state 1`] = `
+[
+ "Contact Information",
+ "Edit your details",
+ "editLabel",
+ "Failed to update",
+ "editLabel",
+]
+`;
+
+exports[`EditableListItem Component should match snapshot with progress indicator 1`] = `
+[
+ "Contact Information",
+ "Edit your details",
+ "editLabel",
+ "editLabel",
+]
+`;
+
+exports[`EditableListItem Component should match snapshot with single item 1`] = `
+[
+ "Contact Information",
+ "Edit your details",
+ "editLabel",
+]
+`;
+
+exports[`EditableListItem Component should match snapshot with success response 1`] = `
+[
+ "Contact Information",
+ "Edit your details",
+ "editLabel",
+ "editLabel",
+]
+`;
diff --git a/components/__snapshots__/GlobalContextProvider.test.tsx.snap b/components/__snapshots__/GlobalContextProvider.test.tsx.snap
new file mode 100644
index 00000000..1f4a7f00
--- /dev/null
+++ b/components/__snapshots__/GlobalContextProvider.test.tsx.snap
@@ -0,0 +1,18 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`GlobalContextProvider Component should match snapshot with children 1`] = `
+
+ Test Child
+
+`;
+
+exports[`GlobalContextProvider Component should match snapshot with multiple children 1`] = `
+[
+
+ Child 1
+ ,
+
+ Child 2
+ ,
+]
+`;
diff --git a/components/__snapshots__/HelpScreen.test.tsx.snap b/components/__snapshots__/HelpScreen.test.tsx.snap
new file mode 100644
index 00000000..cbe83729
--- /dev/null
+++ b/components/__snapshots__/HelpScreen.test.tsx.snap
@@ -0,0 +1,6561 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`HelpScreen Component should match snapshot when disabled 1`] = `
+[
+
+
+ Help
+
+ ,
+
+
+
+
+ answers.inji.one
+
+
+ ,
+ "title": "questions.inji.one",
+ },
+ {
+ "data":
+
+ answers.inji.two
+
+
+ here
+
+
+ ,
+ "title": "questions.inji.two",
+ },
+ {
+ "data":
+
+ answers.inji.three-a
+
+
+
+ answers.inji.three-b
+
+
+
+ answers.inji.three-c
+
+
+ ,
+ "title": "questions.inji.three",
+ },
+ {
+ "data":
+
+ answers.inji.four
+
+
+ ,
+ "title": "questions.inji.four",
+ },
+ {
+ "data":
+
+ answers.inji.five
+
+
+ ,
+ "title": "questions.inji.five",
+ },
+ {
+ "data":
+
+ answers.inji.six
+
+
+ ,
+ "title": "questions.inji.six",
+ },
+ {
+ "data":
+
+ answers.inji.seven
+
+
+ here
+
+
+ ,
+ "title": "questions.inji.seven",
+ },
+ {
+ "data":
+
+ answers.inji.eight
+
+
+ ,
+ "title": "questions.inji.eight",
+ },
+ {
+ "data":
+
+ answers.inji.nine
+
+
+ here
+
+
+ ,
+ "title": "questions.inji.nine",
+ },
+ {
+ "data":
+
+ answers.inji.ten-a
+
+
+ here
+
+
+
+ answers.inji.ten-b
+
+
+ ,
+ "title": "questions.inji.ten",
+ },
+ {
+ "data":
+
+ answers.inji.eleven
+
+
+ ,
+ "title": "questions.inji.eleven",
+ },
+ {
+ "data":
+
+ answers.inji.twelve
+
+
+ ,
+ "title": "questions.inji.twelve",
+ },
+ {
+ "data":
+
+ answers.inji.sixteen
+
+
+ ,
+ "title": "questions.inji.sixteen",
+ },
+ {
+ "data":
+
+ answers.inji.seventeen
+
+
+ ,
+ "title": "questions.inji.seventeen",
+ },
+ {
+ "data":
+
+ answers.inji.thirteen-a
+
+
+ here
+
+
+
+ answers.inji.thirteen-b
+
+
+ ,
+ "title": "questions.inji.thirteen",
+ },
+ {
+ "data":
+
+ answers.inji.fourteen
+
+
+ ,
+ "title": "questions.inji.fourteen",
+ },
+ {
+ "data":
+
+ answers.inji.fifteen
+
+
+ ,
+ "title": "questions.inji.fifteen",
+ },
+ {
+ "data":
+
+ answers.inji.fifteen
+
+
+ ,
+ "title": "questions.inji.fifteen",
+ },
+ {
+ "data":
+
+ answers.backup.one
+
+
+ ,
+ "title": "questions.backup.one",
+ },
+ {
+ "data":
+
+ answers.backup.two
+
+
+ ,
+ "title": "questions.backup.two",
+ },
+ {
+ "data":
+
+ answers.backup.three
+
+
+ ,
+ "title": "questions.backup.three",
+ },
+ {
+ "data":
+
+ answers.backup.four
+
+
+ ,
+ "title": "questions.backup.four",
+ },
+ {
+ "data":
+
+ answers.backup.five
+
+
+ ,
+ "title": "questions.backup.five",
+ },
+ {
+ "data":
+
+ answers.backup.six
+
+
+ ,
+ "title": "questions.backup.six",
+ },
+ {
+ "data":
+
+ answers.backup.seven
+
+
+ ,
+ "title": "questions.backup.seven",
+ },
+ {
+ "data":
+
+ answers.backup.eight
+
+
+ ,
+ "title": "questions.backup.eight",
+ },
+ {
+ "data":
+
+ answers.KeyManagement.one
+
+
+ ,
+ "title": "questions.KeyManagement.one",
+ },
+ {
+ "data":
+
+ answers.KeyManagement.two
+
+
+ ,
+ "title": "questions.KeyManagement.two",
+ },
+ {
+ "data":
+
+ answers.KeyManagement.three
+
+
+ ,
+ "title": "questions.KeyManagement.three",
+ },
+ {
+ "data":
+
+ answers.KeyManagement.four
+
+
+ ,
+ "title": "questions.KeyManagement.four",
+ },
+ {
+ "data":
+
+ answers.KeyManagement.five
+
+
+ ,
+ "title": "questions.KeyManagement.five",
+ },
+ {
+ "data":
+
+ answers.KeyManagement.six
+
+
+ ,
+ "title": "questions.KeyManagement.six",
+ },
+ ]
+ }
+ getItem={[Function]}
+ getItemCount={[Function]}
+ keyExtractor={[Function]}
+ onContentSizeChange={[Function]}
+ onLayout={[Function]}
+ onMomentumScrollBegin={[Function]}
+ onMomentumScrollEnd={[Function]}
+ onScroll={[Function]}
+ onScrollBeginDrag={[Function]}
+ onScrollEndDrag={[Function]}
+ onScrollToIndexFailed={[Function]}
+ removeClippedSubviews={false}
+ renderItem={[Function]}
+ scrollEventThrottle={0.0001}
+ stickyHeaderIndices={[]}
+ viewabilityConfigCallbackPairs={[]}
+ >
+
+
+
+
+ questions.inji.one
+
+
+ answers.inji.one
+
+
+
+
+
+
+
+ questions.inji.two
+
+
+ answers.inji.two
+
+
+ here
+
+
+
+
+
+
+
+ questions.inji.three
+
+
+ answers.inji.three-a
+
+
+
+ answers.inji.three-b
+
+
+
+ answers.inji.three-c
+
+
+
+
+
+
+
+ questions.inji.four
+
+
+ answers.inji.four
+
+
+
+
+
+
+
+ questions.inji.five
+
+
+ answers.inji.five
+
+
+
+
+
+
+
+ questions.inji.six
+
+
+ answers.inji.six
+
+
+
+
+
+
+
+ questions.inji.seven
+
+
+ answers.inji.seven
+
+
+ here
+
+
+
+
+
+
+
+ questions.inji.eight
+
+
+ answers.inji.eight
+
+
+
+
+
+
+
+ questions.inji.nine
+
+
+ answers.inji.nine
+
+
+ here
+
+
+
+
+
+
+
+ questions.inji.ten
+
+
+ answers.inji.ten-a
+
+
+ here
+
+
+
+ answers.inji.ten-b
+
+
+
+
+
+
+
+
+ ,
+]
+`;
+
+exports[`HelpScreen Component should match snapshot with BackUp source 1`] = `
+[
+
+
+ Help
+
+ ,
+
+
+
+
+ answers.inji.one
+
+
+ ,
+ "title": "questions.inji.one",
+ },
+ {
+ "data":
+
+ answers.inji.two
+
+
+ here
+
+
+ ,
+ "title": "questions.inji.two",
+ },
+ {
+ "data":
+
+ answers.inji.three-a
+
+
+
+ answers.inji.three-b
+
+
+
+ answers.inji.three-c
+
+
+ ,
+ "title": "questions.inji.three",
+ },
+ {
+ "data":
+
+ answers.inji.four
+
+
+ ,
+ "title": "questions.inji.four",
+ },
+ {
+ "data":
+
+ answers.inji.five
+
+
+ ,
+ "title": "questions.inji.five",
+ },
+ {
+ "data":
+
+ answers.inji.six
+
+
+ ,
+ "title": "questions.inji.six",
+ },
+ {
+ "data":
+
+ answers.inji.seven
+
+
+ here
+
+
+ ,
+ "title": "questions.inji.seven",
+ },
+ {
+ "data":
+
+ answers.inji.eight
+
+
+ ,
+ "title": "questions.inji.eight",
+ },
+ {
+ "data":
+
+ answers.inji.nine
+
+
+ here
+
+
+ ,
+ "title": "questions.inji.nine",
+ },
+ {
+ "data":
+
+ answers.inji.ten-a
+
+
+ here
+
+
+
+ answers.inji.ten-b
+
+
+ ,
+ "title": "questions.inji.ten",
+ },
+ {
+ "data":
+
+ answers.inji.eleven
+
+
+ ,
+ "title": "questions.inji.eleven",
+ },
+ {
+ "data":
+
+ answers.inji.twelve
+
+
+ ,
+ "title": "questions.inji.twelve",
+ },
+ {
+ "data":
+
+ answers.inji.sixteen
+
+
+ ,
+ "title": "questions.inji.sixteen",
+ },
+ {
+ "data":
+
+ answers.inji.seventeen
+
+
+ ,
+ "title": "questions.inji.seventeen",
+ },
+ {
+ "data":
+
+ answers.inji.thirteen-a
+
+
+ here
+
+
+
+ answers.inji.thirteen-b
+
+
+ ,
+ "title": "questions.inji.thirteen",
+ },
+ {
+ "data":
+
+ answers.inji.fourteen
+
+
+ ,
+ "title": "questions.inji.fourteen",
+ },
+ {
+ "data":
+
+ answers.inji.fifteen
+
+
+ ,
+ "title": "questions.inji.fifteen",
+ },
+ {
+ "data":
+
+ answers.inji.fifteen
+
+
+ ,
+ "title": "questions.inji.fifteen",
+ },
+ {
+ "data":
+
+ answers.backup.one
+
+
+ ,
+ "title": "questions.backup.one",
+ },
+ {
+ "data":
+
+ answers.backup.two
+
+
+ ,
+ "title": "questions.backup.two",
+ },
+ {
+ "data":
+
+ answers.backup.three
+
+
+ ,
+ "title": "questions.backup.three",
+ },
+ {
+ "data":
+
+ answers.backup.four
+
+
+ ,
+ "title": "questions.backup.four",
+ },
+ {
+ "data":
+
+ answers.backup.five
+
+
+ ,
+ "title": "questions.backup.five",
+ },
+ {
+ "data":
+
+ answers.backup.six
+
+
+ ,
+ "title": "questions.backup.six",
+ },
+ {
+ "data":
+
+ answers.backup.seven
+
+
+ ,
+ "title": "questions.backup.seven",
+ },
+ {
+ "data":
+
+ answers.backup.eight
+
+
+ ,
+ "title": "questions.backup.eight",
+ },
+ {
+ "data":
+
+ answers.KeyManagement.one
+
+
+ ,
+ "title": "questions.KeyManagement.one",
+ },
+ {
+ "data":
+
+ answers.KeyManagement.two
+
+
+ ,
+ "title": "questions.KeyManagement.two",
+ },
+ {
+ "data":
+
+ answers.KeyManagement.three
+
+
+ ,
+ "title": "questions.KeyManagement.three",
+ },
+ {
+ "data":
+
+ answers.KeyManagement.four
+
+
+ ,
+ "title": "questions.KeyManagement.four",
+ },
+ {
+ "data":
+
+ answers.KeyManagement.five
+
+
+ ,
+ "title": "questions.KeyManagement.five",
+ },
+ {
+ "data":
+
+ answers.KeyManagement.six
+
+
+ ,
+ "title": "questions.KeyManagement.six",
+ },
+ ]
+ }
+ getItem={[Function]}
+ getItemCount={[Function]}
+ keyExtractor={[Function]}
+ onContentSizeChange={[Function]}
+ onLayout={[Function]}
+ onMomentumScrollBegin={[Function]}
+ onMomentumScrollEnd={[Function]}
+ onScroll={[Function]}
+ onScrollBeginDrag={[Function]}
+ onScrollEndDrag={[Function]}
+ onScrollToIndexFailed={[Function]}
+ removeClippedSubviews={false}
+ renderItem={[Function]}
+ scrollEventThrottle={0.0001}
+ stickyHeaderIndices={[]}
+ viewabilityConfigCallbackPairs={[]}
+ >
+
+
+
+
+ questions.inji.one
+
+
+ answers.inji.one
+
+
+
+
+
+
+
+ questions.inji.two
+
+
+ answers.inji.two
+
+
+ here
+
+
+
+
+
+
+
+ questions.inji.three
+
+
+ answers.inji.three-a
+
+
+
+ answers.inji.three-b
+
+
+
+ answers.inji.three-c
+
+
+
+
+
+
+
+ questions.inji.four
+
+
+ answers.inji.four
+
+
+
+
+
+
+
+ questions.inji.five
+
+
+ answers.inji.five
+
+
+
+
+
+
+
+ questions.inji.six
+
+
+ answers.inji.six
+
+
+
+
+
+
+
+ questions.inji.seven
+
+
+ answers.inji.seven
+
+
+ here
+
+
+
+
+
+
+
+ questions.inji.eight
+
+
+ answers.inji.eight
+
+
+
+
+
+
+
+ questions.inji.nine
+
+
+ answers.inji.nine
+
+
+ here
+
+
+
+
+
+
+
+ questions.inji.ten
+
+
+ answers.inji.ten-a
+
+
+ here
+
+
+
+ answers.inji.ten-b
+
+
+
+
+
+
+
+
+ ,
+]
+`;
+
+exports[`HelpScreen Component should match snapshot with Inji source 1`] = `
+[
+
+
+ Help
+
+ ,
+
+
+
+
+ answers.inji.one
+
+
+ ,
+ "title": "questions.inji.one",
+ },
+ {
+ "data":
+
+ answers.inji.two
+
+
+ here
+
+
+ ,
+ "title": "questions.inji.two",
+ },
+ {
+ "data":
+
+ answers.inji.three-a
+
+
+
+ answers.inji.three-b
+
+
+
+ answers.inji.three-c
+
+
+ ,
+ "title": "questions.inji.three",
+ },
+ {
+ "data":
+
+ answers.inji.four
+
+
+ ,
+ "title": "questions.inji.four",
+ },
+ {
+ "data":
+
+ answers.inji.five
+
+
+ ,
+ "title": "questions.inji.five",
+ },
+ {
+ "data":
+
+ answers.inji.six
+
+
+ ,
+ "title": "questions.inji.six",
+ },
+ {
+ "data":
+
+ answers.inji.seven
+
+
+ here
+
+
+ ,
+ "title": "questions.inji.seven",
+ },
+ {
+ "data":
+
+ answers.inji.eight
+
+
+ ,
+ "title": "questions.inji.eight",
+ },
+ {
+ "data":
+
+ answers.inji.nine
+
+
+ here
+
+
+ ,
+ "title": "questions.inji.nine",
+ },
+ {
+ "data":
+
+ answers.inji.ten-a
+
+
+ here
+
+
+
+ answers.inji.ten-b
+
+
+ ,
+ "title": "questions.inji.ten",
+ },
+ {
+ "data":
+
+ answers.inji.eleven
+
+
+ ,
+ "title": "questions.inji.eleven",
+ },
+ {
+ "data":
+
+ answers.inji.twelve
+
+
+ ,
+ "title": "questions.inji.twelve",
+ },
+ {
+ "data":
+
+ answers.inji.sixteen
+
+
+ ,
+ "title": "questions.inji.sixteen",
+ },
+ {
+ "data":
+
+ answers.inji.seventeen
+
+
+ ,
+ "title": "questions.inji.seventeen",
+ },
+ {
+ "data":
+
+ answers.inji.thirteen-a
+
+
+ here
+
+
+
+ answers.inji.thirteen-b
+
+
+ ,
+ "title": "questions.inji.thirteen",
+ },
+ {
+ "data":
+
+ answers.inji.fourteen
+
+
+ ,
+ "title": "questions.inji.fourteen",
+ },
+ {
+ "data":
+
+ answers.inji.fifteen
+
+
+ ,
+ "title": "questions.inji.fifteen",
+ },
+ {
+ "data":
+
+ answers.inji.fifteen
+
+
+ ,
+ "title": "questions.inji.fifteen",
+ },
+ {
+ "data":
+
+ answers.backup.one
+
+
+ ,
+ "title": "questions.backup.one",
+ },
+ {
+ "data":
+
+ answers.backup.two
+
+
+ ,
+ "title": "questions.backup.two",
+ },
+ {
+ "data":
+
+ answers.backup.three
+
+
+ ,
+ "title": "questions.backup.three",
+ },
+ {
+ "data":
+
+ answers.backup.four
+
+
+ ,
+ "title": "questions.backup.four",
+ },
+ {
+ "data":
+
+ answers.backup.five
+
+
+ ,
+ "title": "questions.backup.five",
+ },
+ {
+ "data":
+
+ answers.backup.six
+
+
+ ,
+ "title": "questions.backup.six",
+ },
+ {
+ "data":
+
+ answers.backup.seven
+
+
+ ,
+ "title": "questions.backup.seven",
+ },
+ {
+ "data":
+
+ answers.backup.eight
+
+
+ ,
+ "title": "questions.backup.eight",
+ },
+ {
+ "data":
+
+ answers.KeyManagement.one
+
+
+ ,
+ "title": "questions.KeyManagement.one",
+ },
+ {
+ "data":
+
+ answers.KeyManagement.two
+
+
+ ,
+ "title": "questions.KeyManagement.two",
+ },
+ {
+ "data":
+
+ answers.KeyManagement.three
+
+
+ ,
+ "title": "questions.KeyManagement.three",
+ },
+ {
+ "data":
+
+ answers.KeyManagement.four
+
+
+ ,
+ "title": "questions.KeyManagement.four",
+ },
+ {
+ "data":
+
+ answers.KeyManagement.five
+
+
+ ,
+ "title": "questions.KeyManagement.five",
+ },
+ {
+ "data":
+
+ answers.KeyManagement.six
+
+
+ ,
+ "title": "questions.KeyManagement.six",
+ },
+ ]
+ }
+ getItem={[Function]}
+ getItemCount={[Function]}
+ keyExtractor={[Function]}
+ onContentSizeChange={[Function]}
+ onLayout={[Function]}
+ onMomentumScrollBegin={[Function]}
+ onMomentumScrollEnd={[Function]}
+ onScroll={[Function]}
+ onScrollBeginDrag={[Function]}
+ onScrollEndDrag={[Function]}
+ onScrollToIndexFailed={[Function]}
+ removeClippedSubviews={false}
+ renderItem={[Function]}
+ scrollEventThrottle={0.0001}
+ stickyHeaderIndices={[]}
+ viewabilityConfigCallbackPairs={[]}
+ >
+
+
+
+
+ questions.inji.one
+
+
+ answers.inji.one
+
+
+
+
+
+
+
+ questions.inji.two
+
+
+ answers.inji.two
+
+
+ here
+
+
+
+
+
+
+
+ questions.inji.three
+
+
+ answers.inji.three-a
+
+
+
+ answers.inji.three-b
+
+
+
+ answers.inji.three-c
+
+
+
+
+
+
+
+ questions.inji.four
+
+
+ answers.inji.four
+
+
+
+
+
+
+
+ questions.inji.five
+
+
+ answers.inji.five
+
+
+
+
+
+
+
+ questions.inji.six
+
+
+ answers.inji.six
+
+
+
+
+
+
+
+ questions.inji.seven
+
+
+ answers.inji.seven
+
+
+ here
+
+
+
+
+
+
+
+ questions.inji.eight
+
+
+ answers.inji.eight
+
+
+
+
+
+
+
+ questions.inji.nine
+
+
+ answers.inji.nine
+
+
+ here
+
+
+
+
+
+
+
+ questions.inji.ten
+
+
+ answers.inji.ten-a
+
+
+ here
+
+
+
+ answers.inji.ten-b
+
+
+
+
+
+
+
+
+ ,
+]
+`;
+
+exports[`HelpScreen Component should match snapshot with keyManagement source 1`] = `
+[
+
+
+ Help
+
+ ,
+
+
+
+
+ answers.inji.one
+
+
+ ,
+ "title": "questions.inji.one",
+ },
+ {
+ "data":
+
+ answers.inji.two
+
+
+ here
+
+
+ ,
+ "title": "questions.inji.two",
+ },
+ {
+ "data":
+
+ answers.inji.three-a
+
+
+
+ answers.inji.three-b
+
+
+
+ answers.inji.three-c
+
+
+ ,
+ "title": "questions.inji.three",
+ },
+ {
+ "data":
+
+ answers.inji.four
+
+
+ ,
+ "title": "questions.inji.four",
+ },
+ {
+ "data":
+
+ answers.inji.five
+
+
+ ,
+ "title": "questions.inji.five",
+ },
+ {
+ "data":
+
+ answers.inji.six
+
+
+ ,
+ "title": "questions.inji.six",
+ },
+ {
+ "data":
+
+ answers.inji.seven
+
+
+ here
+
+
+ ,
+ "title": "questions.inji.seven",
+ },
+ {
+ "data":
+
+ answers.inji.eight
+
+
+ ,
+ "title": "questions.inji.eight",
+ },
+ {
+ "data":
+
+ answers.inji.nine
+
+
+ here
+
+
+ ,
+ "title": "questions.inji.nine",
+ },
+ {
+ "data":
+
+ answers.inji.ten-a
+
+
+ here
+
+
+
+ answers.inji.ten-b
+
+
+ ,
+ "title": "questions.inji.ten",
+ },
+ {
+ "data":
+
+ answers.inji.eleven
+
+
+ ,
+ "title": "questions.inji.eleven",
+ },
+ {
+ "data":
+
+ answers.inji.twelve
+
+
+ ,
+ "title": "questions.inji.twelve",
+ },
+ {
+ "data":
+
+ answers.inji.sixteen
+
+
+ ,
+ "title": "questions.inji.sixteen",
+ },
+ {
+ "data":
+
+ answers.inji.seventeen
+
+
+ ,
+ "title": "questions.inji.seventeen",
+ },
+ {
+ "data":
+
+ answers.inji.thirteen-a
+
+
+ here
+
+
+
+ answers.inji.thirteen-b
+
+
+ ,
+ "title": "questions.inji.thirteen",
+ },
+ {
+ "data":
+
+ answers.inji.fourteen
+
+
+ ,
+ "title": "questions.inji.fourteen",
+ },
+ {
+ "data":
+
+ answers.inji.fifteen
+
+
+ ,
+ "title": "questions.inji.fifteen",
+ },
+ {
+ "data":
+
+ answers.inji.fifteen
+
+
+ ,
+ "title": "questions.inji.fifteen",
+ },
+ {
+ "data":
+
+ answers.backup.one
+
+
+ ,
+ "title": "questions.backup.one",
+ },
+ {
+ "data":
+
+ answers.backup.two
+
+
+ ,
+ "title": "questions.backup.two",
+ },
+ {
+ "data":
+
+ answers.backup.three
+
+
+ ,
+ "title": "questions.backup.three",
+ },
+ {
+ "data":
+
+ answers.backup.four
+
+
+ ,
+ "title": "questions.backup.four",
+ },
+ {
+ "data":
+
+ answers.backup.five
+
+
+ ,
+ "title": "questions.backup.five",
+ },
+ {
+ "data":
+
+ answers.backup.six
+
+
+ ,
+ "title": "questions.backup.six",
+ },
+ {
+ "data":
+
+ answers.backup.seven
+
+
+ ,
+ "title": "questions.backup.seven",
+ },
+ {
+ "data":
+
+ answers.backup.eight
+
+
+ ,
+ "title": "questions.backup.eight",
+ },
+ {
+ "data":
+
+ answers.KeyManagement.one
+
+
+ ,
+ "title": "questions.KeyManagement.one",
+ },
+ {
+ "data":
+
+ answers.KeyManagement.two
+
+
+ ,
+ "title": "questions.KeyManagement.two",
+ },
+ {
+ "data":
+
+ answers.KeyManagement.three
+
+
+ ,
+ "title": "questions.KeyManagement.three",
+ },
+ {
+ "data":
+
+ answers.KeyManagement.four
+
+
+ ,
+ "title": "questions.KeyManagement.four",
+ },
+ {
+ "data":
+
+ answers.KeyManagement.five
+
+
+ ,
+ "title": "questions.KeyManagement.five",
+ },
+ {
+ "data":
+
+ answers.KeyManagement.six
+
+
+ ,
+ "title": "questions.KeyManagement.six",
+ },
+ ]
+ }
+ getItem={[Function]}
+ getItemCount={[Function]}
+ keyExtractor={[Function]}
+ onContentSizeChange={[Function]}
+ onLayout={[Function]}
+ onMomentumScrollBegin={[Function]}
+ onMomentumScrollEnd={[Function]}
+ onScroll={[Function]}
+ onScrollBeginDrag={[Function]}
+ onScrollEndDrag={[Function]}
+ onScrollToIndexFailed={[Function]}
+ removeClippedSubviews={false}
+ renderItem={[Function]}
+ scrollEventThrottle={0.0001}
+ stickyHeaderIndices={[]}
+ viewabilityConfigCallbackPairs={[]}
+ >
+
+
+
+
+ questions.inji.one
+
+
+ answers.inji.one
+
+
+
+
+
+
+
+ questions.inji.two
+
+
+ answers.inji.two
+
+
+ here
+
+
+
+
+
+
+
+ questions.inji.three
+
+
+ answers.inji.three-a
+
+
+
+ answers.inji.three-b
+
+
+
+ answers.inji.three-c
+
+
+
+
+
+
+
+ questions.inji.four
+
+
+ answers.inji.four
+
+
+
+
+
+
+
+ questions.inji.five
+
+
+ answers.inji.five
+
+
+
+
+
+
+
+ questions.inji.six
+
+
+ answers.inji.six
+
+
+
+
+
+
+
+ questions.inji.seven
+
+
+ answers.inji.seven
+
+
+ here
+
+
+
+
+
+
+
+ questions.inji.eight
+
+
+ answers.inji.eight
+
+
+
+
+
+
+
+ questions.inji.nine
+
+
+ answers.inji.nine
+
+
+ here
+
+
+
+
+
+
+
+ questions.inji.ten
+
+
+ answers.inji.ten-a
+
+
+ here
+
+
+
+ answers.inji.ten-b
+
+
+
+
+
+
+
+
+ ,
+]
+`;
diff --git a/components/__snapshots__/KebabPopUp.test.tsx.snap b/components/__snapshots__/KebabPopUp.test.tsx.snap
new file mode 100644
index 00000000..73c26f2d
--- /dev/null
+++ b/components/__snapshots__/KebabPopUp.test.tsx.snap
@@ -0,0 +1,1149 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`KebabPopUp Component should match snapshot when not visible 1`] = `
+
+
+
+ title
+
+
+
+
+
+ Pin Card
+
+
+
+
+
+ Remove
+
+
+
+`;
+
+exports[`KebabPopUp Component should match snapshot with VC that has image 1`] = `
+
+
+
+ title
+
+
+
+
+
+ Pin Card
+
+
+
+
+
+ Remove
+
+
+
+`;
+
+exports[`KebabPopUp Component should match snapshot with custom icon color 1`] = `
+
+
+
+ title
+
+
+
+
+
+ Pin Card
+
+
+
+
+
+ Remove
+
+
+
+`;
+
+exports[`KebabPopUp Component should match snapshot with custom icon component 1`] = `
+
+
+ Custom
+
+
+
+ title
+
+
+
+
+
+ Pin Card
+
+
+
+
+
+ Remove
+
+
+
+`;
+
+exports[`KebabPopUp Component should match snapshot with default icon 1`] = `
+
+
+
+ title
+
+
+
+
+
+ Pin Card
+
+
+
+
+
+ Remove
+
+
+
+`;
diff --git a/components/__snapshots__/LanguageSelector.test.tsx.snap b/components/__snapshots__/LanguageSelector.test.tsx.snap
new file mode 100644
index 00000000..7d6ee666
--- /dev/null
+++ b/components/__snapshots__/LanguageSelector.test.tsx.snap
@@ -0,0 +1,5 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`LanguageSelector Component should match snapshot with custom trigger component 1`] = ``;
+
+exports[`LanguageSelector Component should match snapshot with default trigger 1`] = ``;
diff --git a/components/__snapshots__/Message.test.tsx.snap b/components/__snapshots__/Message.test.tsx.snap
new file mode 100644
index 00000000..24898b20
--- /dev/null
+++ b/components/__snapshots__/Message.test.tsx.snap
@@ -0,0 +1,88 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Message Component should match snapshot with cancel button 1`] = `
+
+ Test
+
+`;
+
+exports[`Message Component should match snapshot with hint text 1`] = `
+
+ Test
+ Hint text
+
+`;
+
+exports[`Message Component should match snapshot with message only 1`] = `
+
+ Test Message
+
+`;
+
+exports[`Message Component should match snapshot with title and message 1`] = `
+
+ Title
+ Message
+
+`;
+
+exports[`Message Component should match snapshot with title only 1`] = `
+
+ Test Title
+
+`;
diff --git a/components/__snapshots__/MessageOverlay.test.tsx.snap b/components/__snapshots__/MessageOverlay.test.tsx.snap
new file mode 100644
index 00000000..9e889d46
--- /dev/null
+++ b/components/__snapshots__/MessageOverlay.test.tsx.snap
@@ -0,0 +1,68 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ErrorMessageOverlay Component should match snapshot with error 1`] = `
+[
+ "network.title",
+ "network.message",
+]
+`;
+
+exports[`ErrorMessageOverlay Component should match snapshot with testID 1`] = `
+[
+ "network.title",
+ "network.message",
+]
+`;
+
+exports[`MessageOverlay Component should match snapshot with button 1`] = `
+[
+ "Test Title",
+ "Test Message",
+]
+`;
+
+exports[`MessageOverlay Component should match snapshot with custom children 1`] = `
+[
+ "Test Title",
+ "Test Message",
+
+ Custom Content
+ ,
+]
+`;
+
+exports[`MessageOverlay Component should match snapshot with custom minHeight 1`] = `
+[
+ "Test Title",
+ "Test Message",
+]
+`;
+
+exports[`MessageOverlay Component should match snapshot with hint text 1`] = `
+[
+ "Test Title",
+ "Test Message",
+ "This is a hint",
+]
+`;
+
+exports[`MessageOverlay Component should match snapshot with numeric progress 1`] = `
+[
+ "Test Title",
+ "Test Message",
+]
+`;
+
+exports[`MessageOverlay Component should match snapshot with progress indicator 1`] = `
+[
+ "Test Title",
+ "Test Message",
+]
+`;
+
+exports[`MessageOverlay Component should match snapshot with title and message 1`] = `
+[
+ "Test Title",
+ "Test Message",
+]
+`;
diff --git a/components/__snapshots__/Passcode.test.tsx.snap b/components/__snapshots__/Passcode.test.tsx.snap
new file mode 100644
index 00000000..c77482f6
--- /dev/null
+++ b/components/__snapshots__/Passcode.test.tsx.snap
@@ -0,0 +1,629 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Passcode Component should match snapshot with both message and error 1`] = `
+
+
+
+
+ Enter passcode
+
+
+
+
+ Authentication failed
+
+
+
+
+`;
+
+exports[`Passcode Component should match snapshot with custom message 1`] = `
+
+
+
+
+ Please enter your 6-digit passcode
+
+
+
+
+
+
+
+`;
+
+exports[`Passcode Component should match snapshot with default props 1`] = `
+
+
+
+
+ Enter your passcode
+
+
+
+
+
+
+
+`;
+
+exports[`Passcode Component should match snapshot with error message 1`] = `
+
+
+
+
+ Enter your passcode
+
+
+
+
+ Incorrect passcode. Try again.
+
+
+
+
+`;
diff --git a/components/__snapshots__/PasscodeVerify.test.tsx.snap b/components/__snapshots__/PasscodeVerify.test.tsx.snap
new file mode 100644
index 00000000..ae9b747a
--- /dev/null
+++ b/components/__snapshots__/PasscodeVerify.test.tsx.snap
@@ -0,0 +1,7 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`PasscodeVerify Component should match snapshot with default props 1`] = `null`;
+
+exports[`PasscodeVerify Component should match snapshot with different testID 1`] = `null`;
+
+exports[`PasscodeVerify Component should match snapshot without onError handler 1`] = `null`;
diff --git a/components/__snapshots__/PendingIcon.test.tsx.snap b/components/__snapshots__/PendingIcon.test.tsx.snap
new file mode 100644
index 00000000..eb7406d5
--- /dev/null
+++ b/components/__snapshots__/PendingIcon.test.tsx.snap
@@ -0,0 +1,39 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`PendingIcon should match snapshot 1`] = `
+
+
+
+`;
+
+exports[`PendingIcon should render with custom color 1`] = `
+
+
+
+`;
diff --git a/components/__snapshots__/PinInput.test.tsx.snap b/components/__snapshots__/PinInput.test.tsx.snap
new file mode 100644
index 00000000..55b071f9
--- /dev/null
+++ b/components/__snapshots__/PinInput.test.tsx.snap
@@ -0,0 +1,719 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`PinInput Component should match snapshot with 4 digit PIN 1`] = `
+
+
+
+
+
+
+`;
+
+exports[`PinInput Component should match snapshot with 6 digit PIN 1`] = `
+
+
+
+
+
+
+
+
+`;
+
+exports[`PinInput Component should match snapshot with custom testID 1`] = `
+
+
+
+
+
+
+`;
+
+exports[`PinInput Component should match snapshot with onChange handler 1`] = `
+
+
+
+
+
+
+`;
+
+exports[`PinInput Component should match snapshot with onDone and autosubmit 1`] = `
+
+
+
+
+
+
+`;
diff --git a/components/__snapshots__/ProfileIcon.test.tsx.snap b/components/__snapshots__/ProfileIcon.test.tsx.snap
new file mode 100644
index 00000000..8e212b44
--- /dev/null
+++ b/components/__snapshots__/ProfileIcon.test.tsx.snap
@@ -0,0 +1,82 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ProfileIcon Component should match snapshot with custom container styles 1`] = `
+
+
+
+`;
+
+exports[`ProfileIcon Component should match snapshot with custom icon size 1`] = `
+
+
+
+`;
+
+exports[`ProfileIcon Component should match snapshot with pinned icon 1`] = `
+
+
+
+
+`;
+
+exports[`ProfileIcon Component should match snapshot without pinned icon 1`] = `
+
+
+
+`;
diff --git a/components/__snapshots__/ProgressingModal.test.tsx.snap b/components/__snapshots__/ProgressingModal.test.tsx.snap
new file mode 100644
index 00000000..3b38f533
--- /dev/null
+++ b/components/__snapshots__/ProgressingModal.test.tsx.snap
@@ -0,0 +1,25 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ProgressingModal Component should match snapshot as requester 1`] = `null`;
+
+exports[`ProgressingModal Component should match snapshot with BLE error visible 1`] = `"Bluetooth error occurred"`;
+
+exports[`ProgressingModal Component should match snapshot with default props 1`] = `null`;
+
+exports[`ProgressingModal Component should match snapshot with hint visible 1`] = `"Please wait..."`;
+
+exports[`ProgressingModal Component should match snapshot with progress spinner 1`] = `
+
+`;
+
+exports[`ProgressingModal Component should match snapshot with retry button 1`] = `"Connection failed"`;
+
+exports[`ProgressingModal Component should match snapshot with stay in progress button 1`] = `"Taking longer than expected"`;
diff --git a/components/__snapshots__/QrCodeOverlay.test.tsx.snap b/components/__snapshots__/QrCodeOverlay.test.tsx.snap
new file mode 100644
index 00000000..cf71eff7
--- /dev/null
+++ b/components/__snapshots__/QrCodeOverlay.test.tsx.snap
@@ -0,0 +1,9 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`QrCodeOverlay Component should match snapshot with default props 1`] = `null`;
+
+exports[`QrCodeOverlay Component should match snapshot with force visible 1`] = `null`;
+
+exports[`QrCodeOverlay Component should match snapshot with inline QR disabled 1`] = `null`;
+
+exports[`QrCodeOverlay Component should match snapshot with onClose handler 1`] = `null`;
diff --git a/components/__snapshots__/QrScanner.test.tsx.snap b/components/__snapshots__/QrScanner.test.tsx.snap
new file mode 100644
index 00000000..f1731d60
--- /dev/null
+++ b/components/__snapshots__/QrScanner.test.tsx.snap
@@ -0,0 +1,7 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`QrScanner Component should match snapshot with custom title 1`] = ``;
+
+exports[`QrScanner Component should match snapshot with default props 1`] = ``;
+
+exports[`QrScanner Component should match snapshot with title 1`] = ``;
diff --git a/components/__snapshots__/SectionLayout.test.tsx.snap b/components/__snapshots__/SectionLayout.test.tsx.snap
new file mode 100644
index 00000000..aca2a2cd
--- /dev/null
+++ b/components/__snapshots__/SectionLayout.test.tsx.snap
@@ -0,0 +1,165 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`SectionLayout Component should match snapshot with complex children 1`] = `
+
+
+ Icon
+
+ Section Header
+
+ Line 1
+
+
+ Line 2
+
+
+
+ Nested content
+
+
+
+`;
+
+exports[`SectionLayout Component should match snapshot with custom marginBottom 1`] = `
+
+
+ Icon
+
+ Section Header
+
+ Section Content
+
+
+`;
+
+exports[`SectionLayout Component should match snapshot with default props 1`] = `
+
+
+ Icon
+
+ Section Header
+
+ Section Content
+
+
+`;
+
+exports[`SectionLayout Component should match snapshot with different header text 1`] = `
+
+
+ Icon
+
+ Custom Section Title
+
+ Section Content
+
+
+`;
+
+exports[`SectionLayout Component should match snapshot with different icon 1`] = `
+
+
+ 🔍
+
+ Section Header
+
+ Section Content
+
+
+`;
+
+exports[`SectionLayout Component should match snapshot with zero marginBottom 1`] = `
+
+
+ Icon
+
+ Section Header
+
+ Section Content
+
+
+`;
diff --git a/components/__snapshots__/TextEditOverlay.test.tsx.snap b/components/__snapshots__/TextEditOverlay.test.tsx.snap
new file mode 100644
index 00000000..71c9704c
--- /dev/null
+++ b/components/__snapshots__/TextEditOverlay.test.tsx.snap
@@ -0,0 +1,501 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`TextEditOverlay Component should match snapshot with default props 1`] = `
+
+ Edit Name
+
+
+
+ cancel
+
+
+
+
+ save
+
+
+
+`;
+
+exports[`TextEditOverlay Component should match snapshot with different label 1`] = `
+
+ Edit Email
+
+
+
+ cancel
+
+
+
+
+ save
+
+
+
+`;
+
+exports[`TextEditOverlay Component should match snapshot with empty value 1`] = `
+
+ Edit Name
+
+
+
+ cancel
+
+
+
+
+ save
+
+
+
+`;
+
+exports[`TextEditOverlay Component should match snapshot with long value 1`] = `
+
+ Edit Name
+
+
+
+ cancel
+
+
+
+
+ save
+
+
+
+`;
+
+exports[`TextEditOverlay Component should match snapshot with maxLength 1`] = `
+
+ Edit Name
+
+
+
+ cancel
+
+
+
+
+ save
+
+
+
+`;
diff --git a/components/__snapshots__/TrustModal.test.tsx.snap b/components/__snapshots__/TrustModal.test.tsx.snap
new file mode 100644
index 00000000..fdbf5b88
--- /dev/null
+++ b/components/__snapshots__/TrustModal.test.tsx.snap
@@ -0,0 +1,1287 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`TrustModal Component should match snapshot when not visible 1`] = `null`;
+
+exports[`TrustModal Component should match snapshot with issuer flow 1`] = `
+
+
+
+
+
+
+ Test Issuer
+
+
+
+
+
+ description
+
+
+
+
+ •
+
+
+ Point 1
+
+
+
+
+ •
+
+
+ Point 2
+
+
+
+
+ •
+
+
+ Point 3
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`TrustModal Component should match snapshot with long name 1`] = `
+
+
+
+
+
+
+ Very Long Issuer Name That Should Wrap Properly
+
+
+
+
+
+ verifierDescription
+
+
+
+
+ •
+
+
+ Point 1
+
+
+
+
+ •
+
+
+ Point 2
+
+
+
+
+ •
+
+
+ Point 3
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`TrustModal Component should match snapshot with verifier flow 1`] = `
+
+
+
+
+
+
+ Test Issuer
+
+
+
+
+
+ verifierDescription
+
+
+
+
+ •
+
+
+ Point 1
+
+
+
+
+ •
+
+
+ Point 2
+
+
+
+
+ •
+
+
+ Point 3
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`TrustModal Component should match snapshot without logo 1`] = `
+
+
+
+
+
+ Test Issuer
+
+
+
+
+
+ description
+
+
+
+
+ •
+
+
+ Point 1
+
+
+
+
+ •
+
+
+ Point 2
+
+
+
+
+ •
+
+
+ Point 3
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`TrustModal Component should match snapshot without logo and name 1`] = `
+
+
+
+
+
+
+ description
+
+
+
+
+ •
+
+
+ Point 1
+
+
+
+
+ •
+
+
+ Point 2
+
+
+
+
+ •
+
+
+ Point 3
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`TrustModal Component should match snapshot without name 1`] = `
+
+
+
+
+
+
+
+
+
+ description
+
+
+
+
+ •
+
+
+ Point 1
+
+
+
+
+ •
+
+
+ Point 2
+
+
+
+
+ •
+
+
+ Point 3
+
+
+
+
+
+
+
+
+
+`;
diff --git a/components/__snapshots__/VcItemContainerProfileImage.test.tsx.snap b/components/__snapshots__/VcItemContainerProfileImage.test.tsx.snap
new file mode 100644
index 00000000..7a3d4cb5
--- /dev/null
+++ b/components/__snapshots__/VcItemContainerProfileImage.test.tsx.snap
@@ -0,0 +1,104 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`VcItemContainerProfileImage Component should match snapshot with empty string face 1`] = `
+
+`;
+
+exports[`VcItemContainerProfileImage Component should match snapshot with face image 1`] = `
+
+
+
+`;
+
+exports[`VcItemContainerProfileImage Component should match snapshot with face image and pinned 1`] = `
+
+
+
+
+`;
+
+exports[`VcItemContainerProfileImage Component should match snapshot without face image 1`] = `
+
+`;
+
+exports[`VcItemContainerProfileImage Component should match snapshot without face image and pinned 1`] = `
+
+`;
diff --git a/components/__snapshots__/VerifiedIcon.test.tsx.snap b/components/__snapshots__/VerifiedIcon.test.tsx.snap
new file mode 100644
index 00000000..3b24a759
--- /dev/null
+++ b/components/__snapshots__/VerifiedIcon.test.tsx.snap
@@ -0,0 +1,20 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`VerifiedIcon should match snapshot 1`] = `
+
+
+
+`;
diff --git a/components/ui/Layout.test.tsx b/components/ui/Layout.test.tsx
new file mode 100644
index 00000000..cf238962
--- /dev/null
+++ b/components/ui/Layout.test.tsx
@@ -0,0 +1,120 @@
+import React from 'react';
+import {render} from '@testing-library/react-native';
+import {Column, Row} from './Layout';
+import {Text} from 'react-native';
+
+describe('Layout Components', () => {
+ describe('Column Component', () => {
+ it('should render Column component', () => {
+ const {toJSON} = render(
+
+ Test
+ ,
+ );
+ expect(toJSON()).toBeTruthy();
+ });
+
+ it('should render children in column', () => {
+ const {getByText} = render(
+
+ Child 1
+ Child 2
+ ,
+ );
+
+ expect(getByText('Child 1')).toBeTruthy();
+ expect(getByText('Child 2')).toBeTruthy();
+ });
+
+ it('should render with fill prop', () => {
+ const {getByLabelText} = render(
+
+ Fill Column
+ ,
+ );
+ expect(getByLabelText('fill-column')).toBeTruthy();
+ });
+
+ it('should render with multiple layout props', () => {
+ const {getByLabelText} = render(
+
+ Complex Column
+ ,
+ );
+ expect(getByLabelText('complex-column')).toBeTruthy();
+ });
+ });
+
+ describe('Row Component', () => {
+ it('should render Row component', () => {
+ const {toJSON} = render(
+
+ Test
+
,
+ );
+ expect(toJSON()).toBeTruthy();
+ });
+
+ it('should render children in row', () => {
+ const {getByText} = render(
+
+ Item 1
+ Item 2
+ Item 3
+
,
+ );
+
+ expect(getByText('Item 1')).toBeTruthy();
+ expect(getByText('Item 2')).toBeTruthy();
+ expect(getByText('Item 3')).toBeTruthy();
+ });
+
+ it('should render with fill prop', () => {
+ const {getByLabelText} = render(
+
+ Fill Row
+
,
+ );
+ expect(getByLabelText('fill-row')).toBeTruthy();
+ });
+
+ it('should render with multiple layout props', () => {
+ const {getByLabelText} = render(
+
+ Complex Row
+
,
+ );
+ expect(getByLabelText('complex-row')).toBeTruthy();
+ });
+
+ it('should handle nested layouts', () => {
+ const {getByText} = render(
+
+
+ Nested 1
+
+
+ Nested 2
+
+
,
+ );
+
+ expect(getByText('Nested 1')).toBeTruthy();
+ expect(getByText('Nested 2')).toBeTruthy();
+ });
+ });
+});
diff --git a/components/ui/TextItem.test.tsx b/components/ui/TextItem.test.tsx
new file mode 100644
index 00000000..a4b86ea0
--- /dev/null
+++ b/components/ui/TextItem.test.tsx
@@ -0,0 +1,111 @@
+import React from 'react';
+import {render} from '@testing-library/react-native';
+import {TextItem} from './TextItem';
+
+describe('TextItem', () => {
+ it('should render text without label', () => {
+ const {getByText} = render();
+ expect(getByText('Test Text')).toBeTruthy();
+ });
+
+ it('should render text with label', () => {
+ const {getByText} = render(
+ ,
+ );
+ expect(getByText('Main Text')).toBeTruthy();
+ expect(getByText('Label Text')).toBeTruthy();
+ });
+
+ it('should render only text when label is not provided', () => {
+ const {getByText, queryByText} = render();
+ expect(getByText('Only Text')).toBeTruthy();
+ expect(queryByText('Label Text')).toBeNull();
+ });
+
+ it('should render with testID prop', () => {
+ const {toJSON} = render();
+ expect(toJSON()).toBeTruthy();
+ });
+
+ it('should render with divider prop', () => {
+ const {getByText} = render();
+ expect(getByText('Test')).toBeTruthy();
+ });
+
+ it('should render without divider when not specified', () => {
+ const {getByText} = render();
+ expect(getByText('Test')).toBeTruthy();
+ });
+
+ it('should render with topDivider prop', () => {
+ const {getByText} = render();
+ expect(getByText('Test')).toBeTruthy();
+ });
+
+ it('should render without topDivider when not specified', () => {
+ const {getByText} = render();
+ expect(getByText('Test')).toBeTruthy();
+ });
+
+ it('should render with both dividers', () => {
+ const {getByText} = render(
+ ,
+ );
+ expect(getByText('Test')).toBeTruthy();
+ });
+
+ it('should render with custom margin when provided', () => {
+ const {getByText} = render();
+ expect(getByText('Test')).toBeTruthy();
+ });
+
+ it('should render long text correctly', () => {
+ const longText = 'A'.repeat(200);
+ const {getByText} = render();
+ expect(getByText(longText)).toBeTruthy();
+ });
+
+ it('should handle empty text', () => {
+ const {toJSON} = render();
+ expect(toJSON()).toBeTruthy();
+ });
+
+ it('should handle special characters in text', () => {
+ const specialText = '!@#$%^&*()_+-={}[]|:";\'<>?,./';
+ const {getByText} = render();
+ expect(getByText(specialText)).toBeTruthy();
+ });
+
+ it('should render with all props combined', () => {
+ const {getByText} = render(
+ ,
+ );
+
+ expect(getByText('Complete Test')).toBeTruthy();
+ expect(getByText('All Props')).toBeTruthy();
+ });
+
+ it('should render multiple TextItems', () => {
+ const {getByText} = render(
+ <>
+
+
+
+ >,
+ );
+
+ expect(getByText('First')).toBeTruthy();
+ expect(getByText('Second')).toBeTruthy();
+ expect(getByText('Third')).toBeTruthy();
+ expect(getByText('One')).toBeTruthy();
+ expect(getByText('Two')).toBeTruthy();
+ expect(getByText('Three')).toBeTruthy();
+ });
+});
diff --git a/components/ui/Timestamp.test.tsx b/components/ui/Timestamp.test.tsx
new file mode 100644
index 00000000..79e2abae
--- /dev/null
+++ b/components/ui/Timestamp.test.tsx
@@ -0,0 +1,87 @@
+import React from 'react';
+import {render} from '@testing-library/react-native';
+import {Timestamp} from './Timestamp';
+
+describe('Timestamp', () => {
+ it('should render formatted date and time', () => {
+ // March 15, 2024 at 14:30:00
+ const timestamp = new Date(2024, 2, 15, 14, 30, 0).getTime();
+ const {getByText} = render();
+
+ const formatted = getByText(/15 March 2024/);
+ expect(formatted).toBeTruthy();
+ expect(formatted.props.children).toContain('PM');
+ });
+
+ it('should format AM time correctly', () => {
+ // March 15, 2024 at 09:15:00
+ const timestamp = new Date(2024, 2, 15, 9, 15, 0).getTime();
+ const {getByText} = render();
+
+ const formatted = getByText(/09:15 AM/);
+ expect(formatted).toBeTruthy();
+ });
+
+ it('should format PM time correctly', () => {
+ // June 20, 2024 at 18:45:00
+ const timestamp = new Date(2024, 5, 20, 18, 45, 0).getTime();
+ const {getByText} = render();
+
+ const formatted = getByText(/06:45 PM/);
+ expect(formatted).toBeTruthy();
+ });
+
+ it('should handle midnight correctly', () => {
+ // January 1, 2024 at 00:00:00
+ const timestamp = new Date(2024, 0, 1, 0, 0, 0).getTime();
+ const {getByText} = render(
+ ,
+ );
+
+ const formatted = getByText(/12:00 AM/);
+ expect(formatted).toBeTruthy();
+ });
+
+ it('should handle noon correctly', () => {
+ // December 25, 2023 at 12:00:00
+ const timestamp = new Date(2023, 11, 25, 12, 0, 0).getTime();
+ const {getByText} = render();
+
+ const formatted = getByText(/12:00 PM/);
+ expect(formatted).toBeTruthy();
+ });
+
+ it('should pad single digit minutes with zero', () => {
+ // April 10, 2024 at 15:05:00
+ const timestamp = new Date(2024, 3, 10, 15, 5, 0).getTime();
+ const {getByText} = render();
+
+ const formatted = getByText(/03:05 PM/);
+ expect(formatted).toBeTruthy();
+ });
+
+ it('should render with testID prop', () => {
+ const timestamp = new Date(2024, 0, 1, 0, 0, 0).getTime();
+ const {toJSON} = render();
+
+ expect(toJSON()).toBeTruthy();
+ });
+
+ it('should display different months correctly', () => {
+ const months = [
+ {month: 0, name: 'January'},
+ {month: 1, name: 'February'},
+ {month: 2, name: 'March'},
+ {month: 6, name: 'July'},
+ {month: 11, name: 'December'},
+ ];
+
+ months.forEach(({month, name}) => {
+ const timestamp = new Date(2024, month, 15, 12, 0, 0).getTime();
+ const {getByText} = render(
+ ,
+ );
+ expect(getByText(new RegExp(name))).toBeTruthy();
+ });
+ });
+});
diff --git a/components/ui/ToastItem.test.tsx b/components/ui/ToastItem.test.tsx
new file mode 100644
index 00000000..3c941e5c
--- /dev/null
+++ b/components/ui/ToastItem.test.tsx
@@ -0,0 +1,65 @@
+import React from 'react';
+import {render} from '@testing-library/react-native';
+import {ToastItem} from './ToastItem';
+
+describe('ToastItem', () => {
+ it('should render toast message', () => {
+ const {getByText} = render();
+ expect(getByText('Success!')).toBeTruthy();
+ });
+
+ it('should render long message', () => {
+ const longMessage =
+ 'This is a very long toast message that should still be displayed properly';
+ const {getByText} = render();
+ expect(getByText(longMessage)).toBeTruthy();
+ });
+
+ it('should render short message', () => {
+ const {getByText} = render();
+ expect(getByText('OK')).toBeTruthy();
+ });
+
+ it('should render empty message', () => {
+ const {toJSON} = render();
+ expect(toJSON()).toBeTruthy();
+ });
+
+ it('should render message with special characters', () => {
+ const message = 'Error: Operation failed! (Code: 500)';
+ const {getByText} = render();
+ expect(getByText(message)).toBeTruthy();
+ });
+
+ it('should render message with numbers', () => {
+ const message = '123 items processed successfully';
+ const {getByText} = render();
+ expect(getByText(message)).toBeTruthy();
+ });
+
+ it('should render message with emojis', () => {
+ const message = '✅ Success! 🎉';
+ const {getByText} = render();
+ expect(getByText(message)).toBeTruthy();
+ });
+
+ it('should render multiline message', () => {
+ const message = 'Line 1\nLine 2\nLine 3';
+ const {getByText} = render();
+ expect(getByText(message)).toBeTruthy();
+ });
+
+ it('should render different toast messages', () => {
+ const messages = [
+ 'Operation completed',
+ 'Error occurred',
+ 'Warning: Low storage',
+ 'Info: Update available',
+ ];
+
+ messages.forEach(message => {
+ const {getByText} = render();
+ expect(getByText(message)).toBeTruthy();
+ });
+ });
+});
diff --git a/components/ui/themes/themes.test.ts b/components/ui/themes/themes.test.ts
new file mode 100644
index 00000000..792fa544
--- /dev/null
+++ b/components/ui/themes/themes.test.ts
@@ -0,0 +1,106 @@
+import {DefaultTheme} from './DefaultTheme';
+import {PurpleTheme} from './PurpleTheme';
+
+describe('DefaultTheme', () => {
+ it('should be defined', () => {
+ expect(DefaultTheme).toBeDefined();
+ });
+
+ it('should have Colors property', () => {
+ expect(DefaultTheme.Colors).toBeDefined();
+ expect(typeof DefaultTheme.Colors).toBe('object');
+ });
+
+ it('should have ProfileIconColor', () => {
+ expect(DefaultTheme.Colors.ProfileIconColor).toBeDefined();
+ });
+
+ it('should have TextStyles property', () => {
+ expect(DefaultTheme.TextStyles).toBeDefined();
+ expect(typeof DefaultTheme.TextStyles).toBe('object');
+ });
+
+ it('should have ButtonStyles property', () => {
+ expect(DefaultTheme.ButtonStyles).toBeDefined();
+ expect(typeof DefaultTheme.ButtonStyles).toBe('object');
+ });
+
+ it('should have spacing function', () => {
+ expect(DefaultTheme.spacing).toBeDefined();
+ expect(typeof DefaultTheme.spacing).toBe('function');
+ });
+
+ it('should have elevation function', () => {
+ expect(DefaultTheme.elevation).toBeDefined();
+ expect(typeof DefaultTheme.elevation).toBe('function');
+ });
+});
+
+describe('PurpleTheme', () => {
+ it('should be defined', () => {
+ expect(PurpleTheme).toBeDefined();
+ });
+
+ it('should have Colors property', () => {
+ expect(PurpleTheme.Colors).toBeDefined();
+ expect(typeof PurpleTheme.Colors).toBe('object');
+ });
+
+ it('should have ProfileIconColor', () => {
+ expect(PurpleTheme.Colors.ProfileIconColor).toBeDefined();
+ });
+
+ it('should have different colors than Default', () => {
+ expect(PurpleTheme.Colors).not.toEqual(DefaultTheme.Colors);
+ });
+
+ it('should have TextStyles property', () => {
+ expect(PurpleTheme.TextStyles).toBeDefined();
+ expect(typeof PurpleTheme.TextStyles).toBe('object');
+ });
+
+ it('should have ButtonStyles property', () => {
+ expect(PurpleTheme.ButtonStyles).toBeDefined();
+ expect(typeof PurpleTheme.ButtonStyles).toBe('object');
+ });
+
+ it('should have spacing function', () => {
+ expect(PurpleTheme.spacing).toBeDefined();
+ expect(typeof PurpleTheme.spacing).toBe('function');
+ });
+
+ it('should have elevation function', () => {
+ expect(PurpleTheme.elevation).toBeDefined();
+ expect(typeof PurpleTheme.elevation).toBe('function');
+ });
+
+ it('should have same structure as DefaultTheme', () => {
+ const defaultKeys = Object.keys(DefaultTheme);
+ const purpleKeys = Object.keys(PurpleTheme);
+ expect(purpleKeys.sort()).toEqual(defaultKeys.sort());
+ });
+});
+
+describe('Theme spacing function', () => {
+ it('should return spacing styles for Default theme', () => {
+ const spacing = DefaultTheme.spacing('margin', 'xs');
+ expect(spacing).toBeDefined();
+ });
+
+ it('should return spacing styles for Purple theme', () => {
+ const spacing = PurpleTheme.spacing('padding', 'sm');
+ expect(spacing).toBeDefined();
+ });
+});
+
+describe('Theme elevation function', () => {
+ it('should return elevation styles for Default theme', () => {
+ const elevation = DefaultTheme.elevation(2);
+ expect(elevation).toBeDefined();
+ });
+
+ it('should return elevation styles for Purple theme', () => {
+ const elevation = PurpleTheme.elevation(3);
+ expect(elevation).toBeDefined();
+ });
+});
diff --git a/jest.config.js b/jest.config.js
index 80c10275..84a41346 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
// jest.config.js
const {defaults: tsjPreset} = require('ts-jest/presets');
module.exports = {
@@ -36,8 +37,8 @@ module.exports = {
],
transformIgnorePatterns: [
'node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg|react-native-vector-icons|jsonld|jsonld-signatures|@digitalbazaar/.*)',
- 'node_modules/(?!(@react-native|react-native|react-native-argon2|@react-navigation|react-native-elements|react-native-size-matters|react-native-ratings|expo-constants|base58-universal|@react-native-*|react-native-google-signin|react-native-linear-gradient|expo-camera|base58-universal/*|react-native-*|react-native-vector-icons|jsonld|jsonld-signatures|@digitalbazaar/.*)/).*/',
- ],
+ 'node_modules/(?!(@react-native|react-native|react-native-argon2|@react-navigation|react-native-elements|react-native-size-matters|react-native-ratings|expo-constants|base58-universal|@react-native-*|react-native-google-signin|react-native-linear-gradient|expo-camera|base58-universal/*|react-native-*|react-native-vector-icons|jsonld|jsonld-signatures|@digitalbazaar/.*)/).*/',
+ ],
setupFiles: [
'/__mocks__/svg.mock.js',
'/__mocks__/jest-init.js',
@@ -52,8 +53,8 @@ module.exports = {
'/__mocks__/@noble/mock-secp256k1.js',
'/__mocks__/@noble/mock-ed25519.js',
'/__mocks__/react-native-base64.js',
- '__mocks__/mockCrytoUtil.js',
- '__mocks__/text-encoder.js',
+ '/__mocks__/mockCrytoUtil.js',
+ '/__mocks__/text-encoder.js',
// https://github.com/react-native-google-signin/google-signin?tab=readme-ov-file#jest-module-mock
'/node_modules/@react-native-google-signin/google-signin/jest/build/setup.js',
],
diff --git a/machines/Issuers/IssuersEvents.test.ts b/machines/Issuers/IssuersEvents.test.ts
new file mode 100644
index 00000000..90b920a6
--- /dev/null
+++ b/machines/Issuers/IssuersEvents.test.ts
@@ -0,0 +1,271 @@
+import {IssuersEvents} from './IssuersEvents';
+import {CredentialTypes} from '../VerifiableCredential/VCMetaMachine/vc';
+
+describe('IssuersEvents', () => {
+ describe('SELECTED_ISSUER', () => {
+ it('should create event with id', () => {
+ const result = IssuersEvents.SELECTED_ISSUER('issuer-123');
+ expect(result).toEqual({id: 'issuer-123'});
+ });
+
+ it('should handle empty id', () => {
+ const result = IssuersEvents.SELECTED_ISSUER('');
+ expect(result).toEqual({id: ''});
+ });
+ });
+
+ describe('DOWNLOAD_ID', () => {
+ it('should create empty event', () => {
+ const result = IssuersEvents.DOWNLOAD_ID();
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('BIOMETRIC_CANCELLED', () => {
+ it('should create event with requester', () => {
+ const result = IssuersEvents.BIOMETRIC_CANCELLED('login');
+ expect(result).toEqual({requester: 'login'});
+ });
+
+ it('should create event without requester', () => {
+ const result = IssuersEvents.BIOMETRIC_CANCELLED();
+ expect(result).toEqual({requester: undefined});
+ });
+ });
+
+ describe('COMPLETED', () => {
+ it('should create empty event', () => {
+ const result = IssuersEvents.COMPLETED();
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('TRY_AGAIN', () => {
+ it('should create empty event', () => {
+ const result = IssuersEvents.TRY_AGAIN();
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('RESET_ERROR', () => {
+ it('should create empty event', () => {
+ const result = IssuersEvents.RESET_ERROR();
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('CHECK_KEY_PAIR', () => {
+ it('should create empty event', () => {
+ const result = IssuersEvents.CHECK_KEY_PAIR();
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('CANCEL', () => {
+ it('should create empty event', () => {
+ const result = IssuersEvents.CANCEL();
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('STORE_RESPONSE', () => {
+ it('should create event with response', () => {
+ const response = {data: 'test'};
+ const result = IssuersEvents.STORE_RESPONSE(response);
+ expect(result).toEqual({response: {data: 'test'}});
+ });
+
+ it('should create event without response', () => {
+ const result = IssuersEvents.STORE_RESPONSE();
+ expect(result).toEqual({response: undefined});
+ });
+ });
+
+ describe('STORE_ERROR', () => {
+ it('should create event with error and requester', () => {
+ const error = new Error('Test error');
+ const result = IssuersEvents.STORE_ERROR(error, 'test-requester');
+ expect(result.error).toBe(error);
+ expect(result.requester).toBe('test-requester');
+ });
+
+ it('should create event with error only', () => {
+ const error = new Error('Test error');
+ const result = IssuersEvents.STORE_ERROR(error);
+ expect(result.error).toBe(error);
+ expect(result.requester).toBeUndefined();
+ });
+ });
+
+ describe('RESET_VERIFY_ERROR', () => {
+ it('should create empty event', () => {
+ const result = IssuersEvents.RESET_VERIFY_ERROR();
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('SELECTED_CREDENTIAL_TYPE', () => {
+ it('should create event with credential type', () => {
+ const credType = {} as unknown as CredentialTypes;
+ const result = IssuersEvents.SELECTED_CREDENTIAL_TYPE(credType);
+ expect(result).toEqual({credType});
+ });
+ });
+
+ describe('SCAN_CREDENTIAL_OFFER_QR_CODE', () => {
+ it('should create empty event', () => {
+ const result = IssuersEvents.SCAN_CREDENTIAL_OFFER_QR_CODE();
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('QR_CODE_SCANNED', () => {
+ it('should create event with data', () => {
+ const result = IssuersEvents.QR_CODE_SCANNED('qr-code-data');
+ expect(result).toEqual({data: 'qr-code-data'});
+ });
+
+ it('should handle JSON data', () => {
+ const jsonData = '{"key": "value"}';
+ const result = IssuersEvents.QR_CODE_SCANNED(jsonData);
+ expect(result).toEqual({data: jsonData});
+ });
+ });
+
+ describe('AUTH_ENDPOINT_RECEIVED', () => {
+ it('should create event with auth endpoint', () => {
+ const result = IssuersEvents.AUTH_ENDPOINT_RECEIVED(
+ 'https://auth.example.com',
+ );
+ expect(result).toEqual({authEndpoint: 'https://auth.example.com'});
+ });
+ });
+
+ describe('PROOF_REQUEST', () => {
+ it('should create event with all parameters', () => {
+ const accessToken = 'token-123';
+ const cNonce = 'nonce-456';
+ const issuerMetadata = {name: 'Test Issuer'};
+ const issuer = {id: 'issuer-1'} as unknown as any;
+ const credentialtypes = {} as unknown as CredentialTypes;
+
+ const result = IssuersEvents.PROOF_REQUEST(
+ accessToken,
+ cNonce,
+ issuerMetadata,
+ issuer,
+ credentialtypes,
+ );
+
+ expect(result.accessToken).toBe('token-123');
+ expect(result.cNonce).toBe('nonce-456');
+ expect(result.issuerMetadata).toEqual({name: 'Test Issuer'});
+ expect(result.issuer).toEqual({id: 'issuer-1'});
+ expect(result.credentialtypes).toBeDefined();
+ });
+
+ it('should handle undefined cNonce', () => {
+ const result = IssuersEvents.PROOF_REQUEST(
+ 'token',
+ undefined,
+ {},
+ {} as unknown as any,
+ {} as unknown as CredentialTypes,
+ );
+
+ expect(result.cNonce).toBeUndefined();
+ });
+ });
+
+ describe('TX_CODE_REQUEST', () => {
+ it('should create empty event', () => {
+ const result = IssuersEvents.TX_CODE_REQUEST();
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('TX_CODE_RECEIVED', () => {
+ it('should create event with txCode', () => {
+ const result = IssuersEvents.TX_CODE_RECEIVED('TX123456');
+ expect(result).toEqual({txCode: 'TX123456'});
+ });
+
+ it('should handle empty txCode', () => {
+ const result = IssuersEvents.TX_CODE_RECEIVED('');
+ expect(result).toEqual({txCode: ''});
+ });
+ });
+
+ describe('ON_CONSENT_GIVEN', () => {
+ it('should create empty event', () => {
+ const result = IssuersEvents.ON_CONSENT_GIVEN();
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('TRUST_ISSUER_CONSENT_REQUEST', () => {
+ it('should create event with issuerMetadata', () => {
+ const issuerMetadata = {
+ name: 'Trusted Issuer',
+ url: 'https://issuer.example.com',
+ };
+ const result = IssuersEvents.TRUST_ISSUER_CONSENT_REQUEST(issuerMetadata);
+ expect(result).toEqual({issuerMetadata});
+ });
+
+ it('should handle empty issuerMetadata', () => {
+ const result = IssuersEvents.TRUST_ISSUER_CONSENT_REQUEST({});
+ expect(result).toEqual({issuerMetadata: {}});
+ });
+ });
+
+ describe('TOKEN_REQUEST', () => {
+ it('should create event with tokenRequest', () => {
+ const tokenRequest = {
+ grant_type: 'authorization_code',
+ code: 'auth-code-123',
+ };
+ const result = IssuersEvents.TOKEN_REQUEST(tokenRequest);
+ expect(result).toEqual({tokenRequest});
+ });
+
+ it('should handle empty tokenRequest', () => {
+ const result = IssuersEvents.TOKEN_REQUEST({});
+ expect(result).toEqual({tokenRequest: {}});
+ });
+ });
+
+ describe('IssuersEvents object structure', () => {
+ it('should have all expected event creators', () => {
+ expect(IssuersEvents.SELECTED_ISSUER).toBeDefined();
+ expect(IssuersEvents.DOWNLOAD_ID).toBeDefined();
+ expect(IssuersEvents.BIOMETRIC_CANCELLED).toBeDefined();
+ expect(IssuersEvents.COMPLETED).toBeDefined();
+ expect(IssuersEvents.TRY_AGAIN).toBeDefined();
+ expect(IssuersEvents.RESET_ERROR).toBeDefined();
+ expect(IssuersEvents.CHECK_KEY_PAIR).toBeDefined();
+ expect(IssuersEvents.CANCEL).toBeDefined();
+ expect(IssuersEvents.STORE_RESPONSE).toBeDefined();
+ expect(IssuersEvents.STORE_ERROR).toBeDefined();
+ expect(IssuersEvents.RESET_VERIFY_ERROR).toBeDefined();
+ expect(IssuersEvents.SELECTED_CREDENTIAL_TYPE).toBeDefined();
+ expect(IssuersEvents.SCAN_CREDENTIAL_OFFER_QR_CODE).toBeDefined();
+ expect(IssuersEvents.QR_CODE_SCANNED).toBeDefined();
+ expect(IssuersEvents.AUTH_ENDPOINT_RECEIVED).toBeDefined();
+ expect(IssuersEvents.PROOF_REQUEST).toBeDefined();
+ expect(IssuersEvents.TX_CODE_REQUEST).toBeDefined();
+ expect(IssuersEvents.TX_CODE_RECEIVED).toBeDefined();
+ expect(IssuersEvents.ON_CONSENT_GIVEN).toBeDefined();
+ expect(IssuersEvents.TRUST_ISSUER_CONSENT_REQUEST).toBeDefined();
+ expect(IssuersEvents.TOKEN_REQUEST).toBeDefined();
+ });
+
+ it('should have all event creators be functions', () => {
+ expect(typeof IssuersEvents.SELECTED_ISSUER).toBe('function');
+ expect(typeof IssuersEvents.DOWNLOAD_ID).toBe('function');
+ expect(typeof IssuersEvents.BIOMETRIC_CANCELLED).toBe('function');
+ expect(typeof IssuersEvents.COMPLETED).toBe('function');
+ expect(typeof IssuersEvents.TRY_AGAIN).toBe('function');
+ });
+ });
+});
diff --git a/machines/Issuers/IssuersModel.test.ts b/machines/Issuers/IssuersModel.test.ts
new file mode 100644
index 00000000..a2e6cb3e
--- /dev/null
+++ b/machines/Issuers/IssuersModel.test.ts
@@ -0,0 +1,301 @@
+import {IssuersModel} from './IssuersModel';
+
+describe('IssuersModel', () => {
+ describe('Model structure', () => {
+ it('should be defined', () => {
+ expect(IssuersModel).toBeDefined();
+ });
+
+ it('should have initialContext', () => {
+ expect(IssuersModel.initialContext).toBeDefined();
+ });
+
+ it('should have events', () => {
+ expect(IssuersModel.events).toBeDefined();
+ });
+ });
+
+ describe('Initial Context', () => {
+ const initialContext = IssuersModel.initialContext;
+
+ it('should have issuers as empty array', () => {
+ expect(initialContext.issuers).toEqual([]);
+ expect(Array.isArray(initialContext.issuers)).toBe(true);
+ });
+
+ it('should have selectedIssuerId as empty string', () => {
+ expect(initialContext.selectedIssuerId).toBe('');
+ });
+
+ it('should have qrData as empty string', () => {
+ expect(initialContext.qrData).toBe('');
+ });
+
+ it('should have selectedIssuer as empty object', () => {
+ expect(initialContext.selectedIssuer).toEqual({});
+ });
+
+ it('should have selectedIssuerWellknownResponse as empty object', () => {
+ expect(initialContext.selectedIssuerWellknownResponse).toEqual({});
+ });
+
+ it('should have tokenResponse as empty object', () => {
+ expect(initialContext.tokenResponse).toEqual({});
+ });
+
+ it('should have errorMessage as empty string', () => {
+ expect(initialContext.errorMessage).toBe('');
+ });
+
+ it('should have loadingReason as displayIssuers', () => {
+ expect(initialContext.loadingReason).toBe('displayIssuers');
+ });
+
+ it('should have verifiableCredential as null', () => {
+ expect(initialContext.verifiableCredential).toBeNull();
+ });
+
+ it('should have selectedCredentialType as empty object', () => {
+ expect(initialContext.selectedCredentialType).toEqual({});
+ });
+
+ it('should have supportedCredentialTypes as empty array', () => {
+ expect(initialContext.supportedCredentialTypes).toEqual([]);
+ expect(Array.isArray(initialContext.supportedCredentialTypes)).toBe(true);
+ });
+
+ it('should have credentialWrapper as empty object', () => {
+ expect(initialContext.credentialWrapper).toEqual({});
+ });
+
+ it('should have serviceRefs as empty object', () => {
+ expect(initialContext.serviceRefs).toEqual({});
+ });
+
+ it('should have verificationErrorMessage as empty string', () => {
+ expect(initialContext.verificationErrorMessage).toBe('');
+ });
+
+ it('should have publicKey as empty string', () => {
+ expect(initialContext.publicKey).toBe('');
+ });
+
+ it('should have privateKey as empty string', () => {
+ expect(initialContext.privateKey).toBe('');
+ });
+
+ it('should have vcMetadata as empty object', () => {
+ expect(initialContext.vcMetadata).toEqual({});
+ });
+
+ it('should have keyType as RS256', () => {
+ expect(initialContext.keyType).toBe('RS256');
+ });
+
+ it('should have wellknownKeyTypes as empty array', () => {
+ expect(initialContext.wellknownKeyTypes).toEqual([]);
+ expect(Array.isArray(initialContext.wellknownKeyTypes)).toBe(true);
+ });
+
+ it('should have authEndpointToOpen as false', () => {
+ expect(initialContext.authEndpointToOpen).toBe(false);
+ });
+
+ it('should have isTransactionCodeRequested as false', () => {
+ expect(initialContext.isTransactionCodeRequested).toBe(false);
+ });
+
+ it('should have authEndpoint as empty string', () => {
+ expect(initialContext.authEndpoint).toBe('');
+ });
+
+ it('should have accessToken as empty string', () => {
+ expect(initialContext.accessToken).toBe('');
+ });
+
+ it('should have txCode as empty string', () => {
+ expect(initialContext.txCode).toBe('');
+ });
+
+ it('should have cNonce as empty string', () => {
+ expect(initialContext.cNonce).toBe('');
+ });
+
+ it('should have isConsentRequested as false', () => {
+ expect(initialContext.isConsentRequested).toBe(false);
+ });
+
+ it('should have issuerLogo as empty string', () => {
+ expect(initialContext.issuerLogo).toBe('');
+ });
+
+ it('should have issuerName as empty string', () => {
+ expect(initialContext.issuerName).toBe('');
+ });
+
+ it('should have txCodeInputMode as empty string', () => {
+ expect(initialContext.txCodeInputMode).toBe('');
+ });
+
+ it('should have txCodeDescription as empty string', () => {
+ expect(initialContext.txCodeDescription).toBe('');
+ });
+
+ it('should have txCodeLength as null', () => {
+ expect(initialContext.txCodeLength).toBeNull();
+ });
+
+ it('should have isCredentialOfferFlow as false', () => {
+ expect(initialContext.isCredentialOfferFlow).toBe(false);
+ });
+
+ it('should have tokenRequestObject as empty object', () => {
+ expect(initialContext.tokenRequestObject).toEqual({});
+ });
+
+ it('should have credentialConfigurationId as empty string', () => {
+ expect(initialContext.credentialConfigurationId).toBe('');
+ });
+
+ it('should have all 35 required properties', () => {
+ const properties = Object.keys(initialContext);
+ expect(properties).toHaveLength(35);
+ });
+ });
+
+ describe('String properties', () => {
+ const context = IssuersModel.initialContext;
+
+ it('all empty string properties should be empty', () => {
+ const emptyStrings = [
+ context.selectedIssuerId,
+ context.qrData,
+ context.errorMessage,
+ context.verificationErrorMessage,
+ context.publicKey,
+ context.privateKey,
+ context.authEndpoint,
+ context.accessToken,
+ context.txCode,
+ context.cNonce,
+ context.issuerLogo,
+ context.issuerName,
+ context.txCodeInputMode,
+ context.txCodeDescription,
+ context.credentialConfigurationId,
+ ];
+
+ emptyStrings.forEach(str => {
+ expect(str).toBe('');
+ expect(typeof str).toBe('string');
+ });
+ });
+
+ it('loadingReason should be displayIssuers', () => {
+ expect(context.loadingReason).toBe('displayIssuers');
+ expect(typeof context.loadingReason).toBe('string');
+ });
+
+ it('keyType should be RS256', () => {
+ expect(context.keyType).toBe('RS256');
+ expect(typeof context.keyType).toBe('string');
+ });
+ });
+
+ describe('Array properties', () => {
+ const context = IssuersModel.initialContext;
+
+ it('all array properties should be empty arrays', () => {
+ const arrays = [
+ context.issuers,
+ context.supportedCredentialTypes,
+ context.wellknownKeyTypes,
+ ];
+
+ arrays.forEach(arr => {
+ expect(Array.isArray(arr)).toBe(true);
+ expect(arr).toHaveLength(0);
+ });
+ });
+ });
+
+ describe('Object properties', () => {
+ const context = IssuersModel.initialContext;
+
+ it('all object properties should be empty objects or as specified', () => {
+ const emptyObjects = [
+ context.selectedIssuer,
+ context.selectedIssuerWellknownResponse,
+ context.tokenResponse,
+ context.selectedCredentialType,
+ context.credentialWrapper,
+ context.serviceRefs,
+ context.vcMetadata,
+ context.tokenRequestObject,
+ ];
+
+ emptyObjects.forEach(obj => {
+ expect(typeof obj).toBe('object');
+ expect(Object.keys(obj)).toHaveLength(0);
+ });
+ });
+ });
+
+ describe('Boolean properties', () => {
+ const context = IssuersModel.initialContext;
+
+ it('all boolean properties should be false initially', () => {
+ const booleans = [
+ context.authEndpointToOpen,
+ context.isTransactionCodeRequested,
+ context.isConsentRequested,
+ context.isCredentialOfferFlow,
+ ];
+
+ booleans.forEach(bool => {
+ expect(bool).toBe(false);
+ expect(typeof bool).toBe('boolean');
+ });
+ });
+ });
+
+ describe('Null properties', () => {
+ const context = IssuersModel.initialContext;
+
+ it('verifiableCredential should be null', () => {
+ expect(context.verifiableCredential).toBeNull();
+ });
+
+ it('txCodeLength should be null', () => {
+ expect(context.txCodeLength).toBeNull();
+ });
+ });
+
+ describe('Model events', () => {
+ it('should have events object', () => {
+ expect(IssuersModel.events).toBeDefined();
+ expect(typeof IssuersModel.events).toBe('object');
+ });
+
+ it('should have event creators', () => {
+ const eventKeys = Object.keys(IssuersModel.events);
+ expect(eventKeys.length).toBeGreaterThan(0);
+ });
+ });
+
+ describe('Property types', () => {
+ const context = IssuersModel.initialContext;
+
+ it('should have correct types for all properties', () => {
+ expect(typeof context.selectedIssuerId).toBe('string');
+ expect(typeof context.qrData).toBe('string');
+ expect(typeof context.errorMessage).toBe('string');
+ expect(typeof context.loadingReason).toBe('string');
+ expect(typeof context.keyType).toBe('string');
+ expect(typeof context.authEndpointToOpen).toBe('boolean');
+ expect(typeof context.isTransactionCodeRequested).toBe('boolean');
+ expect(Array.isArray(context.issuers)).toBe(true);
+ expect(typeof context.selectedIssuer).toBe('object');
+ });
+ });
+});
diff --git a/machines/Issuers/IssuersSelectors.test.ts b/machines/Issuers/IssuersSelectors.test.ts
new file mode 100644
index 00000000..c86a0b03
--- /dev/null
+++ b/machines/Issuers/IssuersSelectors.test.ts
@@ -0,0 +1,372 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import {
+ selectIssuers,
+ selectSelectedIssuer,
+ selectAuthWebViewStatus,
+ selectAuthEndPoint,
+ selectErrorMessageType,
+ selectLoadingReason,
+ selectIsDownloadCredentials,
+ selectIsTxCodeRequested,
+ selectIsConsentRequested,
+ selectIssuerLogo,
+ selectIssuerName,
+ selectTxCodeDisplayDetails,
+ selectIsNonGenericError,
+ selectIsDone,
+ selectIsIdle,
+ selectStoring,
+ selectIsError,
+ selectVerificationErrorMessage,
+ selectSelectingCredentialType,
+ selectSupportedCredentialTypes,
+ selectIsQrScanning,
+} from './IssuersSelectors';
+
+describe('IssuersSelectors', () => {
+ const mockState: any = {
+ context: {
+ issuers: [{issuer_id: '1'}, {issuer_id: '2'}],
+ selectedIssuer: {issuer_id: '1', credential_issuer: 'test.example.com'},
+ authEndpointToOpen: 'https://auth.example.com',
+ authEndpoint: 'https://auth.example.com/authorize',
+ errorMessage: 'Test error',
+ loadingReason: 'Loading data',
+ isTransactionCodeRequested: true,
+ isConsentRequested: false,
+ issuerLogo: 'https://example.com/logo.png',
+ issuerName: 'Test Issuer',
+ txCodeInputMode: 'numeric',
+ txCodeDescription: 'Enter transaction code',
+ txCodeLength: 6,
+ },
+ matches: jest.fn(
+ (stateName: string) => stateName === 'downloadCredentials',
+ ),
+ };
+
+ describe('selectIssuers', () => {
+ it('should return issuers from context', () => {
+ const result = selectIssuers(mockState);
+ expect(result).toEqual(mockState.context.issuers);
+ });
+
+ it('should return array of issuers', () => {
+ const result = selectIssuers(mockState);
+ expect(Array.isArray(result)).toBe(true);
+ expect(result.length).toBe(2);
+ });
+ });
+
+ describe('selectSelectedIssuer', () => {
+ it('should return selected issuer from context', () => {
+ const result = selectSelectedIssuer(mockState);
+ expect(result).toEqual(mockState.context.selectedIssuer);
+ });
+
+ it('should return issuer with issuer_id property', () => {
+ const result = selectSelectedIssuer(mockState);
+ expect(result.issuer_id).toBe('1');
+ expect(result.credential_issuer).toBe('test.example.com');
+ });
+ });
+
+ describe('selectAuthWebViewStatus', () => {
+ it('should return auth endpoint to open', () => {
+ const result = selectAuthWebViewStatus(mockState);
+ expect(result).toBe('https://auth.example.com');
+ });
+ });
+
+ describe('selectAuthEndPoint', () => {
+ it('should return auth endpoint', () => {
+ const result = selectAuthEndPoint(mockState);
+ expect(result).toBe('https://auth.example.com/authorize');
+ });
+ });
+
+ describe('selectErrorMessageType', () => {
+ it('should return error message from context', () => {
+ const result = selectErrorMessageType(mockState);
+ expect(result).toBe('Test error');
+ });
+ });
+
+ describe('selectLoadingReason', () => {
+ it('should return loading reason from context', () => {
+ const result = selectLoadingReason(mockState);
+ expect(result).toBe('Loading data');
+ });
+ });
+
+ describe('selectIsDownloadCredentials', () => {
+ it('should return true when in downloadCredentials state', () => {
+ const result = selectIsDownloadCredentials(mockState);
+ expect(result).toBe(true);
+ });
+
+ it('should call matches with correct state name', () => {
+ selectIsDownloadCredentials(mockState);
+ expect(mockState.matches).toHaveBeenCalledWith('downloadCredentials');
+ });
+ });
+
+ describe('selectIsTxCodeRequested', () => {
+ it('should return transaction code requested status', () => {
+ const result = selectIsTxCodeRequested(mockState);
+ expect(result).toBe(true);
+ });
+
+ it('should return false when not requested', () => {
+ const stateWithFalse: any = {
+ ...mockState,
+ context: {...mockState.context, isTransactionCodeRequested: false},
+ };
+ const result = selectIsTxCodeRequested(stateWithFalse);
+ expect(result).toBe(false);
+ });
+ });
+
+ describe('selectIsConsentRequested', () => {
+ it('should return consent requested status', () => {
+ const result = selectIsConsentRequested(mockState);
+ expect(result).toBe(false);
+ });
+
+ it('should return true when consent requested', () => {
+ const stateWithConsent: any = {
+ ...mockState,
+ context: {...mockState.context, isConsentRequested: true},
+ };
+ const result = selectIsConsentRequested(stateWithConsent);
+ expect(result).toBe(true);
+ });
+ });
+
+ describe('selectIssuerLogo', () => {
+ it('should return issuer logo URL', () => {
+ const result = selectIssuerLogo(mockState);
+ expect(result).toBe('https://example.com/logo.png');
+ });
+ });
+
+ describe('selectIssuerName', () => {
+ it('should return issuer name', () => {
+ const result = selectIssuerName(mockState);
+ expect(result).toBe('Test Issuer');
+ });
+ });
+
+ describe('selectTxCodeDisplayDetails', () => {
+ it('should return transaction code display details', () => {
+ const result = selectTxCodeDisplayDetails(mockState);
+ expect(result).toEqual({
+ inputMode: 'numeric',
+ description: 'Enter transaction code',
+ length: 6,
+ });
+ });
+
+ it('should have all required properties', () => {
+ const result = selectTxCodeDisplayDetails(mockState);
+ expect(result).toHaveProperty('inputMode');
+ expect(result).toHaveProperty('description');
+ expect(result).toHaveProperty('length');
+ });
+
+ it('should return correct input mode', () => {
+ const result = selectTxCodeDisplayDetails(mockState);
+ expect(result.inputMode).toBe('numeric');
+ });
+
+ it('should return correct description', () => {
+ const result = selectTxCodeDisplayDetails(mockState);
+ expect(result.description).toBe('Enter transaction code');
+ });
+
+ it('should return correct length', () => {
+ const result = selectTxCodeDisplayDetails(mockState);
+ expect(result.length).toBe(6);
+ });
+ });
+
+ describe('Selectors with empty/null values', () => {
+ const emptyState: any = {
+ context: {
+ issuers: [],
+ selectedIssuer: null,
+ authEndpointToOpen: '',
+ authEndpoint: '',
+ errorMessage: '',
+ loadingReason: '',
+ isTransactionCodeRequested: false,
+ isConsentRequested: false,
+ issuerLogo: '',
+ issuerName: '',
+ txCodeInputMode: '',
+ txCodeDescription: '',
+ txCodeLength: 0,
+ },
+ matches: jest.fn(() => false),
+ };
+
+ it('should handle empty issuers array', () => {
+ const result = selectIssuers(emptyState);
+ expect(result).toEqual([]);
+ });
+
+ it('should handle null selected issuer', () => {
+ const result = selectSelectedIssuer(emptyState);
+ expect(result).toBeNull();
+ });
+
+ it('should handle empty strings', () => {
+ expect(selectAuthWebViewStatus(emptyState)).toBe('');
+ expect(selectAuthEndPoint(emptyState)).toBe('');
+ expect(selectErrorMessageType(emptyState)).toBe('');
+ expect(selectLoadingReason(emptyState)).toBe('');
+ expect(selectIssuerLogo(emptyState)).toBe('');
+ expect(selectIssuerName(emptyState)).toBe('');
+ });
+
+ it('should handle false boolean values', () => {
+ expect(selectIsTxCodeRequested(emptyState)).toBe(false);
+ expect(selectIsConsentRequested(emptyState)).toBe(false);
+ expect(selectIsDownloadCredentials(emptyState)).toBe(false);
+ });
+
+ it('should handle zero length', () => {
+ const result = selectTxCodeDisplayDetails(emptyState);
+ expect(result.length).toBe(0);
+ });
+ });
+
+ describe('selectIsNonGenericError', () => {
+ it('should return true when error message is not generic and not empty', () => {
+ const stateWithError: any = {
+ context: {errorMessage: 'NETWORK_ERROR'},
+ };
+ const result = selectIsNonGenericError(stateWithError);
+ expect(result).toBe(true);
+ });
+
+ it('should return false when error message is GENERIC', () => {
+ const stateWithGenericError: any = {
+ context: {errorMessage: 'generic'},
+ };
+ const result = selectIsNonGenericError(stateWithGenericError);
+ expect(result).toBe(false);
+ });
+
+ it('should return false when error message is empty', () => {
+ const stateWithNoError: any = {
+ context: {errorMessage: ''},
+ };
+ const result = selectIsNonGenericError(stateWithNoError);
+ expect(result).toBe(false);
+ });
+ });
+
+ describe('selectIsDone', () => {
+ it('should return true when state matches done', () => {
+ const doneState: any = {
+ matches: jest.fn((name: string) => name === 'done'),
+ };
+ const result = selectIsDone(doneState);
+ expect(result).toBe(true);
+ expect(doneState.matches).toHaveBeenCalledWith('done');
+ });
+
+ it('should return false when state does not match done', () => {
+ const notDoneState: any = {
+ matches: jest.fn(() => false),
+ };
+ const result = selectIsDone(notDoneState);
+ expect(result).toBe(false);
+ });
+ });
+
+ describe('selectIsIdle', () => {
+ it('should return true when state matches idle', () => {
+ const idleState: any = {
+ matches: jest.fn((name: string) => name === 'idle'),
+ };
+ const result = selectIsIdle(idleState);
+ expect(result).toBe(true);
+ expect(idleState.matches).toHaveBeenCalledWith('idle');
+ });
+ });
+
+ describe('selectStoring', () => {
+ it('should return true when state matches storing', () => {
+ const storingState: any = {
+ matches: jest.fn((name: string) => name === 'storing'),
+ };
+ const result = selectStoring(storingState);
+ expect(result).toBe(true);
+ expect(storingState.matches).toHaveBeenCalledWith('storing');
+ });
+ });
+
+ describe('selectIsError', () => {
+ it('should return true when state matches error', () => {
+ const errorState: any = {
+ matches: jest.fn((name: string) => name === 'error'),
+ };
+ const result = selectIsError(errorState);
+ expect(result).toBe(true);
+ expect(errorState.matches).toHaveBeenCalledWith('error');
+ });
+ });
+
+ describe('selectVerificationErrorMessage', () => {
+ it('should return verification error message', () => {
+ const stateWithVerificationError: any = {
+ context: {verificationErrorMessage: 'Signature verification failed'},
+ };
+ const result = selectVerificationErrorMessage(stateWithVerificationError);
+ expect(result).toBe('Signature verification failed');
+ });
+ });
+
+ describe('selectSelectingCredentialType', () => {
+ it('should return true when selecting credential type', () => {
+ const selectingState: any = {
+ matches: jest.fn((name: string) => name === 'selectingCredentialType'),
+ };
+ const result = selectSelectingCredentialType(selectingState);
+ expect(result).toBe(true);
+ expect(selectingState.matches).toHaveBeenCalledWith(
+ 'selectingCredentialType',
+ );
+ });
+ });
+
+ describe('selectSupportedCredentialTypes', () => {
+ it('should return supported credential types', () => {
+ const stateWithCredTypes: any = {
+ context: {
+ supportedCredentialTypes: [
+ {id: 'type1', name: 'Type 1'},
+ {id: 'type2', name: 'Type 2'},
+ ],
+ },
+ };
+ const result = selectSupportedCredentialTypes(stateWithCredTypes);
+ expect(result).toHaveLength(2);
+ expect(result[0].id).toBe('type1');
+ expect(result[1].id).toBe('type2');
+ });
+ });
+
+ describe('selectIsQrScanning', () => {
+ it('should return true when waiting for QR scan', () => {
+ const scanningState: any = {
+ matches: jest.fn((name: string) => name === 'waitingForQrScan'),
+ };
+ const result = selectIsQrScanning(scanningState);
+ expect(result).toBe(true);
+ expect(scanningState.matches).toHaveBeenCalledWith('waitingForQrScan');
+ });
+ });
+});
diff --git a/machines/QrLogin/QrLoginModel.test.ts b/machines/QrLogin/QrLoginModel.test.ts
new file mode 100644
index 00000000..5c186c3f
--- /dev/null
+++ b/machines/QrLogin/QrLoginModel.test.ts
@@ -0,0 +1,332 @@
+import {QrLoginmodel} from './QrLoginModel';
+import {VCShareFlowType} from '../../shared/Utils';
+
+describe('QrLoginModel', () => {
+ describe('Model structure', () => {
+ it('should be defined', () => {
+ expect(QrLoginmodel).toBeDefined();
+ });
+
+ it('should have initialContext', () => {
+ expect(QrLoginmodel.initialContext).toBeDefined();
+ });
+
+ it('should have events', () => {
+ expect(QrLoginmodel.events).toBeDefined();
+ });
+ });
+
+ describe('Initial Context', () => {
+ const initialContext = QrLoginmodel.initialContext;
+
+ it('should have serviceRefs as empty object', () => {
+ expect(initialContext.serviceRefs).toEqual({});
+ expect(typeof initialContext.serviceRefs).toBe('object');
+ });
+
+ it('should have selectedVc as empty object', () => {
+ expect(initialContext.selectedVc).toEqual({});
+ expect(typeof initialContext.selectedVc).toBe('object');
+ });
+
+ it('should have linkCode as empty string', () => {
+ expect(initialContext.linkCode).toBe('');
+ expect(typeof initialContext.linkCode).toBe('string');
+ });
+
+ it('should have flowType as SIMPLE_SHARE', () => {
+ expect(initialContext.flowType).toBe(VCShareFlowType.SIMPLE_SHARE);
+ });
+
+ it('should have myVcs as empty array', () => {
+ expect(initialContext.myVcs).toEqual([]);
+ expect(Array.isArray(initialContext.myVcs)).toBe(true);
+ expect(initialContext.myVcs).toHaveLength(0);
+ });
+
+ it('should have thumbprint as empty string', () => {
+ expect(initialContext.thumbprint).toBe('');
+ expect(typeof initialContext.thumbprint).toBe('string');
+ });
+
+ it('should have linkTransactionResponse as empty object', () => {
+ expect(initialContext.linkTransactionResponse).toEqual({});
+ expect(typeof initialContext.linkTransactionResponse).toBe('object');
+ });
+
+ it('should have authFactors as empty array', () => {
+ expect(initialContext.authFactors).toEqual([]);
+ expect(Array.isArray(initialContext.authFactors)).toBe(true);
+ });
+
+ it('should have authorizeScopes as null', () => {
+ expect(initialContext.authorizeScopes).toBeNull();
+ });
+
+ it('should have clientName as empty object', () => {
+ expect(initialContext.clientName).toEqual({});
+ expect(typeof initialContext.clientName).toBe('object');
+ });
+
+ it('should have configs as empty object', () => {
+ expect(initialContext.configs).toEqual({});
+ expect(typeof initialContext.configs).toBe('object');
+ });
+
+ it('should have essentialClaims as empty array', () => {
+ expect(initialContext.essentialClaims).toEqual([]);
+ expect(Array.isArray(initialContext.essentialClaims)).toBe(true);
+ });
+
+ it('should have linkTransactionId as empty string', () => {
+ expect(initialContext.linkTransactionId).toBe('');
+ expect(typeof initialContext.linkTransactionId).toBe('string');
+ });
+
+ it('should have logoUrl as empty string', () => {
+ expect(initialContext.logoUrl).toBe('');
+ expect(typeof initialContext.logoUrl).toBe('string');
+ });
+
+ it('should have voluntaryClaims as empty array', () => {
+ expect(initialContext.voluntaryClaims).toEqual([]);
+ expect(Array.isArray(initialContext.voluntaryClaims)).toBe(true);
+ });
+
+ it('should have selectedVoluntaryClaims as empty array', () => {
+ expect(initialContext.selectedVoluntaryClaims).toEqual([]);
+ expect(Array.isArray(initialContext.selectedVoluntaryClaims)).toBe(true);
+ });
+
+ it('should have errorMessage as empty string', () => {
+ expect(initialContext.errorMessage).toBe('');
+ expect(typeof initialContext.errorMessage).toBe('string');
+ });
+
+ it('should have domainName as empty string', () => {
+ expect(initialContext.domainName).toBe('');
+ expect(typeof initialContext.domainName).toBe('string');
+ });
+
+ it('should have consentClaims with name and picture', () => {
+ expect(initialContext.consentClaims).toEqual(['name', 'picture']);
+ expect(Array.isArray(initialContext.consentClaims)).toBe(true);
+ expect(initialContext.consentClaims).toHaveLength(2);
+ expect(initialContext.consentClaims).toContain('name');
+ expect(initialContext.consentClaims).toContain('picture');
+ });
+
+ it('should have isSharing as empty object', () => {
+ expect(initialContext.isSharing).toEqual({});
+ expect(typeof initialContext.isSharing).toBe('object');
+ });
+
+ it('should have linkedTransactionId as empty string', () => {
+ expect(initialContext.linkedTransactionId).toBe('');
+ expect(typeof initialContext.linkedTransactionId).toBe('string');
+ });
+
+ it('should have showFaceAuthConsent as true', () => {
+ expect(initialContext.showFaceAuthConsent).toBe(true);
+ expect(typeof initialContext.showFaceAuthConsent).toBe('boolean');
+ });
+
+ it('should have isQrLoginViaDeepLink as false', () => {
+ expect(initialContext.isQrLoginViaDeepLink).toBe(false);
+ expect(typeof initialContext.isQrLoginViaDeepLink).toBe('boolean');
+ });
+
+ it('should have all required properties', () => {
+ const requiredProps = [
+ 'serviceRefs',
+ 'selectedVc',
+ 'linkCode',
+ 'flowType',
+ 'myVcs',
+ 'thumbprint',
+ 'linkTransactionResponse',
+ 'authFactors',
+ 'authorizeScopes',
+ 'clientName',
+ 'configs',
+ 'essentialClaims',
+ 'linkTransactionId',
+ 'logoUrl',
+ 'voluntaryClaims',
+ 'selectedVoluntaryClaims',
+ 'errorMessage',
+ 'domainName',
+ 'consentClaims',
+ 'isSharing',
+ 'linkedTransactionId',
+ 'showFaceAuthConsent',
+ 'isQrLoginViaDeepLink',
+ ];
+
+ requiredProps.forEach(prop => {
+ expect(initialContext).toHaveProperty(prop);
+ });
+ });
+
+ it('should have exactly 23 properties in initial context', () => {
+ const propertyCount = Object.keys(initialContext).length;
+ expect(propertyCount).toBe(23);
+ });
+ });
+
+ describe('Model events', () => {
+ it('should have events object defined', () => {
+ expect(QrLoginmodel.events).toBeDefined();
+ expect(typeof QrLoginmodel.events).toBe('object');
+ });
+
+ it('should have SELECT_VC event', () => {
+ expect(QrLoginmodel.events.SELECT_VC).toBeDefined();
+ expect(typeof QrLoginmodel.events.SELECT_VC).toBe('function');
+ });
+
+ it('should have SCANNING_DONE event', () => {
+ expect(QrLoginmodel.events.SCANNING_DONE).toBeDefined();
+ expect(typeof QrLoginmodel.events.SCANNING_DONE).toBe('function');
+ });
+
+ it('should have STORE_RESPONSE event', () => {
+ expect(QrLoginmodel.events.STORE_RESPONSE).toBeDefined();
+ expect(typeof QrLoginmodel.events.STORE_RESPONSE).toBe('function');
+ });
+
+ it('should have STORE_ERROR event', () => {
+ expect(QrLoginmodel.events.STORE_ERROR).toBeDefined();
+ expect(typeof QrLoginmodel.events.STORE_ERROR).toBe('function');
+ });
+
+ it('should have TOGGLE_CONSENT_CLAIM event', () => {
+ expect(QrLoginmodel.events.TOGGLE_CONSENT_CLAIM).toBeDefined();
+ expect(typeof QrLoginmodel.events.TOGGLE_CONSENT_CLAIM).toBe('function');
+ });
+
+ it('should have DISMISS event', () => {
+ expect(QrLoginmodel.events.DISMISS).toBeDefined();
+ expect(typeof QrLoginmodel.events.DISMISS).toBe('function');
+ });
+
+ it('should have CONFIRM event', () => {
+ expect(QrLoginmodel.events.CONFIRM).toBeDefined();
+ expect(typeof QrLoginmodel.events.CONFIRM).toBe('function');
+ });
+
+ it('should have GET event', () => {
+ expect(QrLoginmodel.events.GET).toBeDefined();
+ expect(typeof QrLoginmodel.events.GET).toBe('function');
+ });
+
+ it('should have VERIFY event', () => {
+ expect(QrLoginmodel.events.VERIFY).toBeDefined();
+ expect(typeof QrLoginmodel.events.VERIFY).toBe('function');
+ });
+
+ it('should have CANCEL event', () => {
+ expect(QrLoginmodel.events.CANCEL).toBeDefined();
+ expect(typeof QrLoginmodel.events.CANCEL).toBe('function');
+ });
+
+ it('should have FACE_VALID event', () => {
+ expect(QrLoginmodel.events.FACE_VALID).toBeDefined();
+ expect(typeof QrLoginmodel.events.FACE_VALID).toBe('function');
+ });
+
+ it('should have FACE_INVALID event', () => {
+ expect(QrLoginmodel.events.FACE_INVALID).toBeDefined();
+ expect(typeof QrLoginmodel.events.FACE_INVALID).toBe('function');
+ });
+
+ it('should have RETRY_VERIFICATION event', () => {
+ expect(QrLoginmodel.events.RETRY_VERIFICATION).toBeDefined();
+ expect(typeof QrLoginmodel.events.RETRY_VERIFICATION).toBe('function');
+ });
+
+ it('should have FACE_VERIFICATION_CONSENT event', () => {
+ expect(QrLoginmodel.events.FACE_VERIFICATION_CONSENT).toBeDefined();
+ expect(typeof QrLoginmodel.events.FACE_VERIFICATION_CONSENT).toBe(
+ 'function',
+ );
+ });
+ });
+
+ describe('String properties', () => {
+ const context = QrLoginmodel.initialContext;
+
+ it('all string properties should be empty strings initially', () => {
+ const stringProps = [
+ context.linkCode,
+ context.thumbprint,
+ context.linkTransactionId,
+ context.logoUrl,
+ context.errorMessage,
+ context.domainName,
+ context.linkedTransactionId,
+ ];
+
+ stringProps.forEach(prop => {
+ expect(prop).toBe('');
+ expect(typeof prop).toBe('string');
+ });
+ });
+ });
+
+ describe('Array properties', () => {
+ const context = QrLoginmodel.initialContext;
+
+ it('empty array properties should have length 0', () => {
+ const emptyArrays = [
+ context.myVcs,
+ context.authFactors,
+ context.essentialClaims,
+ context.voluntaryClaims,
+ context.selectedVoluntaryClaims,
+ ];
+
+ emptyArrays.forEach(arr => {
+ expect(Array.isArray(arr)).toBe(true);
+ expect(arr).toHaveLength(0);
+ });
+ });
+
+ it('consentClaims should have length 2', () => {
+ expect(context.consentClaims).toHaveLength(2);
+ expect(Array.isArray(context.consentClaims)).toBe(true);
+ });
+ });
+
+ describe('Object properties', () => {
+ const context = QrLoginmodel.initialContext;
+
+ it('empty object properties should have no keys', () => {
+ const emptyObjects = [
+ context.serviceRefs,
+ context.selectedVc,
+ context.linkTransactionResponse,
+ context.clientName,
+ context.configs,
+ context.isSharing,
+ ];
+
+ emptyObjects.forEach(obj => {
+ expect(typeof obj).toBe('object');
+ expect(Object.keys(obj)).toHaveLength(0);
+ });
+ });
+ });
+
+ describe('Boolean properties', () => {
+ const context = QrLoginmodel.initialContext;
+
+ it('showFaceAuthConsent should be true', () => {
+ expect(context.showFaceAuthConsent).toBe(true);
+ });
+
+ it('isQrLoginViaDeepLink should be false', () => {
+ expect(context.isQrLoginViaDeepLink).toBe(false);
+ });
+ });
+});
diff --git a/machines/QrLogin/QrLoginSelectors.test.ts b/machines/QrLogin/QrLoginSelectors.test.ts
new file mode 100644
index 00000000..8f82cc8f
--- /dev/null
+++ b/machines/QrLogin/QrLoginSelectors.test.ts
@@ -0,0 +1,512 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import {
+ selectMyVcs,
+ selectIsWaitingForData,
+ selectDomainName,
+ selectIsLinkTransaction,
+ selectIsloadMyVcs,
+ selectIsShowingVcList,
+ selectIsisVerifyingIdentity,
+ selectIsInvalidIdentity,
+ selectIsShowError,
+ selectIsRequestConsent,
+ selectIsSendingAuthenticate,
+ selectIsSendingConsent,
+ selectIsVerifyingSuccesful,
+ selectCredential,
+ selectVerifiableCredentialData,
+ selectLinkTransactionResponse,
+ selectEssentialClaims,
+ selectVoluntaryClaims,
+ selectLogoUrl,
+ selectClientName,
+ selectErrorMessage,
+ selectIsSharing,
+ selectIsQrLoginViaDeepLink,
+ selectIsFaceVerificationConsent,
+} from './QrLoginSelectors';
+
+describe('QrLoginSelectors', () => {
+ const mockVc = {
+ vcMetadata: {id: 'vc1', displayName: 'Test VC'},
+ credential: {},
+ };
+
+ const mockState: any = {
+ context: {
+ myVcs: [mockVc],
+ domainName: 'example.com',
+ selectedVc: mockVc,
+ senderInfo: {name: 'Test Sender'},
+ linkTransactionResponse: {status: 'success'},
+ verifiableCredentialData: {type: 'VerifiableCredential'},
+ connectionParams: {uri: 'https://connect.example.com'},
+ },
+ matches: jest.fn((stateName: string) => stateName === 'waitingForData'),
+ };
+
+ describe('selectMyVcs', () => {
+ it('should return my VCs from context', () => {
+ const result = selectMyVcs(mockState);
+ expect(result).toEqual([mockVc]);
+ });
+
+ it('should return array of VCs', () => {
+ const result = selectMyVcs(mockState);
+ expect(Array.isArray(result)).toBe(true);
+ expect(result.length).toBe(1);
+ });
+ });
+
+ describe('selectIsWaitingForData', () => {
+ it('should return true when in waitingForData state', () => {
+ const result = selectIsWaitingForData(mockState);
+ expect(result).toBe(true);
+ });
+
+ it('should call matches with correct state', () => {
+ selectIsWaitingForData(mockState);
+ expect(mockState.matches).toHaveBeenCalledWith('waitingForData');
+ });
+
+ it('should return false when not in waitingForData state', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn(() => false),
+ };
+ const result = selectIsWaitingForData(state);
+ expect(result).toBe(false);
+ });
+ });
+
+ describe('selectDomainName', () => {
+ it('should return domain name from context', () => {
+ const result = selectDomainName(mockState);
+ expect(result).toBe('example.com');
+ });
+ });
+
+ describe('selectIsLinkTransaction', () => {
+ it('should return true when in linkTransaction state', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'linkTransaction'),
+ };
+ const result = selectIsLinkTransaction(state);
+ expect(result).toBe(true);
+ });
+
+ it('should call matches with linkTransaction', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn(() => false),
+ };
+ selectIsLinkTransaction(state);
+ expect(state.matches).toHaveBeenCalledWith('linkTransaction');
+ });
+ });
+
+ describe('selectIsloadMyVcs', () => {
+ it('should return true when in loadMyVcs state', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'loadMyVcs'),
+ };
+ const result = selectIsloadMyVcs(state);
+ expect(result).toBe(true);
+ });
+
+ it('should call matches with loadMyVcs', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn(() => false),
+ };
+ selectIsloadMyVcs(state);
+ expect(state.matches).toHaveBeenCalledWith('loadMyVcs');
+ });
+ });
+
+ describe('selectIsShowingVcList', () => {
+ it('should return true when in showvcList state', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'showvcList'),
+ };
+ const result = selectIsShowingVcList(state);
+ expect(result).toBe(true);
+ });
+
+ it('should call matches with showvcList', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn(() => false),
+ };
+ selectIsShowingVcList(state);
+ expect(state.matches).toHaveBeenCalledWith('showvcList');
+ });
+ });
+
+ describe('selectIsisVerifyingIdentity', () => {
+ it('should return true when in faceAuth state', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'faceAuth'),
+ };
+ const result = selectIsisVerifyingIdentity(state);
+ expect(result).toBe(true);
+ });
+
+ it('should call matches with faceAuth', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn(() => false),
+ };
+ selectIsisVerifyingIdentity(state);
+ expect(state.matches).toHaveBeenCalledWith('faceAuth');
+ });
+ });
+
+ describe('selectIsInvalidIdentity', () => {
+ it('should return true when in invalidIdentity state', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'invalidIdentity'),
+ };
+ const result = selectIsInvalidIdentity(state);
+ expect(result).toBe(true);
+ });
+
+ it('should call matches with invalidIdentity', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn(() => false),
+ };
+ selectIsInvalidIdentity(state);
+ expect(state.matches).toHaveBeenCalledWith('invalidIdentity');
+ });
+ });
+
+ describe('selectIsShowError', () => {
+ it('should return true when in ShowError state', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'ShowError'),
+ };
+ const result = selectIsShowError(state);
+ expect(result).toBe(true);
+ });
+
+ it('should call matches with ShowError', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn(() => false),
+ };
+ selectIsShowError(state);
+ expect(state.matches).toHaveBeenCalledWith('ShowError');
+ });
+ });
+
+ describe('selectIsRequestConsent', () => {
+ it('should return true when in requestConsent state', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'requestConsent'),
+ };
+ const result = selectIsRequestConsent(state);
+ expect(result).toBe(true);
+ });
+
+ it('should call matches with requestConsent', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn(() => false),
+ };
+ selectIsRequestConsent(state);
+ expect(state.matches).toHaveBeenCalledWith('requestConsent');
+ });
+ });
+
+ describe('selectIsSendingAuthenticate', () => {
+ it('should return true when in sendingAuthenticate state', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'sendingAuthenticate'),
+ };
+ const result = selectIsSendingAuthenticate(state);
+ expect(result).toBe(true);
+ });
+
+ it('should call matches with sendingAuthenticate', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn(() => false),
+ };
+ selectIsSendingAuthenticate(state);
+ expect(state.matches).toHaveBeenCalledWith('sendingAuthenticate');
+ });
+ });
+
+ describe('selectIsSendingConsent', () => {
+ it('should return true when in sendingConsent state', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'sendingConsent'),
+ };
+ const result = selectIsSendingConsent(state);
+ expect(result).toBe(true);
+ });
+
+ it('should call matches with sendingConsent', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn(() => false),
+ };
+ selectIsSendingConsent(state);
+ expect(state.matches).toHaveBeenCalledWith('sendingConsent');
+ });
+ });
+
+ describe('selectIsVerifyingSuccesful', () => {
+ it('should return true when in success state', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'success'),
+ };
+ const result = selectIsVerifyingSuccesful(state);
+ expect(result).toBe(true);
+ });
+
+ it('should call matches with success', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn(() => false),
+ };
+ selectIsVerifyingSuccesful(state);
+ expect(state.matches).toHaveBeenCalledWith('success');
+ });
+ });
+
+ describe('selectCredential', () => {
+ it('should return credential from selectedVc', () => {
+ const result = selectCredential(mockState);
+ expect(result).toBeDefined();
+ });
+
+ it('should handle null selectedVc', () => {
+ const state: any = {
+ ...mockState,
+ context: {...mockState.context, selectedVc: null},
+ };
+ const result = selectCredential(state);
+ expect(result).toBeUndefined();
+ });
+ });
+
+ describe('selectVerifiableCredentialData', () => {
+ it('should return verifiable credential data', () => {
+ const result = selectVerifiableCredentialData(mockState);
+ expect(Array.isArray(result)).toBe(true);
+ expect(result.length).toBe(1);
+ });
+
+ it('should include vcMetadata, issuer, and issuerLogo', () => {
+ const result = selectVerifiableCredentialData(mockState);
+ expect(result[0]).toHaveProperty('vcMetadata');
+ expect(result[0]).toHaveProperty('issuer');
+ expect(result[0]).toHaveProperty('issuerLogo');
+ });
+ });
+
+ describe('selectLinkTransactionResponse', () => {
+ it('should return link transaction response from context', () => {
+ const result = selectLinkTransactionResponse(mockState);
+ expect(result).toEqual({status: 'success'});
+ });
+ });
+
+ describe('selectEssentialClaims', () => {
+ const stateWithClaims: any = {
+ ...mockState,
+ context: {...mockState.context, essentialClaims: ['name', 'age']},
+ };
+
+ it('should return essential claims from context', () => {
+ const result = selectEssentialClaims(stateWithClaims);
+ expect(result).toEqual(['name', 'age']);
+ });
+ });
+
+ describe('selectVoluntaryClaims', () => {
+ const stateWithClaims: any = {
+ ...mockState,
+ context: {...mockState.context, voluntaryClaims: ['email']},
+ };
+
+ it('should return voluntary claims from context', () => {
+ const result = selectVoluntaryClaims(stateWithClaims);
+ expect(result).toEqual(['email']);
+ });
+ });
+
+ describe('selectLogoUrl', () => {
+ const stateWithLogo: any = {
+ ...mockState,
+ context: {...mockState.context, logoUrl: 'https://example.com/logo.png'},
+ };
+
+ it('should return logo URL from context', () => {
+ const result = selectLogoUrl(stateWithLogo);
+ expect(result).toBe('https://example.com/logo.png');
+ });
+ });
+
+ describe('selectClientName', () => {
+ const stateWithClient: any = {
+ ...mockState,
+ context: {...mockState.context, clientName: 'Test Client'},
+ };
+
+ it('should return client name from context', () => {
+ const result = selectClientName(stateWithClient);
+ expect(result).toBe('Test Client');
+ });
+ });
+
+ describe('selectErrorMessage', () => {
+ const stateWithError: any = {
+ ...mockState,
+ context: {...mockState.context, errorMessage: 'Test error'},
+ };
+
+ it('should return error message from context', () => {
+ const result = selectErrorMessage(stateWithError);
+ expect(result).toBe('Test error');
+ });
+ });
+
+ describe('selectIsSharing', () => {
+ const stateWithSharing: any = {
+ ...mockState,
+ context: {...mockState.context, isSharing: true},
+ };
+
+ it('should return sharing status from context', () => {
+ const result = selectIsSharing(stateWithSharing);
+ expect(result).toBe(true);
+ });
+
+ it('should return false when not sharing', () => {
+ const state: any = {
+ ...mockState,
+ context: {...mockState.context, isSharing: false},
+ };
+ const result = selectIsSharing(state);
+ expect(result).toBe(false);
+ });
+ });
+
+ describe('selectIsQrLoginViaDeepLink', () => {
+ const stateWithDeepLink: any = {
+ ...mockState,
+ context: {...mockState.context, isQrLoginViaDeepLink: true},
+ };
+
+ it('should return deep link status from context', () => {
+ const result = selectIsQrLoginViaDeepLink(stateWithDeepLink);
+ expect(result).toBe(true);
+ });
+
+ it('should return false when not via deep link', () => {
+ const state: any = {
+ ...mockState,
+ context: {...mockState.context, isQrLoginViaDeepLink: false},
+ };
+ const result = selectIsQrLoginViaDeepLink(state);
+ expect(result).toBe(false);
+ });
+ });
+
+ describe('selectIsFaceVerificationConsent', () => {
+ it('should return true when in faceVerificationConsent state', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'faceVerificationConsent'),
+ };
+ const result = selectIsFaceVerificationConsent(state);
+ expect(result).toBe(true);
+ });
+
+ it('should call matches with faceVerificationConsent', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn(() => false),
+ };
+ selectIsFaceVerificationConsent(state);
+ expect(state.matches).toHaveBeenCalledWith('faceVerificationConsent');
+ });
+ });
+
+ describe('Selectors with empty/null values', () => {
+ const emptyState: any = {
+ context: {
+ myVcs: [],
+ domainName: '',
+ selectedVc: null,
+ linkTransactionResponse: null,
+ essentialClaims: [],
+ voluntaryClaims: [],
+ logoUrl: '',
+ clientName: '',
+ errorMessage: '',
+ isSharing: false,
+ isQrLoginViaDeepLink: false,
+ },
+ matches: jest.fn(() => false),
+ };
+
+ it('should handle empty VCs array', () => {
+ const result = selectMyVcs(emptyState);
+ expect(result).toEqual([]);
+ });
+
+ it('should handle empty domain name', () => {
+ const result = selectDomainName(emptyState);
+ expect(result).toBe('');
+ });
+
+ it('should handle empty strings', () => {
+ expect(selectLogoUrl(emptyState)).toBe('');
+ expect(selectClientName(emptyState)).toBe('');
+ expect(selectErrorMessage(emptyState)).toBe('');
+ });
+
+ it('should handle null responses', () => {
+ expect(selectLinkTransactionResponse(emptyState)).toBeNull();
+ });
+
+ it('should handle empty arrays', () => {
+ expect(selectEssentialClaims(emptyState)).toEqual([]);
+ expect(selectVoluntaryClaims(emptyState)).toEqual([]);
+ });
+
+ it('should handle false boolean values', () => {
+ expect(selectIsSharing(emptyState)).toBe(false);
+ expect(selectIsQrLoginViaDeepLink(emptyState)).toBe(false);
+ });
+
+ it('should return false for all state checks', () => {
+ expect(selectIsWaitingForData(emptyState)).toBe(false);
+ expect(selectIsLinkTransaction(emptyState)).toBe(false);
+ expect(selectIsloadMyVcs(emptyState)).toBe(false);
+ expect(selectIsShowingVcList(emptyState)).toBe(false);
+ expect(selectIsisVerifyingIdentity(emptyState)).toBe(false);
+ expect(selectIsInvalidIdentity(emptyState)).toBe(false);
+ expect(selectIsShowError(emptyState)).toBe(false);
+ expect(selectIsRequestConsent(emptyState)).toBe(false);
+ expect(selectIsSendingAuthenticate(emptyState)).toBe(false);
+ expect(selectIsSendingConsent(emptyState)).toBe(false);
+ expect(selectIsVerifyingSuccesful(emptyState)).toBe(false);
+ expect(selectIsFaceVerificationConsent(emptyState)).toBe(false);
+ });
+ });
+});
diff --git a/machines/VerifiableCredential/VCItemMachine/VCItemModel.test.ts b/machines/VerifiableCredential/VCItemMachine/VCItemModel.test.ts
new file mode 100644
index 00000000..037e18db
--- /dev/null
+++ b/machines/VerifiableCredential/VCItemMachine/VCItemModel.test.ts
@@ -0,0 +1,290 @@
+import {VCItemModel} from './VCItemModel';
+
+describe('VCItemModel', () => {
+ describe('Model structure', () => {
+ it('should be defined', () => {
+ expect(VCItemModel).toBeDefined();
+ });
+
+ it('should have initialContext', () => {
+ expect(VCItemModel.initialContext).toBeDefined();
+ });
+
+ it('should have events', () => {
+ expect(VCItemModel.events).toBeDefined();
+ });
+ });
+
+ describe('Initial Context', () => {
+ const initialContext = VCItemModel.initialContext;
+
+ it('should have serviceRefs as empty object', () => {
+ expect(initialContext.serviceRefs).toEqual({});
+ expect(typeof initialContext.serviceRefs).toBe('object');
+ });
+
+ it('should have vcMetadata as empty object', () => {
+ expect(initialContext.vcMetadata).toEqual({});
+ expect(typeof initialContext.vcMetadata).toBe('object');
+ });
+
+ it('should have generatedOn as Date instance', () => {
+ expect(initialContext.generatedOn).toBeInstanceOf(Date);
+ });
+
+ it('should have credential as null', () => {
+ expect(initialContext.credential).toBeNull();
+ });
+
+ it('should have verifiableCredential as null', () => {
+ expect(initialContext.verifiableCredential).toBeNull();
+ });
+
+ it('should have hashedId as empty string', () => {
+ expect(initialContext.hashedId).toBe('');
+ expect(typeof initialContext.hashedId).toBe('string');
+ });
+
+ it('should have publicKey as empty string', () => {
+ expect(initialContext.publicKey).toBe('');
+ expect(typeof initialContext.publicKey).toBe('string');
+ });
+
+ it('should have privateKey as empty string', () => {
+ expect(initialContext.privateKey).toBe('');
+ expect(typeof initialContext.privateKey).toBe('string');
+ });
+
+ it('should have OTP as empty string', () => {
+ expect(initialContext.OTP).toBe('');
+ expect(typeof initialContext.OTP).toBe('string');
+ });
+
+ it('should have error as empty string', () => {
+ expect(initialContext.error).toBe('');
+ expect(typeof initialContext.error).toBe('string');
+ });
+
+ it('should have bindingTransactionId as empty string', () => {
+ expect(initialContext.bindingTransactionId).toBe('');
+ expect(typeof initialContext.bindingTransactionId).toBe('string');
+ });
+
+ it('should have requestId as empty string', () => {
+ expect(initialContext.requestId).toBe('');
+ expect(typeof initialContext.requestId).toBe('string');
+ });
+
+ it('should have downloadCounter as 0', () => {
+ expect(initialContext.downloadCounter).toBe(0);
+ expect(typeof initialContext.downloadCounter).toBe('number');
+ });
+
+ it('should have maxDownloadCount as null', () => {
+ expect(initialContext.maxDownloadCount).toBeNull();
+ });
+
+ it('should have downloadInterval as null', () => {
+ expect(initialContext.downloadInterval).toBeNull();
+ });
+
+ it('should have walletBindingResponse as null', () => {
+ expect(initialContext.walletBindingResponse).toBeNull();
+ });
+
+ it('should have isMachineInKebabPopupState as false', () => {
+ expect(initialContext.isMachineInKebabPopupState).toBe(false);
+ expect(typeof initialContext.isMachineInKebabPopupState).toBe('boolean');
+ });
+
+ it('should have communicationDetails as null', () => {
+ expect(initialContext.communicationDetails).toBeNull();
+ });
+
+ it('should have verificationStatus as null', () => {
+ expect(initialContext.verificationStatus).toBeNull();
+ });
+
+ it('should have showVerificationStatusBanner as false', () => {
+ expect(initialContext.showVerificationStatusBanner).toBe(false);
+ expect(typeof initialContext.showVerificationStatusBanner).toBe(
+ 'boolean',
+ );
+ });
+
+ it('should have wellknownResponse as empty object', () => {
+ expect(initialContext.wellknownResponse).toEqual({});
+ expect(typeof initialContext.wellknownResponse).toBe('object');
+ });
+
+ it('should have all 24 required properties', () => {
+ const properties = Object.keys(initialContext);
+ expect(properties).toHaveLength(24);
+ });
+ });
+
+ describe('String properties', () => {
+ const context = VCItemModel.initialContext;
+
+ it('all empty string properties should be empty', () => {
+ const emptyStrings = [
+ context.hashedId,
+ context.publicKey,
+ context.privateKey,
+ context.OTP,
+ context.error,
+ context.bindingTransactionId,
+ context.requestId,
+ ];
+
+ emptyStrings.forEach(str => {
+ expect(str).toBe('');
+ expect(typeof str).toBe('string');
+ });
+ });
+ });
+
+ describe('Object properties', () => {
+ const context = VCItemModel.initialContext;
+
+ it('empty object properties should be empty objects', () => {
+ const emptyObjects = [
+ context.serviceRefs,
+ context.vcMetadata,
+ context.wellknownResponse,
+ ];
+
+ emptyObjects.forEach(obj => {
+ expect(typeof obj).toBe('object');
+ expect(Object.keys(obj)).toHaveLength(0);
+ });
+ });
+ });
+
+ describe('Boolean properties', () => {
+ const context = VCItemModel.initialContext;
+
+ it('isMachineInKebabPopupState should be false', () => {
+ expect(context.isMachineInKebabPopupState).toBe(false);
+ expect(typeof context.isMachineInKebabPopupState).toBe('boolean');
+ });
+
+ it('showVerificationStatusBanner should be false', () => {
+ expect(context.showVerificationStatusBanner).toBe(false);
+ expect(typeof context.showVerificationStatusBanner).toBe('boolean');
+ });
+
+ it('all boolean properties should be false initially', () => {
+ const booleans = [
+ context.isMachineInKebabPopupState,
+ context.showVerificationStatusBanner,
+ ];
+
+ booleans.forEach(bool => {
+ expect(bool).toBe(false);
+ expect(typeof bool).toBe('boolean');
+ });
+ });
+ });
+
+ describe('Null properties', () => {
+ const context = VCItemModel.initialContext;
+
+ it('credential should be null', () => {
+ expect(context.credential).toBeNull();
+ });
+
+ it('verifiableCredential should be null', () => {
+ expect(context.verifiableCredential).toBeNull();
+ });
+
+ it('maxDownloadCount should be null', () => {
+ expect(context.maxDownloadCount).toBeNull();
+ });
+
+ it('downloadInterval should be null', () => {
+ expect(context.downloadInterval).toBeNull();
+ });
+
+ it('walletBindingResponse should be null', () => {
+ expect(context.walletBindingResponse).toBeNull();
+ });
+
+ it('communicationDetails should be null', () => {
+ expect(context.communicationDetails).toBeNull();
+ });
+
+ it('verificationStatus should be null', () => {
+ expect(context.verificationStatus).toBeNull();
+ });
+
+ it('all null properties should be null initially', () => {
+ const nullProps = [
+ context.credential,
+ context.verifiableCredential,
+ context.maxDownloadCount,
+ context.downloadInterval,
+ context.walletBindingResponse,
+ context.communicationDetails,
+ context.verificationStatus,
+ ];
+
+ nullProps.forEach(prop => {
+ expect(prop).toBeNull();
+ });
+ });
+ });
+
+ describe('Number properties', () => {
+ const context = VCItemModel.initialContext;
+
+ it('downloadCounter should be 0', () => {
+ expect(context.downloadCounter).toBe(0);
+ expect(typeof context.downloadCounter).toBe('number');
+ });
+ });
+
+ describe('Date properties', () => {
+ const context = VCItemModel.initialContext;
+
+ it('generatedOn should be a Date instance', () => {
+ expect(context.generatedOn).toBeInstanceOf(Date);
+ });
+
+ it('generatedOn should be a valid date', () => {
+ expect(context.generatedOn.getTime()).not.toBeNaN();
+ });
+ });
+
+ describe('Model events', () => {
+ it('should have events object', () => {
+ expect(VCItemModel.events).toBeDefined();
+ expect(typeof VCItemModel.events).toBe('object');
+ });
+
+ it('should have event creators', () => {
+ const eventKeys = Object.keys(VCItemModel.events);
+ expect(eventKeys.length).toBeGreaterThan(0);
+ });
+ });
+
+ describe('Property types validation', () => {
+ const context = VCItemModel.initialContext;
+
+ it('should have correct types for all properties', () => {
+ expect(typeof context.hashedId).toBe('string');
+ expect(typeof context.publicKey).toBe('string');
+ expect(typeof context.privateKey).toBe('string');
+ expect(typeof context.OTP).toBe('string');
+ expect(typeof context.error).toBe('string');
+ expect(typeof context.bindingTransactionId).toBe('string');
+ expect(typeof context.requestId).toBe('string');
+ expect(typeof context.downloadCounter).toBe('number');
+ expect(typeof context.isMachineInKebabPopupState).toBe('boolean');
+ expect(typeof context.showVerificationStatusBanner).toBe('boolean');
+ expect(typeof context.serviceRefs).toBe('object');
+ expect(typeof context.vcMetadata).toBe('object');
+ expect(context.generatedOn).toBeInstanceOf(Date);
+ });
+ });
+});
diff --git a/machines/VerifiableCredential/VCItemMachine/VCItemSelectors.test.ts b/machines/VerifiableCredential/VCItemMachine/VCItemSelectors.test.ts
new file mode 100644
index 00000000..ff9609d8
--- /dev/null
+++ b/machines/VerifiableCredential/VCItemMachine/VCItemSelectors.test.ts
@@ -0,0 +1,451 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import {
+ selectVerificationStatus,
+ selectIsVerificationInProgress,
+ selectIsVerificationCompleted,
+ selectShowVerificationStatusBanner,
+ selectVerifiableCredential,
+ getVerifiableCredential,
+ selectCredential,
+ selectVerifiableCredentialData,
+ selectKebabPopUp,
+ selectContext,
+ selectGeneratedOn,
+ selectWalletBindingSuccess,
+ selectWalletBindingResponse,
+ selectIsCommunicationDetails,
+ selectWalletBindingError,
+ selectBindingAuthFailedError,
+ selectAcceptingBindingOtp,
+ selectWalletBindingInProgress,
+ selectBindingWarning,
+ selectRemoveWalletWarning,
+ selectIsPinned,
+ selectOtpError,
+ selectShowActivities,
+ selectShowWalletBindingError,
+ selectVc,
+} from './VCItemSelectors';
+
+describe('VCItemSelectors', () => {
+ describe('selectVerificationStatus', () => {
+ it('should return verification status from context', () => {
+ const mockState: any = {
+ context: {
+ verificationStatus: 'verified',
+ },
+ };
+ expect(selectVerificationStatus(mockState)).toBe('verified');
+ });
+ });
+
+ describe('selectIsVerificationInProgress', () => {
+ it('should return true when in verifyingCredential state', () => {
+ const mockState: any = {
+ matches: jest.fn(
+ (state: string) => state === 'verifyState.verifyingCredential',
+ ),
+ };
+ expect(selectIsVerificationInProgress(mockState)).toBe(true);
+ });
+
+ it('should return false when not in verifyingCredential state', () => {
+ const mockState: any = {
+ matches: jest.fn(() => false),
+ };
+ expect(selectIsVerificationInProgress(mockState)).toBe(false);
+ });
+ });
+
+ describe('selectIsVerificationCompleted', () => {
+ it('should return true when in verificationCompleted state', () => {
+ const mockState: any = {
+ matches: jest.fn(
+ (state: string) => state === 'verifyState.verificationCompleted',
+ ),
+ };
+ expect(selectIsVerificationCompleted(mockState)).toBe(true);
+ });
+
+ it('should return false when not in verificationCompleted state', () => {
+ const mockState: any = {
+ matches: jest.fn(() => false),
+ };
+ expect(selectIsVerificationCompleted(mockState)).toBe(false);
+ });
+ });
+
+ describe('selectShowVerificationStatusBanner', () => {
+ it('should return showVerificationStatusBanner from context', () => {
+ const mockState: any = {
+ context: {
+ showVerificationStatusBanner: true,
+ },
+ };
+ expect(selectShowVerificationStatusBanner(mockState)).toBe(true);
+ });
+ });
+
+ describe('selectVerifiableCredential', () => {
+ it('should return verifiableCredential from context', () => {
+ const mockVC = {credential: {id: 'test-123'}};
+ const mockState: any = {
+ context: {
+ verifiableCredential: mockVC,
+ },
+ };
+ expect(selectVerifiableCredential(mockState)).toBe(mockVC);
+ });
+ });
+
+ describe('getVerifiableCredential', () => {
+ it('should return credential property if it exists', () => {
+ const mockCredential = {id: 'cred-123'};
+ const mockVC: any = {
+ credential: mockCredential,
+ };
+ expect(getVerifiableCredential(mockVC)).toBe(mockCredential);
+ });
+
+ it('should return the whole object if credential property does not exist', () => {
+ const mockCredential: any = {id: 'cred-456'};
+ expect(getVerifiableCredential(mockCredential)).toBe(mockCredential);
+ });
+ });
+
+ describe('selectCredential', () => {
+ it('should return verifiableCredential from context', () => {
+ const mockVC = {credential: {id: 'test-789'}};
+ const mockState: any = {
+ context: {
+ verifiableCredential: mockVC,
+ },
+ };
+ expect(selectCredential(mockState)).toBe(mockVC);
+ });
+ });
+
+ describe('selectVerifiableCredentialData', () => {
+ it('should return formatted verifiable credential data', () => {
+ const mockState: any = {
+ context: {
+ vcMetadata: {
+ id: 'vc-001',
+ issuer: 'Test Issuer',
+ format: 'ldp_vc',
+ },
+ verifiableCredential: {
+ credential: {
+ credentialSubject: {
+ name: 'John Doe',
+ },
+ },
+ issuerLogo: 'https://example.com/logo.png',
+ wellKnown: 'https://example.com/.well-known',
+ credentialConfigurationId: 'config-123',
+ },
+ format: 'ldp_vc',
+ credential: null,
+ },
+ };
+
+ const result = selectVerifiableCredentialData(mockState);
+ expect(result.issuer).toBe('Test Issuer');
+ expect(result.issuerLogo).toBe('https://example.com/logo.png');
+ expect(result.wellKnown).toBe('https://example.com/.well-known');
+ expect(result.credentialConfigurationId).toBe('config-123');
+ });
+ });
+
+ describe('selectKebabPopUp', () => {
+ it('should return isMachineInKebabPopupState from context', () => {
+ const mockState: any = {
+ context: {
+ isMachineInKebabPopupState: true,
+ },
+ };
+ expect(selectKebabPopUp(mockState)).toBe(true);
+ });
+ });
+
+ describe('selectContext', () => {
+ it('should return entire context', () => {
+ const mockContext = {
+ verificationStatus: 'verified',
+ generatedOn: '2023-01-01',
+ };
+ const mockState: any = {
+ context: mockContext,
+ };
+ expect(selectContext(mockState)).toBe(mockContext);
+ });
+ });
+
+ describe('selectGeneratedOn', () => {
+ it('should return generatedOn from context', () => {
+ const mockState: any = {
+ context: {
+ generatedOn: '2023-12-25',
+ },
+ };
+ expect(selectGeneratedOn(mockState)).toBe('2023-12-25');
+ });
+ });
+
+ describe('selectWalletBindingSuccess', () => {
+ it('should return walletBindingResponse from context', () => {
+ const mockResponse = {walletBindingId: 'binding-123'};
+ const mockState: any = {
+ context: {
+ walletBindingResponse: mockResponse,
+ },
+ };
+ expect(selectWalletBindingSuccess(mockState)).toBe(mockResponse);
+ });
+ });
+
+ describe('selectWalletBindingResponse', () => {
+ it('should return walletBindingResponse from context', () => {
+ const mockResponse = {walletBindingId: 'binding-456'};
+ const mockState: any = {
+ context: {
+ walletBindingResponse: mockResponse,
+ },
+ };
+ expect(selectWalletBindingResponse(mockState)).toBe(mockResponse);
+ });
+ });
+
+ describe('selectIsCommunicationDetails', () => {
+ it('should return communicationDetails from context', () => {
+ const mockDetails = {email: 'test@example.com', phone: '1234567890'};
+ const mockState: any = {
+ context: {
+ communicationDetails: mockDetails,
+ },
+ };
+ expect(selectIsCommunicationDetails(mockState)).toBe(mockDetails);
+ });
+ });
+
+ describe('selectWalletBindingError', () => {
+ it('should return error from context', () => {
+ const mockError = new Error('Binding failed');
+ const mockState: any = {
+ context: {
+ error: mockError,
+ },
+ };
+ expect(selectWalletBindingError(mockState)).toBe(mockError);
+ });
+ });
+
+ describe('selectBindingAuthFailedError', () => {
+ it('should return error from context', () => {
+ const mockError = new Error('Auth failed');
+ const mockState: any = {
+ context: {
+ error: mockError,
+ },
+ };
+ expect(selectBindingAuthFailedError(mockState)).toBe(mockError);
+ });
+ });
+
+ describe('selectAcceptingBindingOtp', () => {
+ it('should return true when in acceptingBindingOTP state', () => {
+ const mockState: any = {
+ matches: jest.fn(
+ (state: string) =>
+ state === 'vcUtilitiesState.walletBinding.acceptingBindingOTP',
+ ),
+ };
+ expect(selectAcceptingBindingOtp(mockState)).toBe(true);
+ });
+
+ it('should return false when not in acceptingBindingOTP state', () => {
+ const mockState: any = {
+ matches: jest.fn(() => false),
+ };
+ expect(selectAcceptingBindingOtp(mockState)).toBe(false);
+ });
+ });
+
+ describe('selectWalletBindingInProgress', () => {
+ it('should return true when in requestingBindingOTP state', () => {
+ const mockState: any = {
+ matches: jest.fn(
+ (state: string) =>
+ state === 'vcUtilitiesState.walletBinding.requestingBindingOTP',
+ ),
+ };
+ expect(selectWalletBindingInProgress(mockState)).toBe(true);
+ });
+
+ it('should return true when in addingWalletBindingId state', () => {
+ const mockState: any = {
+ matches: jest.fn(
+ (state: string) =>
+ state === 'vcUtilitiesState.walletBinding.addingWalletBindingId',
+ ),
+ };
+ expect(selectWalletBindingInProgress(mockState)).toBe(true);
+ });
+
+ it('should return true when in addKeyPair state', () => {
+ const mockState: any = {
+ matches: jest.fn(
+ (state: string) =>
+ state === 'vcUtilitiesState.walletBinding.addKeyPair',
+ ),
+ };
+ expect(selectWalletBindingInProgress(mockState)).toBe(true);
+ });
+
+ it('should return true when in updatingPrivateKey state', () => {
+ const mockState: any = {
+ matches: jest.fn(
+ (state: string) =>
+ state === 'vcUtilitiesState.walletBinding.updatingPrivateKey',
+ ),
+ };
+ expect(selectWalletBindingInProgress(mockState)).toBe(true);
+ });
+
+ it('should return false when not in any wallet binding progress state', () => {
+ const mockState: any = {
+ matches: jest.fn(() => false),
+ };
+ expect(selectWalletBindingInProgress(mockState)).toBe(false);
+ });
+ });
+
+ describe('selectBindingWarning', () => {
+ it('should return true when in showBindingWarning state', () => {
+ const mockState: any = {
+ matches: jest.fn(
+ (state: string) =>
+ state === 'vcUtilitiesState.walletBinding.showBindingWarning',
+ ),
+ };
+ expect(selectBindingWarning(mockState)).toBe(true);
+ });
+
+ it('should return false when not in showBindingWarning state', () => {
+ const mockState: any = {
+ matches: jest.fn(() => false),
+ };
+ expect(selectBindingWarning(mockState)).toBe(false);
+ });
+ });
+
+ describe('selectRemoveWalletWarning', () => {
+ it('should return true when in removeWallet state', () => {
+ const mockState: any = {
+ matches: jest.fn(
+ (state: string) =>
+ state === 'vcUtilitiesState.kebabPopUp.removeWallet',
+ ),
+ };
+ expect(selectRemoveWalletWarning(mockState)).toBe(true);
+ });
+
+ it('should return false when not in removeWallet state', () => {
+ const mockState: any = {
+ matches: jest.fn(() => false),
+ };
+ expect(selectRemoveWalletWarning(mockState)).toBe(false);
+ });
+ });
+
+ describe('selectIsPinned', () => {
+ it('should return isPinned from vcMetadata', () => {
+ const mockState: any = {
+ context: {
+ vcMetadata: {
+ isPinned: true,
+ },
+ },
+ };
+ expect(selectIsPinned(mockState)).toBe(true);
+ });
+
+ it('should return false when isPinned is false', () => {
+ const mockState: any = {
+ context: {
+ vcMetadata: {
+ isPinned: false,
+ },
+ },
+ };
+ expect(selectIsPinned(mockState)).toBe(false);
+ });
+ });
+
+ describe('selectOtpError', () => {
+ it('should return error from context', () => {
+ const mockError = new Error('OTP invalid');
+ const mockState: any = {
+ context: {
+ error: mockError,
+ },
+ };
+ expect(selectOtpError(mockState)).toBe(mockError);
+ });
+ });
+
+ describe('selectShowActivities', () => {
+ it('should return true when in showActivities state', () => {
+ const mockState: any = {
+ matches: jest.fn(
+ (state: string) =>
+ state === 'vcUtilitiesState.kebabPopUp.showActivities',
+ ),
+ };
+ expect(selectShowActivities(mockState)).toBe(true);
+ });
+
+ it('should return false when not in showActivities state', () => {
+ const mockState: any = {
+ matches: jest.fn(() => false),
+ };
+ expect(selectShowActivities(mockState)).toBe(false);
+ });
+ });
+
+ describe('selectShowWalletBindingError', () => {
+ it('should return true when in showingWalletBindingError state', () => {
+ const mockState: any = {
+ matches: jest.fn(
+ (state: string) =>
+ state ===
+ 'vcUtilitiesState.walletBinding.showingWalletBindingError',
+ ),
+ };
+ expect(selectShowWalletBindingError(mockState)).toBe(true);
+ });
+
+ it('should return false when not in showingWalletBindingError state', () => {
+ const mockState: any = {
+ matches: jest.fn(() => false),
+ };
+ expect(selectShowWalletBindingError(mockState)).toBe(false);
+ });
+ });
+
+ describe('selectVc', () => {
+ it('should return context without serviceRefs', () => {
+ const mockState: any = {
+ context: {
+ verificationStatus: 'verified',
+ generatedOn: '2023-01-01',
+ serviceRefs: {ref1: 'service1', ref2: 'service2'},
+ },
+ };
+ const result: any = selectVc(mockState);
+ expect(result.verificationStatus).toBe('verified');
+ expect(result.generatedOn).toBe('2023-01-01');
+ expect(result.serviceRefs).toBeUndefined();
+ });
+ });
+});
diff --git a/machines/VerifiableCredential/VCMetaMachine/VCMetaEvents.test.ts b/machines/VerifiableCredential/VCMetaMachine/VCMetaEvents.test.ts
new file mode 100644
index 00000000..db0ad458
--- /dev/null
+++ b/machines/VerifiableCredential/VCMetaMachine/VCMetaEvents.test.ts
@@ -0,0 +1,288 @@
+import {VcMetaEvents} from './VCMetaEvents';
+import {VCMetadata} from '../../../shared/VCMetadata';
+import {VC} from './vc';
+
+describe('VcMetaEvents', () => {
+ describe('VIEW_VC', () => {
+ it('should create event with vc', () => {
+ const vc = {id: 'vc-123'} as unknown as VC;
+ const result = VcMetaEvents.VIEW_VC(vc);
+ expect(result).toEqual({vc});
+ });
+ });
+
+ describe('GET_VC_ITEM', () => {
+ it('should create event with vcMetadata', () => {
+ const vcMetadata = new VCMetadata();
+ const result = VcMetaEvents.GET_VC_ITEM(vcMetadata);
+ expect(result).toEqual({vcMetadata});
+ });
+ });
+
+ describe('STORE_RESPONSE', () => {
+ it('should create event with response', () => {
+ const response = {data: 'test'};
+ const result = VcMetaEvents.STORE_RESPONSE(response);
+ expect(result).toEqual({response: {data: 'test'}});
+ });
+
+ it('should handle undefined response', () => {
+ const result = VcMetaEvents.STORE_RESPONSE(undefined);
+ expect(result).toEqual({response: undefined});
+ });
+ });
+
+ describe('STORE_ERROR', () => {
+ it('should create event with error', () => {
+ const error = new Error('Test error');
+ const result = VcMetaEvents.STORE_ERROR(error);
+ expect(result).toEqual({error});
+ });
+ });
+
+ describe('VC_ADDED', () => {
+ it('should create event with vcMetadata', () => {
+ const vcMetadata = new VCMetadata();
+ const result = VcMetaEvents.VC_ADDED(vcMetadata);
+ expect(result).toEqual({vcMetadata});
+ });
+ });
+
+ describe('REMOVE_VC_FROM_CONTEXT', () => {
+ it('should create event with vcMetadata', () => {
+ const vcMetadata = new VCMetadata();
+ const result = VcMetaEvents.REMOVE_VC_FROM_CONTEXT(vcMetadata);
+ expect(result).toEqual({vcMetadata});
+ });
+ });
+
+ describe('VC_METADATA_UPDATED', () => {
+ it('should create event with vcMetadata', () => {
+ const vcMetadata = new VCMetadata();
+ const result = VcMetaEvents.VC_METADATA_UPDATED(vcMetadata);
+ expect(result).toEqual({vcMetadata});
+ });
+ });
+
+ describe('VC_DOWNLOADED', () => {
+ it('should create event with vc and vcMetadata', () => {
+ const vc = {id: 'vc-123'} as unknown as VC;
+ const vcMetadata = new VCMetadata();
+ const result = VcMetaEvents.VC_DOWNLOADED(vc, vcMetadata);
+ expect(result.vc).toBe(vc);
+ expect(result.vcMetadata).toBe(vcMetadata);
+ });
+
+ it('should handle undefined vcMetadata', () => {
+ const vc = {id: 'vc-123'} as unknown as VC;
+ const result = VcMetaEvents.VC_DOWNLOADED(vc);
+ expect(result.vc).toBe(vc);
+ expect(result.vcMetadata).toBeUndefined();
+ });
+ });
+
+ describe('REFRESH_MY_VCS', () => {
+ it('should create empty event', () => {
+ const result = VcMetaEvents.REFRESH_MY_VCS();
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('REFRESH_MY_VCS_TWO', () => {
+ it('should create event with vc', () => {
+ const vc = {id: 'vc-123'} as unknown as VC;
+ const result = VcMetaEvents.REFRESH_MY_VCS_TWO(vc);
+ expect(result).toEqual({vc});
+ });
+ });
+
+ describe('REFRESH_RECEIVED_VCS', () => {
+ it('should create empty event', () => {
+ const result = VcMetaEvents.REFRESH_RECEIVED_VCS();
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('WALLET_BINDING_SUCCESS', () => {
+ it('should create event with vcKey and vc', () => {
+ const vcKey = 'key-123';
+ const vc = {id: 'vc-123'} as unknown as VC;
+ const result = VcMetaEvents.WALLET_BINDING_SUCCESS(vcKey, vc);
+ expect(result).toEqual({vcKey: 'key-123', vc});
+ });
+ });
+
+ describe('RESET_WALLET_BINDING_SUCCESS', () => {
+ it('should create empty event', () => {
+ const result = VcMetaEvents.RESET_WALLET_BINDING_SUCCESS();
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('ADD_VC_TO_IN_PROGRESS_DOWNLOADS', () => {
+ it('should create event with requestId', () => {
+ const result = VcMetaEvents.ADD_VC_TO_IN_PROGRESS_DOWNLOADS('req-123');
+ expect(result).toEqual({requestId: 'req-123'});
+ });
+ });
+
+ describe('REMOVE_VC_FROM_IN_PROGRESS_DOWNLOADS', () => {
+ it('should create event with vcMetadata', () => {
+ const vcMetadata = new VCMetadata();
+ const result =
+ VcMetaEvents.REMOVE_VC_FROM_IN_PROGRESS_DOWNLOADS(vcMetadata);
+ expect(result).toEqual({vcMetadata});
+ });
+ });
+
+ describe('RESET_IN_PROGRESS_VCS_DOWNLOADED', () => {
+ it('should create empty event', () => {
+ const result = VcMetaEvents.RESET_IN_PROGRESS_VCS_DOWNLOADED();
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('REMOVE_TAMPERED_VCS', () => {
+ it('should create empty event', () => {
+ const result = VcMetaEvents.REMOVE_TAMPERED_VCS();
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('DOWNLOAD_LIMIT_EXPIRED', () => {
+ it('should create event with vcMetadata', () => {
+ const vcMetadata = new VCMetadata();
+ const result = VcMetaEvents.DOWNLOAD_LIMIT_EXPIRED(vcMetadata);
+ expect(result).toEqual({vcMetadata});
+ });
+ });
+
+ describe('DELETE_VC', () => {
+ it('should create empty event', () => {
+ const result = VcMetaEvents.DELETE_VC();
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('VERIFY_VC_FAILED', () => {
+ it('should create event with errorMessage and vcMetadata', () => {
+ const vcMetadata = new VCMetadata();
+ const result = VcMetaEvents.VERIFY_VC_FAILED(
+ 'Verification failed',
+ vcMetadata,
+ );
+ expect(result.errorMessage).toBe('Verification failed');
+ expect(result.vcMetadata).toBe(vcMetadata);
+ });
+
+ it('should handle undefined vcMetadata', () => {
+ const result = VcMetaEvents.VERIFY_VC_FAILED('Error occurred');
+ expect(result.errorMessage).toBe('Error occurred');
+ expect(result.vcMetadata).toBeUndefined();
+ });
+ });
+
+ describe('RESET_VERIFY_ERROR', () => {
+ it('should create empty event', () => {
+ const result = VcMetaEvents.RESET_VERIFY_ERROR();
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('REFRESH_VCS_METADATA', () => {
+ it('should create empty event', () => {
+ const result = VcMetaEvents.REFRESH_VCS_METADATA();
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('SHOW_TAMPERED_POPUP', () => {
+ it('should create empty event', () => {
+ const result = VcMetaEvents.SHOW_TAMPERED_POPUP();
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('SET_VERIFICATION_STATUS', () => {
+ it('should create event with verificationStatus', () => {
+ const status = {verified: true};
+ const result = VcMetaEvents.SET_VERIFICATION_STATUS(status);
+ expect(result).toEqual({verificationStatus: status});
+ });
+ });
+
+ describe('RESET_VERIFICATION_STATUS', () => {
+ it('should create event with verificationStatus', () => {
+ const status = {message: 'Reset'} as any;
+ const result = VcMetaEvents.RESET_VERIFICATION_STATUS(status);
+ expect(result).toEqual({verificationStatus: status});
+ });
+
+ it('should handle null verificationStatus', () => {
+ const result = VcMetaEvents.RESET_VERIFICATION_STATUS(null);
+ expect(result).toEqual({verificationStatus: null});
+ });
+ });
+
+ describe('VC_DOWNLOADING_FAILED', () => {
+ it('should create empty event', () => {
+ const result = VcMetaEvents.VC_DOWNLOADING_FAILED();
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('RESET_DOWNLOADING_FAILED', () => {
+ it('should create empty event', () => {
+ const result = VcMetaEvents.RESET_DOWNLOADING_FAILED();
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('RESET_DOWNLOADING_SUCCESS', () => {
+ it('should create empty event', () => {
+ const result = VcMetaEvents.RESET_DOWNLOADING_SUCCESS();
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('VcMetaEvents object structure', () => {
+ it('should have all expected event creators', () => {
+ expect(VcMetaEvents.VIEW_VC).toBeDefined();
+ expect(VcMetaEvents.GET_VC_ITEM).toBeDefined();
+ expect(VcMetaEvents.STORE_RESPONSE).toBeDefined();
+ expect(VcMetaEvents.STORE_ERROR).toBeDefined();
+ expect(VcMetaEvents.VC_ADDED).toBeDefined();
+ expect(VcMetaEvents.REMOVE_VC_FROM_CONTEXT).toBeDefined();
+ expect(VcMetaEvents.VC_METADATA_UPDATED).toBeDefined();
+ expect(VcMetaEvents.VC_DOWNLOADED).toBeDefined();
+ expect(VcMetaEvents.REFRESH_MY_VCS).toBeDefined();
+ expect(VcMetaEvents.REFRESH_MY_VCS_TWO).toBeDefined();
+ expect(VcMetaEvents.REFRESH_RECEIVED_VCS).toBeDefined();
+ expect(VcMetaEvents.WALLET_BINDING_SUCCESS).toBeDefined();
+ expect(VcMetaEvents.RESET_WALLET_BINDING_SUCCESS).toBeDefined();
+ expect(VcMetaEvents.ADD_VC_TO_IN_PROGRESS_DOWNLOADS).toBeDefined();
+ expect(VcMetaEvents.REMOVE_VC_FROM_IN_PROGRESS_DOWNLOADS).toBeDefined();
+ expect(VcMetaEvents.RESET_IN_PROGRESS_VCS_DOWNLOADED).toBeDefined();
+ expect(VcMetaEvents.REMOVE_TAMPERED_VCS).toBeDefined();
+ expect(VcMetaEvents.DOWNLOAD_LIMIT_EXPIRED).toBeDefined();
+ expect(VcMetaEvents.DELETE_VC).toBeDefined();
+ expect(VcMetaEvents.VERIFY_VC_FAILED).toBeDefined();
+ expect(VcMetaEvents.RESET_VERIFY_ERROR).toBeDefined();
+ expect(VcMetaEvents.REFRESH_VCS_METADATA).toBeDefined();
+ expect(VcMetaEvents.SHOW_TAMPERED_POPUP).toBeDefined();
+ expect(VcMetaEvents.SET_VERIFICATION_STATUS).toBeDefined();
+ expect(VcMetaEvents.RESET_VERIFICATION_STATUS).toBeDefined();
+ expect(VcMetaEvents.VC_DOWNLOADING_FAILED).toBeDefined();
+ expect(VcMetaEvents.RESET_DOWNLOADING_FAILED).toBeDefined();
+ expect(VcMetaEvents.RESET_DOWNLOADING_SUCCESS).toBeDefined();
+ });
+
+ it('should have all event creators be functions', () => {
+ expect(typeof VcMetaEvents.VIEW_VC).toBe('function');
+ expect(typeof VcMetaEvents.GET_VC_ITEM).toBe('function');
+ expect(typeof VcMetaEvents.STORE_RESPONSE).toBe('function');
+ expect(typeof VcMetaEvents.STORE_ERROR).toBe('function');
+ expect(typeof VcMetaEvents.VC_ADDED).toBe('function');
+ });
+ });
+});
diff --git a/machines/VerifiableCredential/VCMetaMachine/VCMetaModel.test.ts b/machines/VerifiableCredential/VCMetaMachine/VCMetaModel.test.ts
new file mode 100644
index 00000000..1f1e941e
--- /dev/null
+++ b/machines/VerifiableCredential/VCMetaMachine/VCMetaModel.test.ts
@@ -0,0 +1,219 @@
+import {VCMetamodel} from './VCMetaModel';
+
+describe('VCMetaModel', () => {
+ describe('Model structure', () => {
+ it('should be defined', () => {
+ expect(VCMetamodel).toBeDefined();
+ });
+
+ it('should have initialContext', () => {
+ expect(VCMetamodel.initialContext).toBeDefined();
+ });
+
+ it('should have events', () => {
+ expect(VCMetamodel.events).toBeDefined();
+ });
+ });
+
+ describe('Initial Context', () => {
+ const initialContext = VCMetamodel.initialContext;
+
+ it('should have serviceRefs as empty object', () => {
+ expect(initialContext.serviceRefs).toEqual({});
+ expect(typeof initialContext.serviceRefs).toBe('object');
+ });
+
+ it('should have myVcsMetadata as empty array', () => {
+ expect(initialContext.myVcsMetadata).toEqual([]);
+ expect(Array.isArray(initialContext.myVcsMetadata)).toBe(true);
+ expect(initialContext.myVcsMetadata).toHaveLength(0);
+ });
+
+ it('should have receivedVcsMetadata as empty array', () => {
+ expect(initialContext.receivedVcsMetadata).toEqual([]);
+ expect(Array.isArray(initialContext.receivedVcsMetadata)).toBe(true);
+ expect(initialContext.receivedVcsMetadata).toHaveLength(0);
+ });
+
+ it('should have myVcs as empty object', () => {
+ expect(initialContext.myVcs).toEqual({});
+ expect(typeof initialContext.myVcs).toBe('object');
+ expect(Object.keys(initialContext.myVcs)).toHaveLength(0);
+ });
+
+ it('should have receivedVcs as empty object', () => {
+ expect(initialContext.receivedVcs).toEqual({});
+ expect(typeof initialContext.receivedVcs).toBe('object');
+ expect(Object.keys(initialContext.receivedVcs)).toHaveLength(0);
+ });
+
+ it('should have inProgressVcDownloads as empty Set', () => {
+ expect(initialContext.inProgressVcDownloads).toBeInstanceOf(Set);
+ expect(initialContext.inProgressVcDownloads.size).toBe(0);
+ });
+
+ it('should have areAllVcsDownloaded as false', () => {
+ expect(initialContext.areAllVcsDownloaded).toBe(false);
+ expect(typeof initialContext.areAllVcsDownloaded).toBe('boolean');
+ });
+
+ it('should have walletBindingSuccess as false', () => {
+ expect(initialContext.walletBindingSuccess).toBe(false);
+ expect(typeof initialContext.walletBindingSuccess).toBe('boolean');
+ });
+
+ it('should have tamperedVcs as empty array', () => {
+ expect(initialContext.tamperedVcs).toEqual([]);
+ expect(Array.isArray(initialContext.tamperedVcs)).toBe(true);
+ expect(initialContext.tamperedVcs).toHaveLength(0);
+ });
+
+ it('should have downloadingFailedVcs as empty array', () => {
+ expect(initialContext.downloadingFailedVcs).toEqual([]);
+ expect(Array.isArray(initialContext.downloadingFailedVcs)).toBe(true);
+ expect(initialContext.downloadingFailedVcs).toHaveLength(0);
+ });
+
+ it('should have verificationErrorMessage as empty string', () => {
+ expect(initialContext.verificationErrorMessage).toBe('');
+ expect(typeof initialContext.verificationErrorMessage).toBe('string');
+ });
+
+ it('should have verificationStatus as null', () => {
+ expect(initialContext.verificationStatus).toBeNull();
+ });
+
+ it('should have DownloadingCredentialsFailed as false', () => {
+ expect(initialContext.DownloadingCredentialsFailed).toBe(false);
+ expect(typeof initialContext.DownloadingCredentialsFailed).toBe(
+ 'boolean',
+ );
+ });
+
+ it('should have DownloadingCredentialsSuccess as false', () => {
+ expect(initialContext.DownloadingCredentialsSuccess).toBe(false);
+ expect(typeof initialContext.DownloadingCredentialsSuccess).toBe(
+ 'boolean',
+ );
+ });
+
+ it('should have all required properties', () => {
+ expect(initialContext).toHaveProperty('serviceRefs');
+ expect(initialContext).toHaveProperty('myVcsMetadata');
+ expect(initialContext).toHaveProperty('receivedVcsMetadata');
+ expect(initialContext).toHaveProperty('myVcs');
+ expect(initialContext).toHaveProperty('receivedVcs');
+ expect(initialContext).toHaveProperty('inProgressVcDownloads');
+ expect(initialContext).toHaveProperty('areAllVcsDownloaded');
+ expect(initialContext).toHaveProperty('walletBindingSuccess');
+ expect(initialContext).toHaveProperty('tamperedVcs');
+ expect(initialContext).toHaveProperty('downloadingFailedVcs');
+ expect(initialContext).toHaveProperty('verificationErrorMessage');
+ expect(initialContext).toHaveProperty('verificationStatus');
+ expect(initialContext).toHaveProperty('DownloadingCredentialsFailed');
+ expect(initialContext).toHaveProperty('DownloadingCredentialsSuccess');
+ });
+
+ it('should have exactly 16 properties in initial context', () => {
+ const propertyCount = Object.keys(initialContext).length;
+ expect(propertyCount).toBe(16);
+ });
+ });
+
+ describe('Model events', () => {
+ it('should have events object defined', () => {
+ expect(VCMetamodel.events).toBeDefined();
+ expect(typeof VCMetamodel.events).toBe('object');
+ });
+
+ it('should have non-empty events', () => {
+ const eventKeys = Object.keys(VCMetamodel.events);
+ expect(eventKeys.length).toBeGreaterThan(0);
+ });
+ });
+
+ describe('Type validation', () => {
+ const context = VCMetamodel.initialContext;
+
+ it('myVcsMetadata should accept VCMetadata array', () => {
+ expect(() => {
+ const metadata: typeof context.myVcsMetadata = [];
+ expect(Array.isArray(metadata)).toBe(true);
+ }).not.toThrow();
+ });
+
+ it('myVcs should accept Record', () => {
+ expect(() => {
+ const vcs: typeof context.myVcs = {};
+ expect(typeof vcs).toBe('object');
+ }).not.toThrow();
+ });
+
+ it('inProgressVcDownloads should be a Set', () => {
+ expect(context.inProgressVcDownloads).toBeInstanceOf(Set);
+ expect(context.inProgressVcDownloads.constructor.name).toBe('Set');
+ });
+ });
+
+ describe('Boolean flags', () => {
+ const context = VCMetamodel.initialContext;
+
+ it('all boolean flags should be false initially', () => {
+ const booleanFlags = [
+ context.areAllVcsDownloaded,
+ context.walletBindingSuccess,
+ context.DownloadingCredentialsFailed,
+ context.DownloadingCredentialsSuccess,
+ ];
+
+ booleanFlags.forEach(flag => {
+ expect(flag).toBe(false);
+ });
+ });
+ });
+
+ describe('Array properties', () => {
+ const context = VCMetamodel.initialContext;
+
+ it('all array properties should be empty initially', () => {
+ const arrays = [
+ context.myVcsMetadata,
+ context.receivedVcsMetadata,
+ context.tamperedVcs,
+ context.downloadingFailedVcs,
+ ];
+
+ arrays.forEach(arr => {
+ expect(Array.isArray(arr)).toBe(true);
+ expect(arr).toHaveLength(0);
+ });
+ });
+ });
+
+ describe('Object properties', () => {
+ const context = VCMetamodel.initialContext;
+
+ it('all object properties should be empty initially', () => {
+ const objects = [context.serviceRefs, context.myVcs, context.receivedVcs];
+
+ objects.forEach(obj => {
+ expect(typeof obj).toBe('object');
+ expect(Object.keys(obj)).toHaveLength(0);
+ });
+ });
+ });
+
+ describe('Null/undefined properties', () => {
+ const context = VCMetamodel.initialContext;
+
+ it('verificationStatus should be null', () => {
+ expect(context.verificationStatus).toBeNull();
+ expect(context.verificationStatus).not.toBeUndefined();
+ });
+
+ it('verificationErrorMessage should be empty string, not null', () => {
+ expect(context.verificationErrorMessage).not.toBeNull();
+ expect(context.verificationErrorMessage).toBe('');
+ });
+ });
+});
diff --git a/machines/VerifiableCredential/VCMetaMachine/VCMetaSelectors.test.ts b/machines/VerifiableCredential/VCMetaMachine/VCMetaSelectors.test.ts
new file mode 100644
index 00000000..51bc2f41
--- /dev/null
+++ b/machines/VerifiableCredential/VCMetaMachine/VCMetaSelectors.test.ts
@@ -0,0 +1,510 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import {
+ selectVerificationStatus,
+ selectMyVcsMetadata,
+ selectShareableVcsMetadata,
+ selectShareableVcs,
+ selectReceivedVcsMetadata,
+ selectIsRefreshingMyVcs,
+ selectIsRefreshingReceivedVcs,
+ selectAreAllVcsDownloaded,
+ selectBindedVcsMetadata,
+ selectInProgressVcDownloads,
+ selectWalletBindingSuccess,
+ selectIsTampered,
+ selectDownloadingFailedVcs,
+ selectMyVcs,
+ selectVerificationErrorMessage,
+ selectIsDownloadingFailed,
+ selectIsDownloadingSuccess,
+} from './VCMetaSelectors';
+import {VCMetadata} from '../../../shared/VCMetadata';
+
+describe('VCMetaSelectors', () => {
+ const mockVcMetadata1 = new VCMetadata({
+ id: 'vc1',
+ idType: 'NationalID',
+ issuer: 'Test Issuer 1',
+ });
+
+ const mockVcMetadata2 = new VCMetadata({
+ id: 'vc2',
+ idType: 'Passport',
+ issuer: 'Test Issuer 2',
+ });
+
+ const mockVcMetadata3 = new VCMetadata({
+ id: 'vc3',
+ idType: 'DriversLicense',
+ issuer: 'Test Issuer 3',
+ });
+
+ const mockVc1 = {
+ verifiableCredential: {credential: {id: 'cred1'}},
+ walletBindingResponse: null,
+ };
+
+ const mockVc2 = {
+ verifiableCredential: {credential: {id: 'cred2'}},
+ walletBindingResponse: {walletBindingId: 'binding123'},
+ };
+
+ const mockVc3 = {
+ verifiableCredential: null,
+ walletBindingResponse: null,
+ };
+
+ const mockState: any = {
+ context: {
+ verificationStatus: 'verified',
+ myVcsMetadata: [mockVcMetadata1, mockVcMetadata2, mockVcMetadata3],
+ receivedVcsMetadata: [mockVcMetadata1],
+ myVcs: {
+ [mockVcMetadata1.getVcKey()]: mockVc1,
+ [mockVcMetadata2.getVcKey()]: mockVc2,
+ [mockVcMetadata3.getVcKey()]: mockVc3,
+ },
+ areAllVcsDownloaded: true,
+ inProgressVcDownloads: [],
+ walletBindingSuccess: false,
+ downloadingFailedVcs: [],
+ },
+ matches: jest.fn((stateName: string) => stateName === 'ready.myVcs'),
+ };
+
+ describe('selectVerificationStatus', () => {
+ it('should return verification status from context', () => {
+ const result = selectVerificationStatus(mockState);
+ expect(result).toBe('verified');
+ });
+
+ it('should handle different status values', () => {
+ const statuses = ['verified', 'pending', 'failed', 'invalid'];
+ statuses.forEach(status => {
+ const state: any = {
+ ...mockState,
+ context: {...mockState.context, verificationStatus: status},
+ };
+ expect(selectVerificationStatus(state)).toBe(status);
+ });
+ });
+ });
+
+ describe('selectMyVcsMetadata', () => {
+ it('should return all VCs metadata from context', () => {
+ const result = selectMyVcsMetadata(mockState);
+ expect(result).toHaveLength(3);
+ expect(result).toEqual([
+ mockVcMetadata1,
+ mockVcMetadata2,
+ mockVcMetadata3,
+ ]);
+ });
+
+ it('should return empty array when no VCs', () => {
+ const state: any = {
+ ...mockState,
+ context: {...mockState.context, myVcsMetadata: []},
+ };
+ const result = selectMyVcsMetadata(state);
+ expect(result).toEqual([]);
+ });
+ });
+
+ describe('selectShareableVcsMetadata', () => {
+ it('should filter VCs that have verifiableCredential', () => {
+ const result = selectShareableVcsMetadata(mockState);
+ expect(result).toHaveLength(2);
+ expect(result).toContain(mockVcMetadata1);
+ expect(result).toContain(mockVcMetadata2);
+ expect(result).not.toContain(mockVcMetadata3);
+ });
+
+ it('should return empty array when no shareable VCs', () => {
+ const state: any = {
+ ...mockState,
+ context: {
+ ...mockState.context,
+ myVcs: {
+ [mockVcMetadata1.getVcKey()]: {verifiableCredential: null},
+ [mockVcMetadata2.getVcKey()]: {verifiableCredential: null},
+ },
+ },
+ };
+ const result = selectShareableVcsMetadata(state);
+ expect(result).toEqual([]);
+ });
+ });
+
+ describe('selectShareableVcs', () => {
+ it('should filter VCs that have verifiableCredential', () => {
+ const result = selectShareableVcs(mockState);
+ expect(result).toHaveLength(2);
+ expect(result).toContainEqual(mockVc1);
+ expect(result).toContainEqual(mockVc2);
+ expect(result).not.toContainEqual(mockVc3);
+ });
+
+ it('should return empty array when no VCs have verifiableCredential', () => {
+ const state: any = {
+ ...mockState,
+ context: {
+ ...mockState.context,
+ myVcs: {
+ vc1: {verifiableCredential: null},
+ vc2: {verifiableCredential: null},
+ },
+ },
+ };
+ const result = selectShareableVcs(state);
+ expect(result).toEqual([]);
+ });
+ });
+
+ describe('selectReceivedVcsMetadata', () => {
+ it('should return received VCs metadata', () => {
+ const result = selectReceivedVcsMetadata(mockState);
+ expect(result).toHaveLength(1);
+ expect(result).toContain(mockVcMetadata1);
+ });
+
+ it('should return empty array when no received VCs', () => {
+ const state: any = {
+ ...mockState,
+ context: {...mockState.context, receivedVcsMetadata: []},
+ };
+ const result = selectReceivedVcsMetadata(state);
+ expect(result).toEqual([]);
+ });
+ });
+
+ describe('selectIsRefreshingMyVcs', () => {
+ it('should return true when in ready.myVcs state', () => {
+ const result = selectIsRefreshingMyVcs(mockState);
+ expect(result).toBe(true);
+ });
+
+ it('should call matches with ready.myVcs', () => {
+ selectIsRefreshingMyVcs(mockState);
+ expect(mockState.matches).toHaveBeenCalledWith('ready.myVcs');
+ });
+
+ it('should return false when not in ready.myVcs state', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn(() => false),
+ };
+ const result = selectIsRefreshingMyVcs(state);
+ expect(result).toBe(false);
+ });
+ });
+
+ describe('selectIsRefreshingReceivedVcs', () => {
+ it('should return true when in ready.receivedVcs state', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'ready.receivedVcs'),
+ };
+ const result = selectIsRefreshingReceivedVcs(state);
+ expect(result).toBe(true);
+ });
+
+ it('should call matches with ready.receivedVcs', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn(() => false),
+ };
+ selectIsRefreshingReceivedVcs(state);
+ expect(state.matches).toHaveBeenCalledWith('ready.receivedVcs');
+ });
+ });
+
+ describe('selectAreAllVcsDownloaded', () => {
+ it('should return true when all VCs are downloaded', () => {
+ const result = selectAreAllVcsDownloaded(mockState);
+ expect(result).toBe(true);
+ });
+
+ it('should return false when not all VCs are downloaded', () => {
+ const state: any = {
+ ...mockState,
+ context: {...mockState.context, areAllVcsDownloaded: false},
+ };
+ const result = selectAreAllVcsDownloaded(state);
+ expect(result).toBe(false);
+ });
+ });
+
+ describe('selectBindedVcsMetadata', () => {
+ it('should return VCs with wallet binding', () => {
+ const result = selectBindedVcsMetadata(mockState);
+ expect(result).toHaveLength(1);
+ expect(result).toContain(mockVcMetadata2);
+ expect(result).not.toContain(mockVcMetadata1);
+ expect(result).not.toContain(mockVcMetadata3);
+ });
+
+ it('should filter out VCs with null walletBindingResponse', () => {
+ const state: any = {
+ ...mockState,
+ context: {
+ ...mockState.context,
+ myVcs: {
+ [mockVcMetadata1.getVcKey()]: {walletBindingResponse: null},
+ [mockVcMetadata2.getVcKey()]: {walletBindingResponse: {}},
+ },
+ },
+ };
+ const result = selectBindedVcsMetadata(state);
+ expect(result).toEqual([]);
+ });
+
+ it('should filter out VCs with empty walletBindingId', () => {
+ const state: any = {
+ ...mockState,
+ context: {
+ ...mockState.context,
+ myVcs: {
+ [mockVcMetadata1.getVcKey()]: {
+ walletBindingResponse: {walletBindingId: ''},
+ },
+ [mockVcMetadata2.getVcKey()]: {
+ walletBindingResponse: {walletBindingId: null},
+ },
+ },
+ },
+ };
+ const result = selectBindedVcsMetadata(state);
+ expect(result).toEqual([]);
+ });
+
+ it('should return empty array when no binded VCs', () => {
+ const state: any = {
+ ...mockState,
+ context: {
+ ...mockState.context,
+ myVcs: {
+ [mockVcMetadata1.getVcKey()]: {walletBindingResponse: null},
+ },
+ },
+ };
+ const result = selectBindedVcsMetadata(state);
+ expect(result).toEqual([]);
+ });
+ });
+
+ describe('selectInProgressVcDownloads', () => {
+ it('should return in-progress VC downloads', () => {
+ const downloads = ['vc1', 'vc2'];
+ const state: any = {
+ ...mockState,
+ context: {...mockState.context, inProgressVcDownloads: downloads},
+ };
+ const result = selectInProgressVcDownloads(state);
+ expect(result).toEqual(downloads);
+ });
+
+ it('should return empty array when no downloads in progress', () => {
+ const result = selectInProgressVcDownloads(mockState);
+ expect(result).toEqual([]);
+ });
+ });
+
+ describe('selectWalletBindingSuccess', () => {
+ it('should return wallet binding success status', () => {
+ const result = selectWalletBindingSuccess(mockState);
+ expect(result).toBe(false);
+ });
+
+ it('should return true when wallet binding is successful', () => {
+ const state: any = {
+ ...mockState,
+ context: {...mockState.context, walletBindingSuccess: true},
+ };
+ const result = selectWalletBindingSuccess(state);
+ expect(result).toBe(true);
+ });
+ });
+
+ describe('selectIsTampered', () => {
+ it('should return true when in ready.tamperedVCs state', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'ready.tamperedVCs'),
+ };
+ const result = selectIsTampered(state);
+ expect(result).toBe(true);
+ });
+
+ it('should call matches with ready.tamperedVCs', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn(() => false),
+ };
+ selectIsTampered(state);
+ expect(state.matches).toHaveBeenCalledWith('ready.tamperedVCs');
+ });
+
+ it('should return false when not in tampered state', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn(() => false),
+ };
+ const result = selectIsTampered(state);
+ expect(result).toBe(false);
+ });
+ });
+
+ describe('selectDownloadingFailedVcs', () => {
+ it('should return downloading failed VCs', () => {
+ const failedVcs = ['vc1', 'vc2'];
+ const state: any = {
+ ...mockState,
+ context: {...mockState.context, downloadingFailedVcs: failedVcs},
+ };
+ const result = selectDownloadingFailedVcs(state);
+ expect(result).toEqual(failedVcs);
+ });
+
+ it('should return empty array when no failed VCs', () => {
+ const result = selectDownloadingFailedVcs(mockState);
+ expect(result).toEqual([]);
+ });
+ });
+
+ describe('selectMyVcs', () => {
+ it('should return all my VCs object', () => {
+ const result = selectMyVcs(mockState);
+ expect(result).toEqual(mockState.context.myVcs);
+ });
+
+ it('should return object with VC keys', () => {
+ const result = selectMyVcs(mockState);
+ expect(result).toHaveProperty(mockVcMetadata1.getVcKey());
+ expect(result).toHaveProperty(mockVcMetadata2.getVcKey());
+ });
+
+ it('should return empty object when no VCs', () => {
+ const state: any = {
+ ...mockState,
+ context: {...mockState.context, myVcs: {}},
+ };
+ const result = selectMyVcs(state);
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('Edge cases and boundary conditions', () => {
+ it('should handle undefined values in walletBindingResponse', () => {
+ const state: any = {
+ ...mockState,
+ context: {
+ ...mockState.context,
+ myVcs: {
+ [mockVcMetadata1.getVcKey()]: {
+ walletBindingResponse: undefined,
+ },
+ },
+ },
+ };
+ const result = selectBindedVcsMetadata(state);
+ expect(result).toEqual([]);
+ });
+
+ it('should handle empty string in walletBindingId', () => {
+ const state: any = {
+ ...mockState,
+ context: {
+ ...mockState.context,
+ myVcs: {
+ [mockVcMetadata1.getVcKey()]: {
+ walletBindingResponse: {walletBindingId: ''},
+ },
+ },
+ },
+ };
+ const result = selectBindedVcsMetadata(state);
+ expect(result).toEqual([]);
+ });
+
+ it('should handle null verifiableCredential in filtering', () => {
+ const state: any = {
+ ...mockState,
+ context: {
+ ...mockState.context,
+ myVcs: {
+ [mockVcMetadata1.getVcKey()]: {verifiableCredential: null},
+ },
+ },
+ };
+ const result = selectShareableVcsMetadata(state);
+ expect(result).toEqual([]);
+ });
+ });
+
+ describe('selectVerificationErrorMessage', () => {
+ it('should return verification error message from context', () => {
+ const state: any = {
+ context: {
+ verificationErrorMessage: 'Invalid signature',
+ },
+ };
+ const result = selectVerificationErrorMessage(state);
+ expect(result).toBe('Invalid signature');
+ });
+
+ it('should return empty string when no error', () => {
+ const state: any = {
+ context: {
+ verificationErrorMessage: '',
+ },
+ };
+ const result = selectVerificationErrorMessage(state);
+ expect(result).toBe('');
+ });
+ });
+
+ describe('selectIsDownloadingFailed', () => {
+ it('should return DownloadingCredentialsFailed status', () => {
+ const state: any = {
+ context: {
+ DownloadingCredentialsFailed: true,
+ },
+ };
+ const result = selectIsDownloadingFailed(state);
+ expect(result).toBe(true);
+ });
+
+ it('should return false when downloading not failed', () => {
+ const state: any = {
+ context: {
+ DownloadingCredentialsFailed: false,
+ },
+ };
+ const result = selectIsDownloadingFailed(state);
+ expect(result).toBe(false);
+ });
+ });
+
+ describe('selectIsDownloadingSuccess', () => {
+ it('should return DownloadingCredentialsSuccess status', () => {
+ const state: any = {
+ context: {
+ DownloadingCredentialsSuccess: true,
+ },
+ };
+ const result = selectIsDownloadingSuccess(state);
+ expect(result).toBe(true);
+ });
+
+ it('should return false when downloading not successful', () => {
+ const state: any = {
+ context: {
+ DownloadingCredentialsSuccess: false,
+ },
+ };
+ const result = selectIsDownloadingSuccess(state);
+ expect(result).toBe(false);
+ });
+ });
+});
diff --git a/machines/backupAndRestore/backup/backupModel.test.ts b/machines/backupAndRestore/backup/backupModel.test.ts
new file mode 100644
index 00000000..5c55e115
--- /dev/null
+++ b/machines/backupAndRestore/backup/backupModel.test.ts
@@ -0,0 +1,151 @@
+import {backupModel} from './backupModel';
+
+describe('backupModel', () => {
+ describe('Model structure', () => {
+ it('should be defined', () => {
+ expect(backupModel).toBeDefined();
+ });
+
+ it('should have initialContext', () => {
+ expect(backupModel.initialContext).toBeDefined();
+ });
+
+ it('should have events', () => {
+ expect(backupModel.events).toBeDefined();
+ });
+ });
+
+ describe('Initial Context', () => {
+ const initialContext = backupModel.initialContext;
+
+ it('should have serviceRefs as empty object', () => {
+ expect(initialContext.serviceRefs).toEqual({});
+ expect(typeof initialContext.serviceRefs).toBe('object');
+ });
+
+ it('should have dataFromStorage as empty object', () => {
+ expect(initialContext.dataFromStorage).toEqual({});
+ expect(typeof initialContext.dataFromStorage).toBe('object');
+ });
+
+ it('should have fileName as empty string', () => {
+ expect(initialContext.fileName).toBe('');
+ expect(typeof initialContext.fileName).toBe('string');
+ });
+
+ it('should have lastBackupDetails as null', () => {
+ expect(initialContext.lastBackupDetails).toBeNull();
+ });
+
+ it('should have errorReason as empty string', () => {
+ expect(initialContext.errorReason).toBe('');
+ expect(typeof initialContext.errorReason).toBe('string');
+ });
+
+ it('should have isAutoBackUp as true', () => {
+ expect(initialContext.isAutoBackUp).toBe(true);
+ expect(typeof initialContext.isAutoBackUp).toBe('boolean');
+ });
+
+ it('should have isLoadingBackupDetails as true', () => {
+ expect(initialContext.isLoadingBackupDetails).toBe(true);
+ expect(typeof initialContext.isLoadingBackupDetails).toBe('boolean');
+ });
+
+ it('should have showBackupInProgress as false', () => {
+ expect(initialContext.showBackupInProgress).toBe(false);
+ expect(typeof initialContext.showBackupInProgress).toBe('boolean');
+ });
+
+ it('should have all 8 required properties', () => {
+ const properties = Object.keys(initialContext);
+ expect(properties).toHaveLength(8);
+ });
+ });
+
+ describe('String properties', () => {
+ const context = backupModel.initialContext;
+
+ it('all empty string properties should be empty', () => {
+ const emptyStrings = [context.fileName, context.errorReason];
+
+ emptyStrings.forEach(str => {
+ expect(str).toBe('');
+ expect(typeof str).toBe('string');
+ });
+ });
+ });
+
+ describe('Object properties', () => {
+ const context = backupModel.initialContext;
+
+ it('all empty object properties should be empty objects', () => {
+ const emptyObjects = [context.serviceRefs, context.dataFromStorage];
+
+ emptyObjects.forEach(obj => {
+ expect(typeof obj).toBe('object');
+ expect(Object.keys(obj)).toHaveLength(0);
+ });
+ });
+ });
+
+ describe('Boolean properties', () => {
+ const context = backupModel.initialContext;
+
+ it('isAutoBackUp should be true', () => {
+ expect(context.isAutoBackUp).toBe(true);
+ expect(typeof context.isAutoBackUp).toBe('boolean');
+ });
+
+ it('isLoadingBackupDetails should be true', () => {
+ expect(context.isLoadingBackupDetails).toBe(true);
+ expect(typeof context.isLoadingBackupDetails).toBe('boolean');
+ });
+
+ it('showBackupInProgress should be false', () => {
+ expect(context.showBackupInProgress).toBe(false);
+ expect(typeof context.showBackupInProgress).toBe('boolean');
+ });
+
+ it('should have correct initial values for boolean properties', () => {
+ expect(context.isAutoBackUp).toBe(true);
+ expect(context.isLoadingBackupDetails).toBe(true);
+ expect(context.showBackupInProgress).toBe(false);
+ });
+ });
+
+ describe('Null properties', () => {
+ const context = backupModel.initialContext;
+
+ it('lastBackupDetails should be null', () => {
+ expect(context.lastBackupDetails).toBeNull();
+ });
+ });
+
+ describe('Model events', () => {
+ it('should have events object', () => {
+ expect(backupModel.events).toBeDefined();
+ expect(typeof backupModel.events).toBe('object');
+ });
+
+ it('should have event creators', () => {
+ const eventKeys = Object.keys(backupModel.events);
+ expect(eventKeys.length).toBeGreaterThan(0);
+ });
+ });
+
+ describe('Property types validation', () => {
+ const context = backupModel.initialContext;
+
+ it('should have correct types for all properties', () => {
+ expect(typeof context.serviceRefs).toBe('object');
+ expect(typeof context.dataFromStorage).toBe('object');
+ expect(typeof context.fileName).toBe('string');
+ expect(context.lastBackupDetails).toBeNull();
+ expect(typeof context.errorReason).toBe('string');
+ expect(typeof context.isAutoBackUp).toBe('boolean');
+ expect(typeof context.isLoadingBackupDetails).toBe('boolean');
+ expect(typeof context.showBackupInProgress).toBe('boolean');
+ });
+ });
+});
diff --git a/machines/backupAndRestore/backup/backupSelector.test.ts b/machines/backupAndRestore/backup/backupSelector.test.ts
new file mode 100644
index 00000000..6a81cdb6
--- /dev/null
+++ b/machines/backupAndRestore/backup/backupSelector.test.ts
@@ -0,0 +1,214 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import {
+ selectIsBackupInprogress,
+ selectIsLoadingBackupDetails,
+ selectIsBackingUpSuccess,
+ selectIsBackingUpFailure,
+ selectIsNetworkError,
+ lastBackupDetails,
+ selectBackupErrorReason,
+ selectShowBackupInProgress,
+} from './backupSelector';
+
+describe('backupSelector', () => {
+ describe('selectIsBackupInprogress', () => {
+ it('should return true when in checkDataAvailabilityForBackup state', () => {
+ const mockState: any = {
+ matches: jest.fn(
+ (state: string) =>
+ state === 'backingUp.checkDataAvailabilityForBackup',
+ ),
+ };
+ expect(selectIsBackupInprogress(mockState)).toBe(true);
+ });
+
+ it('should return true when in checkStorageAvailability state', () => {
+ const mockState: any = {
+ matches: jest.fn(
+ (state: string) => state === 'backingUp.checkStorageAvailability',
+ ),
+ };
+ expect(selectIsBackupInprogress(mockState)).toBe(true);
+ });
+
+ it('should return true when in fetchDataFromDB state', () => {
+ const mockState: any = {
+ matches: jest.fn(
+ (state: string) => state === 'backingUp.fetchDataFromDB',
+ ),
+ };
+ expect(selectIsBackupInprogress(mockState)).toBe(true);
+ });
+
+ it('should return true when in writeDataToFile state', () => {
+ const mockState: any = {
+ matches: jest.fn(
+ (state: string) => state === 'backingUp.writeDataToFile',
+ ),
+ };
+ expect(selectIsBackupInprogress(mockState)).toBe(true);
+ });
+
+ it('should return true when in zipBackupFile state', () => {
+ const mockState: any = {
+ matches: jest.fn(
+ (state: string) => state === 'backingUp.zipBackupFile',
+ ),
+ };
+ expect(selectIsBackupInprogress(mockState)).toBe(true);
+ });
+
+ it('should return true when in uploadBackupFile state', () => {
+ const mockState: any = {
+ matches: jest.fn(
+ (state: string) => state === 'backingUp.uploadBackupFile',
+ ),
+ };
+ expect(selectIsBackupInprogress(mockState)).toBe(true);
+ });
+
+ it('should return false when not in any backup progress state', () => {
+ const mockState: any = {
+ matches: jest.fn(() => false),
+ };
+ expect(selectIsBackupInprogress(mockState)).toBe(false);
+ });
+ });
+
+ describe('selectIsLoadingBackupDetails', () => {
+ it('should return isLoadingBackupDetails from context', () => {
+ const mockState: any = {
+ context: {
+ isLoadingBackupDetails: true,
+ },
+ };
+ expect(selectIsLoadingBackupDetails(mockState)).toBe(true);
+ });
+
+ it('should return false when isLoadingBackupDetails is false', () => {
+ const mockState: any = {
+ context: {
+ isLoadingBackupDetails: false,
+ },
+ };
+ expect(selectIsLoadingBackupDetails(mockState)).toBe(false);
+ });
+ });
+
+ describe('selectIsBackingUpSuccess', () => {
+ it('should return true when in backingUp.success state', () => {
+ const mockState: any = {
+ matches: jest.fn((state: string) => state === 'backingUp.success'),
+ };
+ expect(selectIsBackingUpSuccess(mockState)).toBe(true);
+ });
+
+ it('should return false when not in success state', () => {
+ const mockState: any = {
+ matches: jest.fn(() => false),
+ };
+ expect(selectIsBackingUpSuccess(mockState)).toBe(false);
+ });
+ });
+
+ describe('selectIsBackingUpFailure', () => {
+ it('should return true when in backingUp.failure state', () => {
+ const mockState: any = {
+ matches: jest.fn((state: string) => state === 'backingUp.failure'),
+ };
+ expect(selectIsBackingUpFailure(mockState)).toBe(true);
+ });
+
+ it('should return false when not in failure state', () => {
+ const mockState: any = {
+ matches: jest.fn(() => false),
+ };
+ expect(selectIsBackingUpFailure(mockState)).toBe(false);
+ });
+ });
+
+ describe('selectIsNetworkError', () => {
+ it('should return true when in fetchLastBackupDetails.noInternet state', () => {
+ const mockState: any = {
+ matches: jest.fn(
+ (state: string) => state === 'fetchLastBackupDetails.noInternet',
+ ),
+ };
+ expect(selectIsNetworkError(mockState)).toBe(true);
+ });
+
+ it('should return false when not in network error state', () => {
+ const mockState: any = {
+ matches: jest.fn(() => false),
+ };
+ expect(selectIsNetworkError(mockState)).toBe(false);
+ });
+ });
+
+ describe('lastBackupDetails', () => {
+ it('should return lastBackupDetails from context', () => {
+ const mockDetails = {
+ timestamp: '2024-01-01',
+ size: '10MB',
+ fileName: 'backup_123.zip',
+ };
+ const mockState: any = {
+ context: {
+ lastBackupDetails: mockDetails,
+ },
+ };
+ expect(lastBackupDetails(mockState)).toBe(mockDetails);
+ });
+
+ it('should return undefined when lastBackupDetails is not set', () => {
+ const mockState: any = {
+ context: {
+ lastBackupDetails: undefined,
+ },
+ };
+ expect(lastBackupDetails(mockState)).toBeUndefined();
+ });
+ });
+
+ describe('selectBackupErrorReason', () => {
+ it('should return errorReason from context', () => {
+ const mockState: any = {
+ context: {
+ errorReason: 'Insufficient storage space',
+ },
+ };
+ expect(selectBackupErrorReason(mockState)).toBe(
+ 'Insufficient storage space',
+ );
+ });
+
+ it('should return null when no error', () => {
+ const mockState: any = {
+ context: {
+ errorReason: null,
+ },
+ };
+ expect(selectBackupErrorReason(mockState)).toBeNull();
+ });
+ });
+
+ describe('selectShowBackupInProgress', () => {
+ it('should return showBackupInProgress from context', () => {
+ const mockState: any = {
+ context: {
+ showBackupInProgress: true,
+ },
+ };
+ expect(selectShowBackupInProgress(mockState)).toBe(true);
+ });
+
+ it('should return false when showBackupInProgress is false', () => {
+ const mockState: any = {
+ context: {
+ showBackupInProgress: false,
+ },
+ };
+ expect(selectShowBackupInProgress(mockState)).toBe(false);
+ });
+ });
+});
diff --git a/machines/backupAndRestore/backupAndRestoreSetup/backupAndRestoreSetupModel.test.ts b/machines/backupAndRestore/backupAndRestoreSetup/backupAndRestoreSetupModel.test.ts
new file mode 100644
index 00000000..7f6fa0b7
--- /dev/null
+++ b/machines/backupAndRestore/backupAndRestoreSetup/backupAndRestoreSetupModel.test.ts
@@ -0,0 +1,138 @@
+import {backupAndRestoreSetupModel} from './backupAndRestoreSetupModel';
+
+describe('backupAndRestoreSetupModel', () => {
+ describe('Model structure', () => {
+ it('should be defined', () => {
+ expect(backupAndRestoreSetupModel).toBeDefined();
+ });
+
+ it('should have initialContext', () => {
+ expect(backupAndRestoreSetupModel.initialContext).toBeDefined();
+ });
+
+ it('should have events', () => {
+ expect(backupAndRestoreSetupModel.events).toBeDefined();
+ });
+ });
+
+ describe('Initial Context', () => {
+ const initialContext = backupAndRestoreSetupModel.initialContext;
+
+ it('should have isLoading as false', () => {
+ expect(initialContext.isLoading).toBe(false);
+ expect(typeof initialContext.isLoading).toBe('boolean');
+ });
+
+ it('should have profileInfo as undefined', () => {
+ expect(initialContext.profileInfo).toBeUndefined();
+ });
+
+ it('should have errorMessage as empty string', () => {
+ expect(initialContext.errorMessage).toBe('');
+ expect(typeof initialContext.errorMessage).toBe('string');
+ });
+
+ it('should have serviceRefs as empty object', () => {
+ expect(initialContext.serviceRefs).toEqual({});
+ expect(typeof initialContext.serviceRefs).toBe('object');
+ });
+
+ it('should have shouldTriggerAutoBackup as false', () => {
+ expect(initialContext.shouldTriggerAutoBackup).toBe(false);
+ expect(typeof initialContext.shouldTriggerAutoBackup).toBe('boolean');
+ });
+
+ it('should have isCloudSignedIn as false', () => {
+ expect(initialContext.isCloudSignedIn).toBe(false);
+ expect(typeof initialContext.isCloudSignedIn).toBe('boolean');
+ });
+
+ it('should have all 6 required properties', () => {
+ const properties = Object.keys(initialContext);
+ expect(properties).toHaveLength(6);
+ });
+ });
+
+ describe('String properties', () => {
+ const context = backupAndRestoreSetupModel.initialContext;
+
+ it('errorMessage should be empty', () => {
+ expect(context.errorMessage).toBe('');
+ expect(typeof context.errorMessage).toBe('string');
+ });
+ });
+
+ describe('Object properties', () => {
+ const context = backupAndRestoreSetupModel.initialContext;
+
+ it('serviceRefs should be empty object', () => {
+ expect(typeof context.serviceRefs).toBe('object');
+ expect(Object.keys(context.serviceRefs)).toHaveLength(0);
+ });
+ });
+
+ describe('Boolean properties', () => {
+ const context = backupAndRestoreSetupModel.initialContext;
+
+ it('isLoading should be false', () => {
+ expect(context.isLoading).toBe(false);
+ expect(typeof context.isLoading).toBe('boolean');
+ });
+
+ it('shouldTriggerAutoBackup should be false', () => {
+ expect(context.shouldTriggerAutoBackup).toBe(false);
+ expect(typeof context.shouldTriggerAutoBackup).toBe('boolean');
+ });
+
+ it('isCloudSignedIn should be false', () => {
+ expect(context.isCloudSignedIn).toBe(false);
+ expect(typeof context.isCloudSignedIn).toBe('boolean');
+ });
+
+ it('should have correct initial values for boolean properties', () => {
+ const falseProps = [
+ context.isLoading,
+ context.shouldTriggerAutoBackup,
+ context.isCloudSignedIn,
+ ];
+
+ falseProps.forEach(prop => {
+ expect(prop).toBe(false);
+ expect(typeof prop).toBe('boolean');
+ });
+ });
+ });
+
+ describe('Undefined properties', () => {
+ const context = backupAndRestoreSetupModel.initialContext;
+
+ it('profileInfo should be undefined', () => {
+ expect(context.profileInfo).toBeUndefined();
+ });
+ });
+
+ describe('Model events', () => {
+ it('should have events object', () => {
+ expect(backupAndRestoreSetupModel.events).toBeDefined();
+ expect(typeof backupAndRestoreSetupModel.events).toBe('object');
+ });
+
+ it('should have event creators', () => {
+ const eventKeys = Object.keys(backupAndRestoreSetupModel.events);
+ expect(eventKeys.length).toBeGreaterThan(0);
+ });
+ });
+
+ describe('Property types validation', () => {
+ const context = backupAndRestoreSetupModel.initialContext;
+
+ it('should have correct types for all properties', () => {
+ expect(typeof context.isLoading).toBe('boolean');
+ expect(context.profileInfo).toBeUndefined();
+ expect(typeof context.errorMessage).toBe('string');
+ expect(typeof context.serviceRefs).toBe('object');
+ expect(typeof context.shouldTriggerAutoBackup).toBe('boolean');
+ expect(typeof context.isCloudSignedIn).toBe('boolean');
+ });
+ });
+});
diff --git a/machines/backupAndRestore/backupAndRestoreSetup/backupAndRestoreSetupSelectors.test.ts b/machines/backupAndRestore/backupAndRestoreSetup/backupAndRestoreSetupSelectors.test.ts
new file mode 100644
index 00000000..b559e0a4
--- /dev/null
+++ b/machines/backupAndRestore/backupAndRestoreSetup/backupAndRestoreSetupSelectors.test.ts
@@ -0,0 +1,289 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import {
+ selectIsLoading,
+ selectProfileInfo,
+ selectIsNetworkError,
+ selectShouldTriggerAutoBackup,
+ selectShowAccountSelectionConfirmation,
+ selectIsSigningIn,
+ selectIsSigningInSuccessful,
+ selectIsSigningFailure,
+ selectIsCloudSignedInFailed,
+} from './backupAndRestoreSetupSelectors';
+
+describe('backupAndRestoreSetupSelectors', () => {
+ const mockProfileInfo = {
+ email: 'test@example.com',
+ name: 'Test User',
+ id: 'user123',
+ };
+
+ const mockState: any = {
+ context: {
+ isLoading: false,
+ profileInfo: mockProfileInfo,
+ shouldTriggerAutoBackup: true,
+ },
+ matches: jest.fn(() => false),
+ };
+
+ describe('selectIsLoading', () => {
+ it('should return loading status from context', () => {
+ const result = selectIsLoading(mockState);
+ expect(result).toBe(false);
+ });
+
+ it('should return true when loading', () => {
+ const state: any = {
+ ...mockState,
+ context: {...mockState.context, isLoading: true},
+ };
+ const result = selectIsLoading(state);
+ expect(result).toBe(true);
+ });
+ });
+
+ describe('selectProfileInfo', () => {
+ it('should return profile info from context', () => {
+ const result = selectProfileInfo(mockState);
+ expect(result).toEqual(mockProfileInfo);
+ });
+
+ it('should return profile with all properties', () => {
+ const result = selectProfileInfo(mockState);
+ expect(result).toHaveProperty('email');
+ expect(result).toHaveProperty('name');
+ expect(result).toHaveProperty('id');
+ });
+
+ it('should handle null profile info', () => {
+ const state: any = {
+ ...mockState,
+ context: {...mockState.context, profileInfo: null},
+ };
+ const result = selectProfileInfo(state);
+ expect(result).toBeNull();
+ });
+ });
+
+ describe('selectIsNetworkError', () => {
+ it('should return true when in init.noInternet state', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'init.noInternet'),
+ };
+ const result = selectIsNetworkError(state);
+ expect(result).toBe(true);
+ });
+
+ it('should return true when in checkSignIn.noInternet state', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'checkSignIn.noInternet'),
+ };
+ const result = selectIsNetworkError(state);
+ expect(result).toBe(true);
+ });
+
+ it('should return true when in signIn.noInternet state', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'signIn.noInternet'),
+ };
+ const result = selectIsNetworkError(state);
+ expect(result).toBe(true);
+ });
+
+ it('should return false when not in any noInternet state', () => {
+ const result = selectIsNetworkError(mockState);
+ expect(result).toBe(false);
+ });
+
+ it('should call matches with all three network error states', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn(() => false),
+ };
+ selectIsNetworkError(state);
+ expect(state.matches).toHaveBeenCalledWith('init.noInternet');
+ expect(state.matches).toHaveBeenCalledWith('checkSignIn.noInternet');
+ expect(state.matches).toHaveBeenCalledWith('signIn.noInternet');
+ });
+ });
+
+ describe('selectShouldTriggerAutoBackup', () => {
+ it('should return auto backup trigger flag', () => {
+ const result = selectShouldTriggerAutoBackup(mockState);
+ expect(result).toBe(true);
+ });
+
+ it('should return false when auto backup should not trigger', () => {
+ const state: any = {
+ ...mockState,
+ context: {...mockState.context, shouldTriggerAutoBackup: false},
+ };
+ const result = selectShouldTriggerAutoBackup(state);
+ expect(result).toBe(false);
+ });
+ });
+
+ describe('selectShowAccountSelectionConfirmation', () => {
+ it('should return true when in selectCloudAccount state', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'selectCloudAccount'),
+ };
+ const result = selectShowAccountSelectionConfirmation(state);
+ expect(result).toBe(true);
+ });
+
+ it('should call matches with selectCloudAccount', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn(() => false),
+ };
+ selectShowAccountSelectionConfirmation(state);
+ expect(state.matches).toHaveBeenCalledWith('selectCloudAccount');
+ });
+
+ it('should return false when not in selectCloudAccount state', () => {
+ const result = selectShowAccountSelectionConfirmation(mockState);
+ expect(result).toBe(false);
+ });
+ });
+
+ describe('selectIsSigningIn', () => {
+ it('should return true when in signIn state', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'signIn'),
+ };
+ const result = selectIsSigningIn(state);
+ expect(result).toBe(true);
+ });
+
+ it('should call matches with signIn', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn(() => false),
+ };
+ selectIsSigningIn(state);
+ expect(state.matches).toHaveBeenCalledWith('signIn');
+ });
+
+ it('should return false when not signing in', () => {
+ const result = selectIsSigningIn(mockState);
+ expect(result).toBe(false);
+ });
+ });
+
+ describe('selectIsSigningInSuccessful', () => {
+ it('should return true when in backupAndRestore state', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'backupAndRestore'),
+ };
+ const result = selectIsSigningInSuccessful(state);
+ expect(result).toBe(true);
+ });
+
+ it('should call matches with backupAndRestore', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn(() => false),
+ };
+ selectIsSigningInSuccessful(state);
+ expect(state.matches).toHaveBeenCalledWith('backupAndRestore');
+ });
+
+ it('should return false when sign in not successful', () => {
+ const result = selectIsSigningInSuccessful(mockState);
+ expect(result).toBe(false);
+ });
+ });
+
+ describe('selectIsSigningFailure', () => {
+ it('should return true when in signIn.error state', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'signIn.error'),
+ };
+ const result = selectIsSigningFailure(state);
+ expect(result).toBe(true);
+ });
+
+ it('should return true when in checkSignIn.error state', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'checkSignIn.error'),
+ };
+ const result = selectIsSigningFailure(state);
+ expect(result).toBe(true);
+ });
+
+ it('should return false when not in error state', () => {
+ const result = selectIsSigningFailure(mockState);
+ expect(result).toBe(false);
+ });
+
+ it('should call matches with both error states', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn(() => false),
+ };
+ selectIsSigningFailure(state);
+ expect(state.matches).toHaveBeenCalledWith('signIn.error');
+ expect(state.matches).toHaveBeenCalledWith('checkSignIn.error');
+ });
+ });
+
+ describe('selectIsCloudSignedInFailed', () => {
+ it('should return true when in checkSignIn.error state', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'checkSignIn.error'),
+ };
+ const result = selectIsCloudSignedInFailed(state);
+ expect(result).toBe(true);
+ });
+
+ it('should call matches with checkSignIn.error', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn(() => false),
+ };
+ selectIsCloudSignedInFailed(state);
+ expect(state.matches).toHaveBeenCalledWith('checkSignIn.error');
+ });
+
+ it('should return false when cloud sign in did not fail', () => {
+ const result = selectIsCloudSignedInFailed(mockState);
+ expect(result).toBe(false);
+ });
+ });
+
+ describe('Edge cases', () => {
+ it('should handle empty profile info', () => {
+ const state: any = {
+ ...mockState,
+ context: {...mockState.context, profileInfo: {}},
+ };
+ const result = selectProfileInfo(state);
+ expect(result).toEqual({});
+ });
+
+ it('should handle undefined values', () => {
+ const state: any = {
+ ...mockState,
+ context: {
+ isLoading: undefined,
+ profileInfo: undefined,
+ shouldTriggerAutoBackup: undefined,
+ },
+ };
+ expect(selectIsLoading(state)).toBeUndefined();
+ expect(selectProfileInfo(state)).toBeUndefined();
+ expect(selectShouldTriggerAutoBackup(state)).toBeUndefined();
+ });
+ });
+});
diff --git a/machines/backupAndRestore/restore/restoreModel.test.ts b/machines/backupAndRestore/restore/restoreModel.test.ts
new file mode 100644
index 00000000..caee3e39
--- /dev/null
+++ b/machines/backupAndRestore/restore/restoreModel.test.ts
@@ -0,0 +1,114 @@
+import {restoreModel} from './restoreModel';
+
+describe('restoreModel', () => {
+ describe('Model structure', () => {
+ it('should be defined', () => {
+ expect(restoreModel).toBeDefined();
+ });
+
+ it('should have initialContext', () => {
+ expect(restoreModel.initialContext).toBeDefined();
+ });
+
+ it('should have events', () => {
+ expect(restoreModel.events).toBeDefined();
+ });
+ });
+
+ describe('Initial Context', () => {
+ const initialContext = restoreModel.initialContext;
+
+ it('should have serviceRefs as empty object', () => {
+ expect(initialContext.serviceRefs).toEqual({});
+ expect(typeof initialContext.serviceRefs).toBe('object');
+ });
+
+ it('should have fileName as empty string', () => {
+ expect(initialContext.fileName).toBe('');
+ expect(typeof initialContext.fileName).toBe('string');
+ });
+
+ it('should have dataFromBackupFile as empty object', () => {
+ expect(initialContext.dataFromBackupFile).toEqual({});
+ expect(typeof initialContext.dataFromBackupFile).toBe('object');
+ });
+
+ it('should have errorReason as empty string', () => {
+ expect(initialContext.errorReason).toBe('');
+ expect(typeof initialContext.errorReason).toBe('string');
+ });
+
+ it('should have showRestoreInProgress as false', () => {
+ expect(initialContext.showRestoreInProgress).toBe(false);
+ expect(typeof initialContext.showRestoreInProgress).toBe('boolean');
+ });
+
+ it('should have all 5 required properties', () => {
+ const properties = Object.keys(initialContext);
+ expect(properties).toHaveLength(5);
+ });
+ });
+
+ describe('String properties', () => {
+ const context = restoreModel.initialContext;
+
+ it('all empty string properties should be empty', () => {
+ const emptyStrings = [context.fileName, context.errorReason];
+
+ emptyStrings.forEach(str => {
+ expect(str).toBe('');
+ expect(typeof str).toBe('string');
+ });
+ });
+ });
+
+ describe('Object properties', () => {
+ const context = restoreModel.initialContext;
+
+ it('all empty object properties should be empty objects', () => {
+ const emptyObjects = [context.serviceRefs, context.dataFromBackupFile];
+
+ emptyObjects.forEach(obj => {
+ expect(typeof obj).toBe('object');
+ expect(Object.keys(obj)).toHaveLength(0);
+ });
+ });
+ });
+
+ describe('Boolean properties', () => {
+ const context = restoreModel.initialContext;
+
+ it('showRestoreInProgress should be false', () => {
+ expect(context.showRestoreInProgress).toBe(false);
+ expect(typeof context.showRestoreInProgress).toBe('boolean');
+ });
+
+ it('should have correct initial values for boolean properties', () => {
+ expect(context.showRestoreInProgress).toBe(false);
+ });
+ });
+
+ describe('Model events', () => {
+ it('should have events object', () => {
+ expect(restoreModel.events).toBeDefined();
+ expect(typeof restoreModel.events).toBe('object');
+ });
+
+ it('should have event creators', () => {
+ const eventKeys = Object.keys(restoreModel.events);
+ expect(eventKeys.length).toBeGreaterThan(0);
+ });
+ });
+
+ describe('Property types validation', () => {
+ const context = restoreModel.initialContext;
+
+ it('should have correct types for all properties', () => {
+ expect(typeof context.serviceRefs).toBe('object');
+ expect(typeof context.fileName).toBe('string');
+ expect(typeof context.dataFromBackupFile).toBe('object');
+ expect(typeof context.errorReason).toBe('string');
+ expect(typeof context.showRestoreInProgress).toBe('boolean');
+ });
+ });
+});
diff --git a/machines/backupAndRestore/restore/restoreSelector.test.ts b/machines/backupAndRestore/restore/restoreSelector.test.ts
new file mode 100644
index 00000000..ebe6dc88
--- /dev/null
+++ b/machines/backupAndRestore/restore/restoreSelector.test.ts
@@ -0,0 +1,125 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import {
+ selectErrorReason,
+ selectIsBackUpRestoring,
+ selectIsBackUpRestoreSuccess,
+ selectIsBackUpRestoreFailure,
+ selectShowRestoreInProgress,
+} from './restoreSelector';
+
+describe('restoreSelector', () => {
+ describe('selectErrorReason', () => {
+ it('should return errorReason from context', () => {
+ const mockState: any = {
+ context: {
+ errorReason: 'Failed to restore backup',
+ },
+ };
+ expect(selectErrorReason(mockState)).toBe('Failed to restore backup');
+ });
+
+ it('should return null when no error', () => {
+ const mockState: any = {
+ context: {
+ errorReason: null,
+ },
+ };
+ expect(selectErrorReason(mockState)).toBeNull();
+ });
+ });
+
+ describe('selectIsBackUpRestoring', () => {
+ it('should return true when in restoreBackup state but not success or failure', () => {
+ const mockState: any = {
+ matches: jest.fn((state: string) => {
+ if (state === 'restoreBackup') return true;
+ if (state === 'restoreBackup.success') return false;
+ if (state === 'restoreBackup.failure') return false;
+ return false;
+ }),
+ };
+ expect(selectIsBackUpRestoring(mockState)).toBe(true);
+ });
+
+ it('should return false when in restoreBackup.success state', () => {
+ const mockState: any = {
+ matches: jest.fn((state: string) => {
+ if (state === 'restoreBackup') return true;
+ if (state === 'restoreBackup.success') return true;
+ return false;
+ }),
+ };
+ expect(selectIsBackUpRestoring(mockState)).toBe(false);
+ });
+
+ it('should return false when in restoreBackup.failure state', () => {
+ const mockState: any = {
+ matches: jest.fn((state: string) => {
+ if (state === 'restoreBackup') return true;
+ if (state === 'restoreBackup.failure') return true;
+ return false;
+ }),
+ };
+ expect(selectIsBackUpRestoring(mockState)).toBe(false);
+ });
+
+ it('should return false when not in restoreBackup state', () => {
+ const mockState: any = {
+ matches: jest.fn(() => false),
+ };
+ expect(selectIsBackUpRestoring(mockState)).toBe(false);
+ });
+ });
+
+ describe('selectIsBackUpRestoreSuccess', () => {
+ it('should return true when in restoreBackup.success state', () => {
+ const mockState: any = {
+ matches: jest.fn((state: string) => state === 'restoreBackup.success'),
+ };
+ expect(selectIsBackUpRestoreSuccess(mockState)).toBe(true);
+ });
+
+ it('should return false when not in success state', () => {
+ const mockState: any = {
+ matches: jest.fn(() => false),
+ };
+ expect(selectIsBackUpRestoreSuccess(mockState)).toBe(false);
+ });
+ });
+
+ describe('selectIsBackUpRestoreFailure', () => {
+ it('should return true when in restoreBackup.failure state', () => {
+ const mockState: any = {
+ matches: jest.fn((state: string) => state === 'restoreBackup.failure'),
+ };
+ expect(selectIsBackUpRestoreFailure(mockState)).toBe(true);
+ });
+
+ it('should return false when not in failure state', () => {
+ const mockState: any = {
+ matches: jest.fn(() => false),
+ };
+ expect(selectIsBackUpRestoreFailure(mockState)).toBe(false);
+ });
+ });
+
+ describe('selectShowRestoreInProgress', () => {
+ it('should return showRestoreInProgress from context', () => {
+ const mockState: any = {
+ context: {
+ showRestoreInProgress: true,
+ },
+ };
+ expect(selectShowRestoreInProgress(mockState)).toBe(true);
+ });
+
+ it('should return false when showRestoreInProgress is false', () => {
+ const mockState: any = {
+ context: {
+ showRestoreInProgress: false,
+ },
+ };
+ expect(selectShowRestoreInProgress(mockState)).toBe(false);
+ });
+ });
+});
diff --git a/machines/bleShare/commonSelectors.test.ts b/machines/bleShare/commonSelectors.test.ts
new file mode 100644
index 00000000..c583253d
--- /dev/null
+++ b/machines/bleShare/commonSelectors.test.ts
@@ -0,0 +1,298 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import {
+ selectIsCancelling,
+ selectIsReviewing,
+ selectIsAccepted,
+ selectIsRejected,
+ selectIsVerifyingIdentity,
+ selectIsInvalidIdentity,
+ selectIsDisconnected,
+ selectIsBluetoothDenied,
+ selectBleError,
+ selectIsExchangingDeviceInfo,
+ selectIsExchangingDeviceInfoTimeout,
+ selectIsOffline,
+ selectIsHandlingBleError,
+ selectReadyForBluetoothStateCheck,
+ selectIsNearByDevicesPermissionDenied,
+ selectIsBluetoothPermissionDenied,
+ selectIsStartPermissionCheck,
+ selectIsLocationPermissionRationale,
+} from './commonSelectors';
+
+describe('commonSelectors', () => {
+ const mockState: any = {
+ context: {
+ bleError: null,
+ readyForBluetoothStateCheck: false,
+ },
+ matches: jest.fn(() => false),
+ };
+
+ describe('selectIsCancelling', () => {
+ it('should return true when in cancelling state', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'cancelling'),
+ };
+ expect(selectIsCancelling(state)).toBe(true);
+ });
+
+ it('should return false when not cancelling', () => {
+ expect(selectIsCancelling(mockState)).toBe(false);
+ });
+ });
+
+ describe('selectIsReviewing', () => {
+ it('should return true when in reviewing state', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'reviewing'),
+ };
+ expect(selectIsReviewing(state)).toBe(true);
+ });
+
+ it('should return false when not reviewing', () => {
+ expect(selectIsReviewing(mockState)).toBe(false);
+ });
+ });
+
+ describe('selectIsAccepted', () => {
+ it('should return true when in reviewing.accepted state', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'reviewing.accepted'),
+ };
+ expect(selectIsAccepted(state)).toBe(true);
+ });
+
+ it('should call matches with reviewing.accepted', () => {
+ selectIsAccepted(mockState);
+ expect(mockState.matches).toHaveBeenCalledWith('reviewing.accepted');
+ });
+ });
+
+ describe('selectIsRejected', () => {
+ it('should return true when in reviewing.rejected state', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'reviewing.rejected'),
+ };
+ expect(selectIsRejected(state)).toBe(true);
+ });
+
+ it('should call matches with reviewing.rejected', () => {
+ selectIsRejected(mockState);
+ expect(mockState.matches).toHaveBeenCalledWith('reviewing.rejected');
+ });
+ });
+
+ describe('selectIsVerifyingIdentity', () => {
+ it('should return true when in reviewing.verifyingIdentity state', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'reviewing.verifyingIdentity'),
+ };
+ expect(selectIsVerifyingIdentity(state)).toBe(true);
+ });
+
+ it('should call matches with reviewing.verifyingIdentity', () => {
+ selectIsVerifyingIdentity(mockState);
+ expect(mockState.matches).toHaveBeenCalledWith(
+ 'reviewing.verifyingIdentity',
+ );
+ });
+ });
+
+ describe('selectIsInvalidIdentity', () => {
+ it('should return true when in reviewing.invalidIdentity state', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'reviewing.invalidIdentity'),
+ };
+ expect(selectIsInvalidIdentity(state)).toBe(true);
+ });
+
+ it('should call matches with reviewing.invalidIdentity', () => {
+ selectIsInvalidIdentity(mockState);
+ expect(mockState.matches).toHaveBeenCalledWith(
+ 'reviewing.invalidIdentity',
+ );
+ });
+ });
+
+ describe('selectIsDisconnected', () => {
+ it('should return true when in disconnected state', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'disconnected'),
+ };
+ expect(selectIsDisconnected(state)).toBe(true);
+ });
+
+ it('should call matches with disconnected', () => {
+ selectIsDisconnected(mockState);
+ expect(mockState.matches).toHaveBeenCalledWith('disconnected');
+ });
+ });
+
+ describe('selectIsBluetoothDenied', () => {
+ it('should return true when in bluetoothDenied state', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'bluetoothDenied'),
+ };
+ expect(selectIsBluetoothDenied(state)).toBe(true);
+ });
+
+ it('should call matches with bluetoothDenied', () => {
+ selectIsBluetoothDenied(mockState);
+ expect(mockState.matches).toHaveBeenCalledWith('bluetoothDenied');
+ });
+ });
+
+ describe('selectBleError', () => {
+ it('should return BLE error from context', () => {
+ const bleError = {code: 'BLE_001', message: 'Connection failed'};
+ const state: any = {
+ ...mockState,
+ context: {...mockState.context, bleError},
+ };
+ expect(selectBleError(state)).toEqual(bleError);
+ });
+
+ it('should return null when no BLE error', () => {
+ expect(selectBleError(mockState)).toBeNull();
+ });
+ });
+
+ describe('TODO selectors (hardcoded)', () => {
+ it('selectIsExchangingDeviceInfo should always return false', () => {
+ expect(selectIsExchangingDeviceInfo()).toBe(false);
+ });
+
+ it('selectIsExchangingDeviceInfoTimeout should always return false', () => {
+ expect(selectIsExchangingDeviceInfoTimeout()).toBe(false);
+ });
+
+ it('selectIsOffline should always return false', () => {
+ expect(selectIsOffline()).toBe(false);
+ });
+ });
+
+ describe('selectIsHandlingBleError', () => {
+ it('should return true when in handlingBleError state', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'handlingBleError'),
+ };
+ expect(selectIsHandlingBleError(state)).toBe(true);
+ });
+
+ it('should call matches with handlingBleError', () => {
+ selectIsHandlingBleError(mockState);
+ expect(mockState.matches).toHaveBeenCalledWith('handlingBleError');
+ });
+ });
+
+ describe('selectReadyForBluetoothStateCheck', () => {
+ it('should return ready status from context', () => {
+ expect(selectReadyForBluetoothStateCheck(mockState)).toBe(false);
+ });
+
+ it('should return true when ready', () => {
+ const state: any = {
+ ...mockState,
+ context: {...mockState.context, readyForBluetoothStateCheck: true},
+ };
+ expect(selectReadyForBluetoothStateCheck(state)).toBe(true);
+ });
+ });
+
+ describe('selectIsNearByDevicesPermissionDenied', () => {
+ it('should return true when in nearByDevicesPermissionDenied state', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'nearByDevicesPermissionDenied'),
+ };
+ expect(selectIsNearByDevicesPermissionDenied(state)).toBe(true);
+ });
+
+ it('should call matches with nearByDevicesPermissionDenied', () => {
+ selectIsNearByDevicesPermissionDenied(mockState);
+ expect(mockState.matches).toHaveBeenCalledWith(
+ 'nearByDevicesPermissionDenied',
+ );
+ });
+ });
+
+ describe('selectIsBluetoothPermissionDenied', () => {
+ it('should return true when in bluetoothPermissionDenied state', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'bluetoothPermissionDenied'),
+ };
+ expect(selectIsBluetoothPermissionDenied(state)).toBe(true);
+ });
+
+ it('should call matches with bluetoothPermissionDenied', () => {
+ selectIsBluetoothPermissionDenied(mockState);
+ expect(mockState.matches).toHaveBeenCalledWith(
+ 'bluetoothPermissionDenied',
+ );
+ });
+ });
+
+ describe('selectIsStartPermissionCheck', () => {
+ it('should return true when in startPermissionCheck state', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'startPermissionCheck'),
+ };
+ expect(selectIsStartPermissionCheck(state)).toBe(true);
+ });
+
+ it('should call matches with startPermissionCheck', () => {
+ selectIsStartPermissionCheck(mockState);
+ expect(mockState.matches).toHaveBeenCalledWith('startPermissionCheck');
+ });
+ });
+
+ describe('selectIsLocationPermissionRationale', () => {
+ it('should return true when in checkingLocationState.LocationPermissionRationale state', () => {
+ const state: any = {
+ ...mockState,
+ matches: jest.fn(
+ (s: string) =>
+ s === 'checkingLocationState.LocationPermissionRationale',
+ ),
+ };
+ expect(selectIsLocationPermissionRationale(state)).toBe(true);
+ });
+
+ it('should call matches with correct state path', () => {
+ selectIsLocationPermissionRationale(mockState);
+ expect(mockState.matches).toHaveBeenCalledWith(
+ 'checkingLocationState.LocationPermissionRationale',
+ );
+ });
+ });
+
+ describe('Edge cases', () => {
+ it('should handle undefined bleError', () => {
+ const state: any = {
+ ...mockState,
+ context: {...mockState.context, bleError: undefined},
+ };
+ expect(selectBleError(state)).toBeUndefined();
+ });
+
+ it('should handle empty bleError object', () => {
+ const state: any = {
+ ...mockState,
+ context: {...mockState.context, bleError: {}},
+ };
+ expect(selectBleError(state)).toEqual({});
+ });
+ });
+});
diff --git a/machines/bleShare/request/selectors.test.ts b/machines/bleShare/request/selectors.test.ts
new file mode 100644
index 00000000..ab59228e
--- /dev/null
+++ b/machines/bleShare/request/selectors.test.ts
@@ -0,0 +1,233 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import {
+ selectSenderInfo,
+ selectCredential,
+ selectVerifiableCredentialData,
+ selectIsReviewingInIdle,
+ selectIsWaitingForConnection,
+ selectIsCheckingBluetoothService,
+ selectIsWaitingForVc,
+ selectIsWaitingForVcTimeout,
+ selectOpenId4VpUri,
+ selectIsAccepting,
+ selectIsDisplayingIncomingVC,
+ selectIsSavingFailedInIdle,
+ selectIsDone,
+ selectIsNavigatingToReceivedCards,
+ selectIsNavigatingToHome,
+} from './selectors';
+
+describe('requestMachine selectors', () => {
+ describe('selectSenderInfo', () => {
+ it('should return senderInfo from context', () => {
+ const mockSenderInfo = {name: 'John Doe', deviceId: 'device-123'};
+ const mockState: any = {
+ context: {
+ senderInfo: mockSenderInfo,
+ },
+ };
+ expect(selectSenderInfo(mockState)).toBe(mockSenderInfo);
+ });
+ });
+
+ describe('selectCredential', () => {
+ it('should return verifiableCredential from incomingVc', () => {
+ const mockVC = {credential: {id: 'cred-123'}};
+ const mockState: any = {
+ context: {
+ incomingVc: {
+ verifiableCredential: mockVC,
+ },
+ },
+ };
+ expect(selectCredential(mockState)).toBe(mockVC);
+ });
+ });
+
+ describe('selectVerifiableCredentialData', () => {
+ it('should return formatted verifiable credential data', () => {
+ const mockState: any = {
+ context: {
+ incomingVc: {
+ vcMetadata: {
+ id: 'vc-001',
+ issuer: 'Test Issuer',
+ },
+ verifiableCredential: {
+ credential: {
+ credentialSubject: {
+ face: 'base64-face-data',
+ },
+ },
+ issuerLogo: 'https://example.com/logo.png',
+ wellKnown: 'https://example.com/.well-known',
+ credentialConfigurationId: 'config-123',
+ },
+ },
+ },
+ };
+
+ const result = selectVerifiableCredentialData(mockState);
+ expect(result.issuer).toBe('Test Issuer');
+ expect(result.issuerLogo).toBe('https://example.com/logo.png');
+ expect(result.wellKnown).toBe('https://example.com/.well-known');
+ expect(result.credentialConfigurationId).toBe('config-123');
+ expect(result.face).toBe('base64-face-data');
+ });
+
+ it('should use biometrics face when credentialSubject face is not available', () => {
+ const mockState: any = {
+ context: {
+ incomingVc: {
+ vcMetadata: {
+ id: 'vc-002',
+ issuer: 'Mosip',
+ },
+ credential: {
+ biometrics: {
+ face: 'biometric-face-data',
+ },
+ },
+ verifiableCredential: {},
+ },
+ },
+ };
+
+ const result = selectVerifiableCredentialData(mockState);
+ expect(result.face).toBe('biometric-face-data');
+ });
+ });
+
+ describe('selectIsReviewingInIdle', () => {
+ it('should return true when in reviewing.idle state', () => {
+ const mockState: any = {
+ matches: jest.fn((state: string) => state === 'reviewing.idle'),
+ };
+ expect(selectIsReviewingInIdle(mockState)).toBe(true);
+ });
+
+ it('should return false when not in reviewing.idle state', () => {
+ const mockState: any = {
+ matches: jest.fn(() => false),
+ };
+ expect(selectIsReviewingInIdle(mockState)).toBe(false);
+ });
+ });
+
+ describe('selectIsWaitingForConnection', () => {
+ it('should return true when in waitingForConnection state', () => {
+ const mockState: any = {
+ matches: jest.fn((state: string) => state === 'waitingForConnection'),
+ };
+ expect(selectIsWaitingForConnection(mockState)).toBe(true);
+ });
+ });
+
+ describe('selectIsCheckingBluetoothService', () => {
+ it('should return true when in checkingBluetoothService state', () => {
+ const mockState: any = {
+ matches: jest.fn(
+ (state: string) => state === 'checkingBluetoothService',
+ ),
+ };
+ expect(selectIsCheckingBluetoothService(mockState)).toBe(true);
+ });
+ });
+
+ describe('selectIsWaitingForVc', () => {
+ it('should return true when in waitingForVc.inProgress state', () => {
+ const mockState: any = {
+ matches: jest.fn(
+ (state: string) => state === 'waitingForVc.inProgress',
+ ),
+ };
+ expect(selectIsWaitingForVc(mockState)).toBe(true);
+ });
+ });
+
+ describe('selectIsWaitingForVcTimeout', () => {
+ it('should return true when in waitingForVc.timeout state', () => {
+ const mockState: any = {
+ matches: jest.fn((state: string) => state === 'waitingForVc.timeout'),
+ };
+ expect(selectIsWaitingForVcTimeout(mockState)).toBe(true);
+ });
+ });
+
+ describe('selectOpenId4VpUri', () => {
+ it('should return openId4VpUri from context', () => {
+ const mockState: any = {
+ context: {
+ openId4VpUri: 'openid4vp://verify?request=abc123',
+ },
+ };
+ expect(selectOpenId4VpUri(mockState)).toBe(
+ 'openid4vp://verify?request=abc123',
+ );
+ });
+ });
+
+ describe('selectIsAccepting', () => {
+ it('should return true when in reviewing.accepting state', () => {
+ const mockState: any = {
+ matches: jest.fn((state: string) => state === 'reviewing.accepting'),
+ };
+ expect(selectIsAccepting(mockState)).toBe(true);
+ });
+ });
+
+ describe('selectIsDisplayingIncomingVC', () => {
+ it('should return true when in reviewing.displayingIncomingVC state', () => {
+ const mockState: any = {
+ matches: jest.fn(
+ (state: string) => state === 'reviewing.displayingIncomingVC',
+ ),
+ };
+ expect(selectIsDisplayingIncomingVC(mockState)).toBe(true);
+ });
+ });
+
+ describe('selectIsSavingFailedInIdle', () => {
+ it('should return true when in reviewing.savingFailed.idle state', () => {
+ const mockState: any = {
+ matches: jest.fn(
+ (state: string) => state === 'reviewing.savingFailed.idle',
+ ),
+ };
+ expect(selectIsSavingFailedInIdle(mockState)).toBe(true);
+ });
+ });
+
+ describe('selectIsDone', () => {
+ it('should return true when in reviewing.navigatingToHistory state', () => {
+ const mockState: any = {
+ matches: jest.fn(
+ (state: string) => state === 'reviewing.navigatingToHistory',
+ ),
+ };
+ expect(selectIsDone(mockState)).toBe(true);
+ });
+ });
+
+ describe('selectIsNavigatingToReceivedCards', () => {
+ it('should return true when in reviewing.navigatingToReceivedCards state', () => {
+ const mockState: any = {
+ matches: jest.fn(
+ (state: string) => state === 'reviewing.navigatingToReceivedCards',
+ ),
+ };
+ expect(selectIsNavigatingToReceivedCards(mockState)).toBe(true);
+ });
+ });
+
+ describe('selectIsNavigatingToHome', () => {
+ it('should return true when in reviewing.navigatingToHome state', () => {
+ const mockState: any = {
+ matches: jest.fn(
+ (state: string) => state === 'reviewing.navigatingToHome',
+ ),
+ };
+ expect(selectIsNavigatingToHome(mockState)).toBe(true);
+ });
+ });
+});
diff --git a/machines/bleShare/scan/scanModel.test.ts b/machines/bleShare/scan/scanModel.test.ts
new file mode 100644
index 00000000..dd591484
--- /dev/null
+++ b/machines/bleShare/scan/scanModel.test.ts
@@ -0,0 +1,292 @@
+import {ScanModel} from './scanModel';
+import {VCShareFlowType} from '../../../shared/Utils';
+
+describe('ScanModel', () => {
+ describe('Model structure', () => {
+ it('should be defined', () => {
+ expect(ScanModel).toBeDefined();
+ });
+
+ it('should have initialContext', () => {
+ expect(ScanModel.initialContext).toBeDefined();
+ });
+
+ it('should have events', () => {
+ expect(ScanModel.events).toBeDefined();
+ });
+ });
+
+ describe('Initial Context', () => {
+ const initialContext = ScanModel.initialContext;
+
+ it('should have serviceRefs as empty object', () => {
+ expect(initialContext.serviceRefs).toEqual({});
+ expect(typeof initialContext.serviceRefs).toBe('object');
+ });
+
+ it('should have senderInfo as empty object', () => {
+ expect(initialContext.senderInfo).toEqual({});
+ expect(typeof initialContext.senderInfo).toBe('object');
+ });
+
+ it('should have receiverInfo as empty object', () => {
+ expect(initialContext.receiverInfo).toEqual({});
+ expect(typeof initialContext.receiverInfo).toBe('object');
+ });
+
+ it('should have selectedVc as empty object', () => {
+ expect(initialContext.selectedVc).toEqual({});
+ expect(typeof initialContext.selectedVc).toBe('object');
+ });
+
+ it('should have bleError as empty object', () => {
+ expect(initialContext.bleError).toEqual({});
+ expect(typeof initialContext.bleError).toBe('object');
+ });
+
+ it('should have loggers as empty array', () => {
+ expect(initialContext.loggers).toEqual([]);
+ expect(Array.isArray(initialContext.loggers)).toBe(true);
+ });
+
+ it('should have vcName as empty string', () => {
+ expect(initialContext.vcName).toBe('');
+ expect(typeof initialContext.vcName).toBe('string');
+ });
+
+ it('should have flowType as SIMPLE_SHARE', () => {
+ expect(initialContext.flowType).toBe(VCShareFlowType.SIMPLE_SHARE);
+ });
+
+ it('should have openID4VPFlowType as empty string', () => {
+ expect(initialContext.openID4VPFlowType).toBe('');
+ expect(typeof initialContext.openID4VPFlowType).toBe('string');
+ });
+
+ it('should have verificationImage as empty object', () => {
+ expect(initialContext.verificationImage).toEqual({});
+ expect(typeof initialContext.verificationImage).toBe('object');
+ });
+
+ it('should have openId4VpUri as empty string', () => {
+ expect(initialContext.openId4VpUri).toBe('');
+ expect(typeof initialContext.openId4VpUri).toBe('string');
+ });
+
+ it('should have shareLogType as empty string', () => {
+ expect(initialContext.shareLogType).toBe('');
+ expect(typeof initialContext.shareLogType).toBe('string');
+ });
+
+ it('should have QrLoginRef as empty object', () => {
+ expect(initialContext.QrLoginRef).toEqual({});
+ expect(typeof initialContext.QrLoginRef).toBe('object');
+ });
+
+ it('should have OpenId4VPRef as empty object', () => {
+ expect(initialContext.OpenId4VPRef).toEqual({});
+ expect(typeof initialContext.OpenId4VPRef).toBe('object');
+ });
+
+ it('should have showQuickShareSuccessBanner as false', () => {
+ expect(initialContext.showQuickShareSuccessBanner).toBe(false);
+ expect(typeof initialContext.showQuickShareSuccessBanner).toBe('boolean');
+ });
+
+ it('should have linkCode as empty string', () => {
+ expect(initialContext.linkCode).toBe('');
+ expect(typeof initialContext.linkCode).toBe('string');
+ });
+
+ it('should have authorizationRequest as empty string', () => {
+ expect(initialContext.authorizationRequest).toBe('');
+ expect(typeof initialContext.authorizationRequest).toBe('string');
+ });
+
+ it('should have quickShareData as empty object', () => {
+ expect(initialContext.quickShareData).toEqual({});
+ expect(typeof initialContext.quickShareData).toBe('object');
+ });
+
+ it('should have isQrLoginViaDeepLink as false', () => {
+ expect(initialContext.isQrLoginViaDeepLink).toBe(false);
+ expect(typeof initialContext.isQrLoginViaDeepLink).toBe('boolean');
+ });
+
+ it('should have isOVPViaDeepLink as false', () => {
+ expect(initialContext.isOVPViaDeepLink).toBe(false);
+ expect(typeof initialContext.isOVPViaDeepLink).toBe('boolean');
+ });
+
+ it('should have showFaceAuthConsent as true', () => {
+ expect(initialContext.showFaceAuthConsent).toBe(true);
+ expect(typeof initialContext.showFaceAuthConsent).toBe('boolean');
+ });
+
+ it('should have readyForBluetoothStateCheck as false', () => {
+ expect(initialContext.readyForBluetoothStateCheck).toBe(false);
+ expect(typeof initialContext.readyForBluetoothStateCheck).toBe('boolean');
+ });
+
+ it('should have showFaceCaptureSuccessBanner as false', () => {
+ expect(initialContext.showFaceCaptureSuccessBanner).toBe(false);
+ expect(typeof initialContext.showFaceCaptureSuccessBanner).toBe(
+ 'boolean',
+ );
+ });
+
+ it('should have all 23 required properties', () => {
+ const properties = Object.keys(initialContext);
+ expect(properties).toHaveLength(23);
+ });
+ });
+
+ describe('String properties', () => {
+ const context = ScanModel.initialContext;
+
+ it('all empty string properties should be empty', () => {
+ const emptyStrings = [
+ context.vcName,
+ context.openID4VPFlowType,
+ context.openId4VpUri,
+ context.shareLogType,
+ context.linkCode,
+ context.authorizationRequest,
+ ];
+
+ emptyStrings.forEach(str => {
+ expect(str).toBe('');
+ expect(typeof str).toBe('string');
+ });
+ });
+
+ it('flowType should be SIMPLE_SHARE enum value', () => {
+ expect(context.flowType).toBe(VCShareFlowType.SIMPLE_SHARE);
+ });
+ });
+
+ describe('Array properties', () => {
+ const context = ScanModel.initialContext;
+
+ it('loggers should be empty array', () => {
+ expect(Array.isArray(context.loggers)).toBe(true);
+ expect(context.loggers).toHaveLength(0);
+ });
+ });
+
+ describe('Object properties', () => {
+ const context = ScanModel.initialContext;
+
+ it('all empty object properties should be empty objects', () => {
+ const emptyObjects = [
+ context.serviceRefs,
+ context.senderInfo,
+ context.receiverInfo,
+ context.selectedVc,
+ context.bleError,
+ context.verificationImage,
+ context.QrLoginRef,
+ context.OpenId4VPRef,
+ context.quickShareData,
+ ];
+
+ emptyObjects.forEach(obj => {
+ expect(typeof obj).toBe('object');
+ expect(Object.keys(obj)).toHaveLength(0);
+ });
+ });
+ });
+
+ describe('Boolean properties', () => {
+ const context = ScanModel.initialContext;
+
+ it('showQuickShareSuccessBanner should be false', () => {
+ expect(context.showQuickShareSuccessBanner).toBe(false);
+ expect(typeof context.showQuickShareSuccessBanner).toBe('boolean');
+ });
+
+ it('isQrLoginViaDeepLink should be false', () => {
+ expect(context.isQrLoginViaDeepLink).toBe(false);
+ expect(typeof context.isQrLoginViaDeepLink).toBe('boolean');
+ });
+
+ it('isOVPViaDeepLink should be false', () => {
+ expect(context.isOVPViaDeepLink).toBe(false);
+ expect(typeof context.isOVPViaDeepLink).toBe('boolean');
+ });
+
+ it('showFaceAuthConsent should be true', () => {
+ expect(context.showFaceAuthConsent).toBe(true);
+ expect(typeof context.showFaceAuthConsent).toBe('boolean');
+ });
+
+ it('readyForBluetoothStateCheck should be false', () => {
+ expect(context.readyForBluetoothStateCheck).toBe(false);
+ expect(typeof context.readyForBluetoothStateCheck).toBe('boolean');
+ });
+
+ it('showFaceCaptureSuccessBanner should be false', () => {
+ expect(context.showFaceCaptureSuccessBanner).toBe(false);
+ expect(typeof context.showFaceCaptureSuccessBanner).toBe('boolean');
+ });
+
+ it('should have correct initial values for boolean properties', () => {
+ const falseProps = [
+ context.showQuickShareSuccessBanner,
+ context.isQrLoginViaDeepLink,
+ context.isOVPViaDeepLink,
+ context.readyForBluetoothStateCheck,
+ context.showFaceCaptureSuccessBanner,
+ ];
+
+ falseProps.forEach(prop => {
+ expect(prop).toBe(false);
+ expect(typeof prop).toBe('boolean');
+ });
+
+ expect(context.showFaceAuthConsent).toBe(true);
+ expect(typeof context.showFaceAuthConsent).toBe('boolean');
+ });
+ });
+
+ describe('Model events', () => {
+ it('should have events object', () => {
+ expect(ScanModel.events).toBeDefined();
+ expect(typeof ScanModel.events).toBe('object');
+ });
+
+ it('should have event creators', () => {
+ const eventKeys = Object.keys(ScanModel.events);
+ expect(eventKeys.length).toBeGreaterThan(0);
+ });
+ });
+
+ describe('Property types validation', () => {
+ const context = ScanModel.initialContext;
+
+ it('should have correct types for all properties', () => {
+ expect(typeof context.vcName).toBe('string');
+ expect(typeof context.openID4VPFlowType).toBe('string');
+ expect(typeof context.openId4VpUri).toBe('string');
+ expect(typeof context.shareLogType).toBe('string');
+ expect(typeof context.linkCode).toBe('string');
+ expect(typeof context.authorizationRequest).toBe('string');
+ expect(Array.isArray(context.loggers)).toBe(true);
+ expect(typeof context.showQuickShareSuccessBanner).toBe('boolean');
+ expect(typeof context.isQrLoginViaDeepLink).toBe('boolean');
+ expect(typeof context.isOVPViaDeepLink).toBe('boolean');
+ expect(typeof context.showFaceAuthConsent).toBe('boolean');
+ expect(typeof context.readyForBluetoothStateCheck).toBe('boolean');
+ expect(typeof context.showFaceCaptureSuccessBanner).toBe('boolean');
+ expect(typeof context.serviceRefs).toBe('object');
+ expect(typeof context.senderInfo).toBe('object');
+ expect(typeof context.receiverInfo).toBe('object');
+ expect(typeof context.selectedVc).toBe('object');
+ expect(typeof context.bleError).toBe('object');
+ expect(typeof context.verificationImage).toBe('object');
+ expect(typeof context.QrLoginRef).toBe('object');
+ expect(typeof context.OpenId4VPRef).toBe('object');
+ expect(typeof context.quickShareData).toBe('object');
+ });
+ });
+});
diff --git a/machines/bleShare/scan/scanSelectors.test.ts b/machines/bleShare/scan/scanSelectors.test.ts
new file mode 100644
index 00000000..9a07adda
--- /dev/null
+++ b/machines/bleShare/scan/scanSelectors.test.ts
@@ -0,0 +1,500 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import {
+ selectFlowType,
+ selectOpenID4VPFlowType,
+ selectReceiverInfo,
+ selectVcName,
+ selectCredential,
+ selectVerifiableCredentialData,
+ selectQrLoginRef,
+ selectIsScanning,
+ selectIsQuickShareDone,
+ selectShowQuickShareSuccessBanner,
+ selectIsConnecting,
+ selectIsConnectingTimeout,
+ selectIsSelectingVc,
+ selectIsSendingVc,
+ selectIsSendingVP,
+ selectIsSendingVPError,
+ selectIsSendingVPSuccess,
+ selectIsFaceIdentityVerified,
+ selectIsSendingVcTimeout,
+ selectIsSendingVPTimeout,
+ selectIsSent,
+ selectIsInvalid,
+ selectIsLocationDenied,
+ selectIsLocationDisabled,
+ selectIsShowQrLogin,
+ selectIsQrLoginDone,
+ selectIsQrLoginDoneViaDeeplink,
+ selectIsQrLoginStoring,
+ selectIsDone,
+ selectIsMinimumStorageRequiredForAuditEntryLimitReached,
+ selectIsFaceVerificationConsent,
+ selectIsOVPViaDeepLink,
+} from './scanSelectors';
+
+describe('scanSelectors', () => {
+ const mockState: any = {
+ context: {
+ flowType: '',
+ openID4VPFlowType: '',
+ receiverInfo: null,
+ vcName: '',
+ selectedVc: null,
+ QrLoginRef: null,
+ showQuickShareSuccessBanner: false,
+ isOVPViaDeepLink: false,
+ showFaceCaptureSuccessBanner: false,
+ },
+ matches: jest.fn(() => false),
+ };
+
+ describe('selectFlowType', () => {
+ it('should return flowType from context', () => {
+ const state = {
+ ...mockState,
+ context: {...mockState.context, flowType: 'simple_share'},
+ };
+ expect(selectFlowType(state)).toBe('simple_share');
+ });
+ });
+
+ describe('selectOpenID4VPFlowType', () => {
+ it('should return openID4VPFlowType from context', () => {
+ const state = {
+ ...mockState,
+ context: {...mockState.context, openID4VPFlowType: 'ovp_flow'},
+ };
+ expect(selectOpenID4VPFlowType(state)).toBe('ovp_flow');
+ });
+ });
+
+ describe('selectReceiverInfo', () => {
+ it('should return receiverInfo from context', () => {
+ const receiverInfo = {name: 'Test', id: '123'};
+ const state = {
+ ...mockState,
+ context: {...mockState.context, receiverInfo},
+ };
+ expect(selectReceiverInfo(state)).toBe(receiverInfo);
+ });
+ });
+
+ describe('selectVcName', () => {
+ it('should return vcName from context', () => {
+ const state = {
+ ...mockState,
+ context: {...mockState.context, vcName: 'National ID'},
+ };
+ expect(selectVcName(state)).toBe('National ID');
+ });
+ });
+
+ describe('selectCredential', () => {
+ it('should return credential when verifiableCredential has credential property', () => {
+ const mockCredential = {id: 'cred123'};
+ const state = {
+ ...mockState,
+ context: {
+ ...mockState.context,
+ selectedVc: {
+ verifiableCredential: {credential: mockCredential},
+ },
+ },
+ };
+ const result = selectCredential(state);
+ expect(result).toEqual([mockCredential]);
+ });
+
+ it('should return verifiableCredential when no credential property', () => {
+ const mockVC = {id: 'vc456'};
+ const state = {
+ ...mockState,
+ context: {
+ ...mockState.context,
+ selectedVc: {
+ verifiableCredential: mockVC,
+ },
+ },
+ };
+ const result = selectCredential(state);
+ expect(result).toEqual([mockVC]);
+ });
+ });
+
+ describe('selectVerifiableCredentialData', () => {
+ it('should return formatted credential data with face from credentialSubject', () => {
+ const mockCredential = {
+ credentialSubject: {
+ face: '/9j/4AAQSkZJRgABAQAAAQ...',
+ gender: 'Male',
+ UIN: '123456789',
+ },
+ issuer: 'did:web:example.com',
+ issuanceDate: '2023-01-01T00:00:00Z',
+ };
+ const state = {
+ ...mockState,
+ context: {
+ ...mockState.context,
+ selectedVc: {
+ verifiableCredential: {credential: mockCredential},
+ vcMetadata: {},
+ credential: mockCredential,
+ format: 'ldp_vc',
+ },
+ },
+ };
+ const result = selectVerifiableCredentialData(state);
+
+ expect(result).toBeDefined();
+ expect(Array.isArray(result)).toBe(true);
+ expect(result[0]).toBeDefined();
+ expect(result[0].face).toBeDefined();
+ });
+
+ it('should use biometrics face when credentialSubject face is not available', () => {
+ const mockCredential = {
+ credentialSubject: {
+ gender: 'Male',
+ UIN: '123456789',
+ },
+ biometrics: {
+ face: '/9j/4AAQSkZJRgABAQBBBB...',
+ },
+ issuer: 'did:web:example.com',
+ issuanceDate: '2023-01-01T00:00:00Z',
+ };
+ const state = {
+ ...mockState,
+ context: {
+ ...mockState.context,
+ selectedVc: {
+ verifiableCredential: {credential: mockCredential},
+ vcMetadata: {},
+ credential: mockCredential,
+ format: 'ldp_vc',
+ },
+ },
+ };
+ const result = selectVerifiableCredentialData(state);
+
+ expect(result).toBeDefined();
+ expect(Array.isArray(result)).toBe(true);
+ expect(result[0].face).toBe('/9j/4AAQSkZJRgABAQBBBB...');
+ });
+
+ it('should handle credential without face data', () => {
+ const mockCredential = {
+ credentialSubject: {
+ gender: 'Male',
+ UIN: '123456789',
+ },
+ issuer: 'did:web:example.com',
+ issuanceDate: '2023-01-01T00:00:00Z',
+ };
+ const state = {
+ ...mockState,
+ context: {
+ ...mockState.context,
+ selectedVc: {
+ verifiableCredential: {credential: mockCredential},
+ vcMetadata: {},
+ credential: mockCredential,
+ format: 'ldp_vc',
+ },
+ },
+ };
+ const result = selectVerifiableCredentialData(state);
+
+ expect(result).toBeDefined();
+ expect(Array.isArray(result)).toBe(true);
+ expect(result[0].face).toBeUndefined();
+ });
+ });
+
+ describe('selectQrLoginRef', () => {
+ it('should return QrLoginRef from context', () => {
+ const ref = {current: 'test'};
+ const state = {
+ ...mockState,
+ context: {...mockState.context, QrLoginRef: ref},
+ };
+ expect(selectQrLoginRef(state)).toBe(ref);
+ });
+ });
+
+ describe('selectIsScanning', () => {
+ it('should return true when in findingConnection state', () => {
+ const state = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'findingConnection'),
+ };
+ expect(selectIsScanning(state)).toBe(true);
+ });
+
+ it('should return false when not in findingConnection state', () => {
+ expect(selectIsScanning(mockState)).toBe(false);
+ });
+ });
+
+ describe('selectIsQuickShareDone', () => {
+ it('should return true when in loadVCS.navigatingToHome state', () => {
+ const state = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'loadVCS.navigatingToHome'),
+ };
+ expect(selectIsQuickShareDone(state)).toBe(true);
+ });
+
+ it('should return false when not in state', () => {
+ expect(selectIsQuickShareDone(mockState)).toBe(false);
+ });
+ });
+
+ describe('selectShowQuickShareSuccessBanner', () => {
+ it('should return showQuickShareSuccessBanner from context', () => {
+ const state = {
+ ...mockState,
+ context: {...mockState.context, showQuickShareSuccessBanner: true},
+ };
+ expect(selectShowQuickShareSuccessBanner(state)).toBe(true);
+ });
+ });
+
+ describe('selectIsConnecting', () => {
+ it('should return true when in connecting.inProgress state', () => {
+ const state = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'connecting.inProgress'),
+ };
+ expect(selectIsConnecting(state)).toBe(true);
+ });
+ });
+
+ describe('selectIsConnectingTimeout', () => {
+ it('should return true when in connecting.timeout state', () => {
+ const state = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'connecting.timeout'),
+ };
+ expect(selectIsConnectingTimeout(state)).toBe(true);
+ });
+ });
+
+ describe('selectIsSelectingVc', () => {
+ it('should return true when in reviewing.selectingVc state', () => {
+ const state = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'reviewing.selectingVc'),
+ };
+ expect(selectIsSelectingVc(state)).toBe(true);
+ });
+ });
+
+ describe('selectIsSendingVc', () => {
+ it('should return true when in reviewing.sendingVc.inProgress state', () => {
+ const state = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'reviewing.sendingVc.inProgress'),
+ };
+ expect(selectIsSendingVc(state)).toBe(true);
+ });
+ });
+
+ describe('selectIsSendingVP', () => {
+ it('should return true when in startVPSharing.inProgress state', () => {
+ const state = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'startVPSharing.inProgress'),
+ };
+ expect(selectIsSendingVP(state)).toBe(true);
+ });
+ });
+
+ describe('selectIsSendingVPError', () => {
+ it('should return true when in startVPSharing.showError state', () => {
+ const state = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'startVPSharing.showError'),
+ };
+ expect(selectIsSendingVPError(state)).toBe(true);
+ });
+ });
+
+ describe('selectIsSendingVPSuccess', () => {
+ it('should return true when in startVPSharing.success state', () => {
+ const state = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'startVPSharing.success'),
+ };
+ expect(selectIsSendingVPSuccess(state)).toBe(true);
+ });
+ });
+
+ describe('selectIsFaceIdentityVerified', () => {
+ it('should return true when sendingVc and banner is true', () => {
+ const state = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'reviewing.sendingVc.inProgress'),
+ context: {...mockState.context, showFaceCaptureSuccessBanner: true},
+ };
+ expect(selectIsFaceIdentityVerified(state)).toBe(true);
+ });
+
+ it('should return false when not in sendingVc state', () => {
+ const state = {
+ ...mockState,
+ context: {...mockState.context, showFaceCaptureSuccessBanner: true},
+ };
+ expect(selectIsFaceIdentityVerified(state)).toBe(false);
+ });
+ });
+
+ describe('selectIsSendingVcTimeout', () => {
+ it('should return true when in reviewing.sendingVc.timeout state', () => {
+ const state = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'reviewing.sendingVc.timeout'),
+ };
+ expect(selectIsSendingVcTimeout(state)).toBe(true);
+ });
+ });
+
+ describe('selectIsSendingVPTimeout', () => {
+ it('should return true when in startVPSharing.timeout state', () => {
+ const state = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'startVPSharing.timeout'),
+ };
+ expect(selectIsSendingVPTimeout(state)).toBe(true);
+ });
+ });
+
+ describe('selectIsSent', () => {
+ it('should return true when in reviewing.sendingVc.sent state', () => {
+ const state = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'reviewing.sendingVc.sent'),
+ };
+ expect(selectIsSent(state)).toBe(true);
+ });
+ });
+
+ describe('selectIsInvalid', () => {
+ it('should return true when in invalid state', () => {
+ const state = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'invalid'),
+ };
+ expect(selectIsInvalid(state)).toBe(true);
+ });
+ });
+
+ describe('selectIsLocationDenied', () => {
+ it('should return true when in checkingLocationState.denied state', () => {
+ const state = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'checkingLocationState.denied'),
+ };
+ expect(selectIsLocationDenied(state)).toBe(true);
+ });
+ });
+
+ describe('selectIsLocationDisabled', () => {
+ it('should return true when in checkingLocationState.disabled state', () => {
+ const state = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'checkingLocationState.disabled'),
+ };
+ expect(selectIsLocationDisabled(state)).toBe(true);
+ });
+ });
+
+ describe('selectIsShowQrLogin', () => {
+ it('should return true when in showQrLogin state', () => {
+ const state = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'showQrLogin'),
+ };
+ expect(selectIsShowQrLogin(state)).toBe(true);
+ });
+ });
+
+ describe('selectIsQrLoginDone', () => {
+ it('should return true when in showQrLogin.navigatingToHistory state', () => {
+ const state = {
+ ...mockState,
+ matches: jest.fn(
+ (s: string) => s === 'showQrLogin.navigatingToHistory',
+ ),
+ };
+ expect(selectIsQrLoginDone(state)).toBe(true);
+ });
+ });
+
+ describe('selectIsQrLoginDoneViaDeeplink', () => {
+ it('should return true when in showQrLogin.navigatingToHome state', () => {
+ const state = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'showQrLogin.navigatingToHome'),
+ };
+ expect(selectIsQrLoginDoneViaDeeplink(state)).toBe(true);
+ });
+ });
+
+ describe('selectIsQrLoginStoring', () => {
+ it('should return true when in showQrLogin.storing state', () => {
+ const state = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'showQrLogin.storing'),
+ };
+ expect(selectIsQrLoginStoring(state)).toBe(true);
+ });
+ });
+
+ describe('selectIsDone', () => {
+ it('should return true when in reviewing.disconnect state', () => {
+ const state = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'reviewing.disconnect'),
+ };
+ expect(selectIsDone(state)).toBe(true);
+ });
+ });
+
+ describe('selectIsMinimumStorageRequiredForAuditEntryLimitReached', () => {
+ it('should return true when in restrictSharingVc state', () => {
+ const state = {
+ ...mockState,
+ matches: jest.fn((s: string) => s === 'restrictSharingVc'),
+ };
+ expect(
+ selectIsMinimumStorageRequiredForAuditEntryLimitReached(state),
+ ).toBe(true);
+ });
+ });
+
+ describe('selectIsFaceVerificationConsent', () => {
+ it('should return true when in reviewing.faceVerificationConsent state', () => {
+ const state = {
+ ...mockState,
+ matches: jest.fn(
+ (s: string) => s === 'reviewing.faceVerificationConsent',
+ ),
+ };
+ expect(selectIsFaceVerificationConsent(state)).toBe(true);
+ });
+ });
+
+ describe('selectIsOVPViaDeepLink', () => {
+ it('should return isOVPViaDeepLink from context', () => {
+ const state = {
+ ...mockState,
+ context: {...mockState.context, isOVPViaDeepLink: true},
+ };
+ expect(selectIsOVPViaDeepLink(state)).toBe(true);
+ });
+ });
+});
diff --git a/machines/openID4VP/openID4VPModel.test.ts b/machines/openID4VP/openID4VPModel.test.ts
new file mode 100644
index 00000000..0bedccdd
--- /dev/null
+++ b/machines/openID4VP/openID4VPModel.test.ts
@@ -0,0 +1,350 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import {openID4VPModel} from './openID4VPModel';
+import {KeyTypes} from '../../shared/cryptoutil/KeyTypes';
+
+describe('openID4VPModel', () => {
+ describe('Model context', () => {
+ it('should initialize with default context values', () => {
+ const initialContext = openID4VPModel.initialContext;
+
+ expect(initialContext.urlEncodedAuthorizationRequest).toBe('');
+ expect(initialContext.checkedAll).toBe(false);
+ expect(initialContext.isShareWithSelfie).toBe(false);
+ expect(initialContext.showFaceAuthConsent).toBe(true);
+ expect(initialContext.purpose).toBe('');
+ expect(initialContext.error).toBe('');
+ expect(initialContext.publicKey).toBe('');
+ expect(initialContext.privateKey).toBe('');
+ expect(initialContext.keyType).toBe(KeyTypes.ED25519);
+ expect(initialContext.flowType).toBe('');
+ expect(initialContext.openID4VPRetryCount).toBe(0);
+ expect(initialContext.showFaceCaptureSuccessBanner).toBe(false);
+ expect(initialContext.isFaceVerificationRetryAttempt).toBe(false);
+ expect(initialContext.requestedClaims).toBe('');
+ expect(initialContext.showLoadingScreen).toBe(false);
+ expect(initialContext.isOVPViaDeepLink).toBe(false);
+ expect(initialContext.showTrustConsentModal).toBe(false);
+ });
+
+ it('should initialize with empty object contexts', () => {
+ const initialContext = openID4VPModel.initialContext;
+
+ expect(initialContext.authenticationResponse).toEqual({});
+ expect(initialContext.vcsMatchingAuthRequest).toEqual({});
+ expect(initialContext.selectedVCs).toEqual({});
+ expect(initialContext.selectedDisclosuresByVc).toEqual({});
+ expect(initialContext.miniViewSelectedVC).toEqual({});
+ });
+
+ it('should initialize with empty array contexts', () => {
+ const initialContext = openID4VPModel.initialContext;
+
+ expect(initialContext.trustedVerifiers).toEqual([]);
+ });
+ });
+
+ describe('Events', () => {
+ describe('AUTHENTICATE', () => {
+ it('should create AUTHENTICATE event with all parameters', () => {
+ const encodedAuthRequest = 'encoded_request_123';
+ const flowType = 'OpenID4VP';
+ const selectedVC = {id: 'vc123'};
+ const isOVPViaDeepLink = true;
+
+ const event = openID4VPModel.events.AUTHENTICATE(
+ encodedAuthRequest,
+ flowType,
+ selectedVC,
+ isOVPViaDeepLink,
+ );
+
+ expect(event.encodedAuthRequest).toBe(encodedAuthRequest);
+ expect(event.flowType).toBe(flowType);
+ expect(event.selectedVC).toEqual(selectedVC);
+ expect(event.isOVPViaDeepLink).toBe(isOVPViaDeepLink);
+ });
+
+ it('should create AUTHENTICATE event with false deeplink flag', () => {
+ const event = openID4VPModel.events.AUTHENTICATE(
+ 'request',
+ 'flow',
+ {},
+ false,
+ );
+
+ expect(event.isOVPViaDeepLink).toBe(false);
+ });
+ });
+
+ describe('DOWNLOADED_VCS', () => {
+ it('should create DOWNLOADED_VCS event with VCs array', () => {
+ const vcs: any[] = [
+ {id: 'vc1', credential: {}},
+ {id: 'vc2', credential: {}},
+ ];
+
+ const event = openID4VPModel.events.DOWNLOADED_VCS(vcs);
+
+ expect(event.vcs).toEqual(vcs);
+ expect(event.vcs.length).toBe(2);
+ });
+
+ it('should create DOWNLOADED_VCS event with empty array', () => {
+ const event = openID4VPModel.events.DOWNLOADED_VCS([]);
+
+ expect(event.vcs).toEqual([]);
+ });
+ });
+
+ describe('SELECT_VC', () => {
+ it('should create SELECT_VC event with vcKey and inputDescriptorId', () => {
+ const vcKey = 'vc_key_123';
+ const inputDescriptorId = 'descriptor_456';
+
+ const event = openID4VPModel.events.SELECT_VC(vcKey, inputDescriptorId);
+
+ expect(event.vcKey).toBe(vcKey);
+ expect(event.inputDescriptorId).toBe(inputDescriptorId);
+ });
+
+ it('should create SELECT_VC event with null inputDescriptorId', () => {
+ const event = openID4VPModel.events.SELECT_VC('key', null);
+
+ expect(event.vcKey).toBe('key');
+ expect(event.inputDescriptorId).toBeNull();
+ });
+ });
+
+ describe('ACCEPT_REQUEST', () => {
+ it('should create ACCEPT_REQUEST event with selectedVCs and disclosures', () => {
+ const selectedVCs = {
+ descriptor1: [{id: 'vc1'}] as any[],
+ descriptor2: [{id: 'vc2'}] as any[],
+ };
+ const selectedDisclosuresByVc = {
+ vc1: ['claim1', 'claim2'],
+ vc2: ['claim3'],
+ };
+
+ const event = openID4VPModel.events.ACCEPT_REQUEST(
+ selectedVCs,
+ selectedDisclosuresByVc,
+ );
+
+ expect(event.selectedVCs).toEqual(selectedVCs);
+ expect(event.selectedDisclosuresByVc).toEqual(selectedDisclosuresByVc);
+ });
+
+ it('should create ACCEPT_REQUEST event with empty objects', () => {
+ const event = openID4VPModel.events.ACCEPT_REQUEST({}, {});
+
+ expect(event.selectedVCs).toEqual({});
+ expect(event.selectedDisclosuresByVc).toEqual({});
+ });
+ });
+
+ describe('VERIFIER_TRUST_CONSENT_GIVEN', () => {
+ it('should create VERIFIER_TRUST_CONSENT_GIVEN event', () => {
+ const event = openID4VPModel.events.VERIFIER_TRUST_CONSENT_GIVEN();
+
+ expect(event.type).toBe('VERIFIER_TRUST_CONSENT_GIVEN');
+ });
+ });
+
+ describe('VERIFY_AND_ACCEPT_REQUEST', () => {
+ it('should create VERIFY_AND_ACCEPT_REQUEST event with selectedVCs and disclosures', () => {
+ const selectedVCs = {descriptor1: [{id: 'vc1'}] as any[]};
+ const selectedDisclosuresByVc = {vc1: ['claim1']};
+
+ const event = openID4VPModel.events.VERIFY_AND_ACCEPT_REQUEST(
+ selectedVCs,
+ selectedDisclosuresByVc,
+ );
+
+ expect(event.selectedVCs).toEqual(selectedVCs);
+ expect(event.selectedDisclosuresByVc).toEqual(selectedDisclosuresByVc);
+ });
+ });
+
+ describe('CONFIRM', () => {
+ it('should create CONFIRM event', () => {
+ const event = openID4VPModel.events.CONFIRM();
+
+ expect(event.type).toBe('CONFIRM');
+ });
+ });
+
+ describe('CANCEL', () => {
+ it('should create CANCEL event', () => {
+ const event = openID4VPModel.events.CANCEL();
+
+ expect(event.type).toBe('CANCEL');
+ });
+ });
+
+ describe('FACE_VERIFICATION_CONSENT', () => {
+ it('should create FACE_VERIFICATION_CONSENT event with checked flag true', () => {
+ const event = openID4VPModel.events.FACE_VERIFICATION_CONSENT(true);
+
+ expect(event.isDoNotAskAgainChecked).toBe(true);
+ });
+
+ it('should create FACE_VERIFICATION_CONSENT event with checked flag false', () => {
+ const event = openID4VPModel.events.FACE_VERIFICATION_CONSENT(false);
+
+ expect(event.isDoNotAskAgainChecked).toBe(false);
+ });
+ });
+
+ describe('FACE_VALID', () => {
+ it('should create FACE_VALID event', () => {
+ const event = openID4VPModel.events.FACE_VALID();
+
+ expect(event.type).toBe('FACE_VALID');
+ });
+ });
+
+ describe('FACE_INVALID', () => {
+ it('should create FACE_INVALID event', () => {
+ const event = openID4VPModel.events.FACE_INVALID();
+
+ expect(event.type).toBe('FACE_INVALID');
+ });
+ });
+
+ describe('DISMISS', () => {
+ it('should create DISMISS event', () => {
+ const event = openID4VPModel.events.DISMISS();
+
+ expect(event.type).toBe('DISMISS');
+ });
+ });
+
+ describe('DISMISS_POPUP', () => {
+ it('should create DISMISS_POPUP event', () => {
+ const event = openID4VPModel.events.DISMISS_POPUP();
+
+ expect(event.type).toBe('DISMISS_POPUP');
+ });
+ });
+
+ describe('RETRY_VERIFICATION', () => {
+ it('should create RETRY_VERIFICATION event', () => {
+ const event = openID4VPModel.events.RETRY_VERIFICATION();
+
+ expect(event.type).toBe('RETRY_VERIFICATION');
+ });
+ });
+
+ describe('STORE_RESPONSE', () => {
+ it('should create STORE_RESPONSE event with response object', () => {
+ const response = {
+ status: 'success',
+ data: {id: '123'},
+ };
+
+ const event = openID4VPModel.events.STORE_RESPONSE(response);
+
+ expect(event.response).toEqual(response);
+ });
+
+ it('should create STORE_RESPONSE event with null response', () => {
+ const event = openID4VPModel.events.STORE_RESPONSE(null);
+
+ expect(event.response).toBeNull();
+ });
+ });
+
+ describe('GO_BACK', () => {
+ it('should create GO_BACK event', () => {
+ const event = openID4VPModel.events.GO_BACK();
+
+ expect(event.type).toBe('GO_BACK');
+ });
+ });
+
+ describe('CHECK_SELECTED_VC', () => {
+ it('should create CHECK_SELECTED_VC event', () => {
+ const event = openID4VPModel.events.CHECK_SELECTED_VC();
+
+ expect(event.type).toBe('CHECK_SELECTED_VC');
+ });
+ });
+
+ describe('SET_SELECTED_VC', () => {
+ it('should create SET_SELECTED_VC event', () => {
+ const event = openID4VPModel.events.SET_SELECTED_VC();
+
+ expect(event.type).toBe('SET_SELECTED_VC');
+ });
+ });
+
+ describe('CHECK_FOR_IMAGE', () => {
+ it('should create CHECK_FOR_IMAGE event', () => {
+ const event = openID4VPModel.events.CHECK_FOR_IMAGE();
+
+ expect(event.type).toBe('CHECK_FOR_IMAGE');
+ });
+ });
+
+ describe('RETRY', () => {
+ it('should create RETRY event', () => {
+ const event = openID4VPModel.events.RETRY();
+
+ expect(event.type).toBe('RETRY');
+ });
+ });
+
+ describe('RESET_RETRY_COUNT', () => {
+ it('should create RESET_RETRY_COUNT event', () => {
+ const event = openID4VPModel.events.RESET_RETRY_COUNT();
+
+ expect(event.type).toBe('RESET_RETRY_COUNT');
+ });
+ });
+
+ describe('RESET_ERROR', () => {
+ it('should create RESET_ERROR event', () => {
+ const event = openID4VPModel.events.RESET_ERROR();
+
+ expect(event.type).toBe('RESET_ERROR');
+ });
+ });
+
+ describe('CLOSE_BANNER', () => {
+ it('should create CLOSE_BANNER event', () => {
+ const event = openID4VPModel.events.CLOSE_BANNER();
+
+ expect(event.type).toBe('CLOSE_BANNER');
+ });
+ });
+
+ describe('LOG_ACTIVITY', () => {
+ it('should create LOG_ACTIVITY event with SHARED_SUCCESSFULLY logType', () => {
+ const event = openID4VPModel.events.LOG_ACTIVITY('SHARED_SUCCESSFULLY');
+
+ expect(event.logType).toBe('SHARED_SUCCESSFULLY');
+ });
+
+ it('should create LOG_ACTIVITY event with USER_DECLINED_CONSENT logType', () => {
+ const event = openID4VPModel.events.LOG_ACTIVITY(
+ 'USER_DECLINED_CONSENT',
+ );
+
+ expect(event.logType).toBe('USER_DECLINED_CONSENT');
+ });
+
+ it('should create LOG_ACTIVITY event with TECHNICAL_ERROR logType', () => {
+ const event = openID4VPModel.events.LOG_ACTIVITY('TECHNICAL_ERROR');
+
+ expect(event.logType).toBe('TECHNICAL_ERROR');
+ });
+
+ it('should create LOG_ACTIVITY event with empty string logType', () => {
+ const event = openID4VPModel.events.LOG_ACTIVITY('');
+
+ expect(event.logType).toBe('');
+ });
+ });
+ });
+});
diff --git a/machines/openID4VP/openID4VPSelectors.test.ts b/machines/openID4VP/openID4VPSelectors.test.ts
new file mode 100644
index 00000000..aad10ad3
--- /dev/null
+++ b/machines/openID4VP/openID4VPSelectors.test.ts
@@ -0,0 +1,637 @@
+import {
+ selectIsGetVCsSatisfyingAuthRequest,
+ selectVCsMatchingAuthRequest,
+ selectSelectedVCs,
+ selectAreAllVCsChecked,
+ selectIsGetVPSharingConsent,
+ selectIsFaceVerificationConsent,
+ selectIsVerifyingIdentity,
+ selectIsInvalidIdentity,
+ selectIsSharingVP,
+ selectIsShowLoadingScreen,
+ selectCredentials,
+ selectVerifiableCredentialsData,
+ selectPurpose,
+ selectShowConfirmationPopup,
+ selectIsSelectingVcs,
+ selectIsError,
+ selectOpenID4VPRetryCount,
+ selectIsOVPViaDeeplink,
+ selectIsFaceVerifiedInVPSharing,
+ selectVerifierNameInVPSharing,
+ selectRequestedClaimsByVerifier,
+ selectshowTrustConsentModal,
+ selectVerifierNameInTrustModal,
+ selectVerifierLogoInTrustModal,
+} from './openID4VPSelectors';
+
+describe('openID4VPSelectors', () => {
+ describe('selectIsGetVCsSatisfyingAuthRequest', () => {
+ it('should return true when in getVCsSatisfyingAuthRequest state', () => {
+ const state = {
+ matches: jest.fn(() => true),
+ context: {},
+ } as any;
+
+ expect(selectIsGetVCsSatisfyingAuthRequest(state)).toBe(true);
+ expect(state.matches).toHaveBeenCalledWith('getVCsSatisfyingAuthRequest');
+ });
+ });
+
+ describe('selectVCsMatchingAuthRequest', () => {
+ it('should return vcsMatchingAuthRequest from context', () => {
+ const mockVCs = [{id: 'vc1'}, {id: 'vc2'}];
+ const state = {
+ context: {
+ vcsMatchingAuthRequest: mockVCs,
+ },
+ } as any;
+
+ expect(selectVCsMatchingAuthRequest(state)).toEqual(mockVCs);
+ });
+ });
+
+ describe('selectSelectedVCs', () => {
+ it('should return selectedVCs from context', () => {
+ const mockSelectedVCs = {vc1: {data: 'test'}};
+ const state = {
+ context: {
+ selectedVCs: mockSelectedVCs,
+ },
+ } as any;
+
+ expect(selectSelectedVCs(state)).toEqual(mockSelectedVCs);
+ });
+ });
+
+ describe('selectAreAllVCsChecked', () => {
+ it('should return checkedAll from context', () => {
+ const state = {
+ context: {
+ checkedAll: true,
+ },
+ } as any;
+
+ expect(selectAreAllVCsChecked(state)).toBe(true);
+ });
+ });
+
+ describe('selectIsGetVPSharingConsent', () => {
+ it('should return true when in getConsentForVPSharing state', () => {
+ const state = {
+ matches: jest.fn(() => true),
+ context: {},
+ } as any;
+
+ expect(selectIsGetVPSharingConsent(state)).toBe(true);
+ expect(state.matches).toHaveBeenCalledWith('getConsentForVPSharing');
+ });
+ });
+
+ describe('selectIsFaceVerificationConsent', () => {
+ it('should return true when in faceVerificationConsent state', () => {
+ const state = {
+ matches: jest.fn(() => true),
+ context: {},
+ } as any;
+
+ expect(selectIsFaceVerificationConsent(state)).toBe(true);
+ expect(state.matches).toHaveBeenCalledWith('faceVerificationConsent');
+ });
+ });
+
+ describe('selectIsVerifyingIdentity', () => {
+ it('should return true when in verifyingIdentity state', () => {
+ const state = {
+ matches: jest.fn(() => true),
+ context: {},
+ } as any;
+
+ expect(selectIsVerifyingIdentity(state)).toBe(true);
+ expect(state.matches).toHaveBeenCalledWith('verifyingIdentity');
+ });
+ });
+
+ describe('selectIsInvalidIdentity', () => {
+ it('should return true when in invalidIdentity state', () => {
+ const state = {
+ matches: jest.fn(() => true),
+ context: {},
+ } as any;
+
+ expect(selectIsInvalidIdentity(state)).toBe(true);
+ expect(state.matches).toHaveBeenCalledWith('invalidIdentity');
+ });
+ });
+
+ describe('selectIsSharingVP', () => {
+ it('should return true when in sendingVP state', () => {
+ const state = {
+ matches: jest.fn(() => true),
+ context: {},
+ } as any;
+
+ expect(selectIsSharingVP(state)).toBe(true);
+ expect(state.matches).toHaveBeenCalledWith('sendingVP');
+ });
+ });
+
+ describe('selectIsShowLoadingScreen', () => {
+ it('should return showLoadingScreen from context', () => {
+ const state = {
+ context: {
+ showLoadingScreen: true,
+ },
+ } as any;
+
+ expect(selectIsShowLoadingScreen(state)).toBe(true);
+ });
+ });
+
+ describe('selectCredentials', () => {
+ it('should return empty array when selectedVCs is empty', () => {
+ const state = {
+ context: {
+ selectedVCs: {},
+ },
+ } as any;
+
+ expect(selectCredentials(state)).toEqual([]);
+ });
+
+ it('should process and return credentials from selectedVCs', () => {
+ const mockCredential = {data: 'test'};
+ const state = {
+ context: {
+ selectedVCs: {
+ key1: {
+ inner1: [
+ {
+ verifiableCredential: {
+ credential: mockCredential,
+ },
+ },
+ ],
+ },
+ },
+ },
+ } as any;
+
+ const result = selectCredentials(state);
+ expect(result).toEqual([mockCredential]);
+ });
+
+ it('should handle credentials without nested credential property', () => {
+ const mockCredential = {data: 'test'};
+ const state = {
+ context: {
+ selectedVCs: {
+ key1: {
+ inner1: [
+ {
+ verifiableCredential: mockCredential,
+ },
+ ],
+ },
+ },
+ },
+ } as any;
+
+ const result = selectCredentials(state);
+ expect(result).toEqual([mockCredential]);
+ });
+ });
+
+ describe('selectVerifiableCredentialsData', () => {
+ it('should return empty array when no selectedVCs', () => {
+ const state = {
+ context: {
+ selectedVCs: {},
+ },
+ } as any;
+
+ expect(selectVerifiableCredentialsData(state)).toEqual([]);
+ });
+
+ it('should process and return verifiable credentials data', () => {
+ const state = {
+ context: {
+ selectedVCs: {
+ key1: {
+ inner1: [
+ {
+ vcMetadata: {
+ issuer: 'TestIssuer',
+ },
+ verifiableCredential: {
+ issuerLogo: 'logo.png',
+ wellKnown: 'wellknown',
+ credentialTypes: ['type1'],
+ credential: {
+ credentialSubject: {
+ face: 'faceData',
+ },
+ },
+ },
+ },
+ ],
+ },
+ },
+ },
+ } as any;
+
+ const result = selectVerifiableCredentialsData(state);
+ expect(result).toHaveLength(1);
+ expect(result[0].issuer).toBe('TestIssuer');
+ expect(result[0].face).toBe('faceData');
+ });
+ });
+
+ describe('selectPurpose', () => {
+ it('should return purpose from context', () => {
+ const state = {
+ context: {
+ purpose: 'verification',
+ },
+ } as any;
+
+ expect(selectPurpose(state)).toBe('verification');
+ });
+ });
+
+ describe('selectShowConfirmationPopup', () => {
+ it('should return true when in showConfirmationPopup state', () => {
+ const state = {
+ matches: jest.fn(() => true),
+ context: {},
+ } as any;
+
+ expect(selectShowConfirmationPopup(state)).toBe(true);
+ expect(state.matches).toHaveBeenCalledWith('showConfirmationPopup');
+ });
+ });
+
+ describe('selectIsSelectingVcs', () => {
+ it('should return true when in selectingVCs state', () => {
+ const state = {
+ matches: jest.fn(() => true),
+ context: {},
+ } as any;
+
+ expect(selectIsSelectingVcs(state)).toBe(true);
+ expect(state.matches).toHaveBeenCalledWith('selectingVCs');
+ });
+ });
+
+ describe('selectIsError', () => {
+ it('should return error from context', () => {
+ const mockError = new Error('Test error');
+ const state = {
+ context: {
+ error: mockError,
+ },
+ } as any;
+
+ expect(selectIsError(state)).toBe(mockError);
+ });
+ });
+
+ describe('selectOpenID4VPRetryCount', () => {
+ it('should return openID4VPRetryCount from context', () => {
+ const state = {
+ context: {
+ openID4VPRetryCount: 3,
+ },
+ } as any;
+
+ expect(selectOpenID4VPRetryCount(state)).toBe(3);
+ });
+ });
+
+ describe('selectIsOVPViaDeeplink', () => {
+ it('should return isOVPViaDeepLink from context', () => {
+ const state = {
+ context: {
+ isOVPViaDeepLink: true,
+ },
+ } as any;
+
+ expect(selectIsOVPViaDeeplink(state)).toBe(true);
+ });
+ });
+
+ describe('selectIsFaceVerifiedInVPSharing', () => {
+ it('should return true when in sendingVP state and banner shown', () => {
+ const state = {
+ matches: jest.fn(() => true),
+ context: {
+ showFaceCaptureSuccessBanner: true,
+ },
+ } as any;
+
+ expect(selectIsFaceVerifiedInVPSharing(state)).toBe(true);
+ expect(state.matches).toHaveBeenCalledWith('sendingVP');
+ });
+
+ it('should return false when not in sendingVP state', () => {
+ const state = {
+ matches: jest.fn(() => false),
+ context: {
+ showFaceCaptureSuccessBanner: true,
+ },
+ } as any;
+
+ expect(selectIsFaceVerifiedInVPSharing(state)).toBe(false);
+ });
+ });
+
+ describe('selectVerifierNameInVPSharing', () => {
+ it('should return client_name from client_metadata', () => {
+ const state = {
+ context: {
+ authenticationResponse: {
+ client_metadata: {
+ client_name: 'TestVerifier',
+ },
+ },
+ },
+ } as any;
+
+ expect(selectVerifierNameInVPSharing(state)).toBe('TestVerifier');
+ });
+
+ it('should return client_id when client_name not available', () => {
+ const state = {
+ context: {
+ authenticationResponse: {
+ client_id: 'verifier123',
+ },
+ },
+ } as any;
+
+ expect(selectVerifierNameInVPSharing(state)).toBe('verifier123');
+ });
+ });
+
+ describe('selectRequestedClaimsByVerifier', () => {
+ it('should return requestedClaims from context', () => {
+ const mockClaims = ['name', 'age'];
+ const state = {
+ context: {
+ requestedClaims: mockClaims,
+ },
+ } as any;
+
+ expect(selectRequestedClaimsByVerifier(state)).toEqual(mockClaims);
+ });
+ });
+
+ describe('selectshowTrustConsentModal', () => {
+ it('should return showTrustConsentModal from context', () => {
+ const state = {
+ context: {
+ showTrustConsentModal: true,
+ },
+ } as any;
+
+ expect(selectshowTrustConsentModal(state)).toBe(true);
+ });
+ });
+
+ describe('selectVerifierNameInTrustModal', () => {
+ it('should return client_name from client_metadata', () => {
+ const state = {
+ context: {
+ authenticationResponse: {
+ client_metadata: {
+ client_name: 'TrustedVerifier',
+ },
+ },
+ },
+ } as any;
+
+ expect(selectVerifierNameInTrustModal(state)).toBe('TrustedVerifier');
+ });
+ });
+
+ describe('selectVerifierLogoInTrustModal', () => {
+ it('should return logo_uri from client_metadata', () => {
+ const state = {
+ context: {
+ authenticationResponse: {
+ client_metadata: {
+ logo_uri: 'https://example.com/logo.png',
+ },
+ },
+ },
+ } as any;
+
+ expect(selectVerifierLogoInTrustModal(state)).toBe(
+ 'https://example.com/logo.png',
+ );
+ });
+ });
+
+ describe('selectCredentials', () => {
+ it('should return array of credentials from selectedVCs', () => {
+ const mockState: any = {
+ context: {
+ selectedVCs: {
+ type1: {
+ vc1: {
+ verifiableCredential: {
+ credential: {id: 'cred-1', name: 'John'},
+ },
+ },
+ },
+ type2: {
+ vc2: {
+ verifiableCredential: {id: 'cred-2', name: 'Jane'},
+ },
+ },
+ },
+ },
+ };
+
+ const result: any = selectCredentials(mockState);
+ expect(Array.isArray(result)).toBe(true);
+ expect(result.length).toBe(2);
+ });
+
+ it('should handle verifiableCredential without credential property', () => {
+ const mockState: any = {
+ context: {
+ selectedVCs: {
+ type1: {
+ vc1: {
+ verifiableCredential: {id: 'direct-vc', name: 'Direct'},
+ },
+ },
+ },
+ },
+ };
+
+ const result: any = selectCredentials(mockState);
+ expect(result.length).toBe(1);
+ });
+
+ it('should return undefined when no credentials selected', () => {
+ const mockState: any = {
+ context: {
+ selectedVCs: {},
+ },
+ };
+
+ const result: any = selectCredentials(mockState);
+ expect(Array.isArray(result) ? result.length : result).toBe(0);
+ });
+
+ it('should flatten nested credential structures', () => {
+ const mockState: any = {
+ context: {
+ selectedVCs: {
+ type1: {
+ vc1: {verifiableCredential: {credential: {id: '1'}}},
+ vc2: {verifiableCredential: {credential: {id: '2'}}},
+ },
+ type2: {
+ vc3: {verifiableCredential: {credential: {id: '3'}}},
+ },
+ },
+ },
+ };
+
+ const result: any = selectCredentials(mockState);
+ expect(result.length).toBe(3);
+ });
+ });
+
+ describe('selectVerifiableCredentialsData', () => {
+ it('should return array of formatted verifiable credential data', () => {
+ const mockState: any = {
+ context: {
+ selectedVCs: {
+ type1: {
+ vc1: {
+ vcMetadata: {
+ id: 'vc-001',
+ issuer: 'Test Issuer',
+ },
+ verifiableCredential: {
+ issuerLogo: 'https://example.com/logo.png',
+ wellKnown: 'https://example.com/.well-known',
+ credentialTypes: ['VerifiableCredential', 'NationalID'],
+ credential: {
+ credentialSubject: {
+ face: 'base64-image-data',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ };
+
+ const result = selectVerifiableCredentialsData(mockState);
+ expect(Array.isArray(result)).toBe(true);
+ expect(result.length).toBe(1);
+ expect(result[0].issuer).toBe('Test Issuer');
+ expect(result[0].issuerLogo).toBe('https://example.com/logo.png');
+ expect(result[0].wellKnown).toBe('https://example.com/.well-known');
+ });
+
+ it('should use getMosipLogo when issuerLogo is not available', () => {
+ const mockState: any = {
+ context: {
+ selectedVCs: {
+ type1: {
+ vc1: {
+ vcMetadata: {
+ id: 'vc-002',
+ issuer: 'Mosip',
+ },
+ verifiableCredential: {
+ credential: {
+ credentialSubject: {},
+ },
+ },
+ },
+ },
+ },
+ },
+ };
+
+ const result = selectVerifiableCredentialsData(mockState);
+ expect(result[0].issuerLogo).toBeDefined();
+ });
+
+ it('should handle face from credential.biometrics.face', () => {
+ const mockState: any = {
+ context: {
+ selectedVCs: {
+ type1: {
+ vc1: {
+ vcMetadata: {
+ id: 'vc-003',
+ issuer: 'Issuer',
+ },
+ credential: {
+ biometrics: {
+ face: 'biometric-face-data',
+ },
+ },
+ verifiableCredential: {},
+ },
+ },
+ },
+ },
+ };
+
+ const result = selectVerifiableCredentialsData(mockState);
+ expect(result[0].face).toBe('biometric-face-data');
+ });
+
+ it('should handle empty selectedVCs', () => {
+ const mockState: any = {
+ context: {
+ selectedVCs: {},
+ },
+ };
+
+ const result = selectVerifiableCredentialsData(mockState);
+ expect(result).toEqual([]);
+ });
+
+ it('should handle multiple VCs from different types', () => {
+ const mockState: any = {
+ context: {
+ selectedVCs: {
+ type1: {
+ vc1: {
+ vcMetadata: {id: '1', issuer: 'Issuer1'},
+ verifiableCredential: {},
+ },
+ },
+ type2: {
+ vc2: {
+ vcMetadata: {id: '2', issuer: 'Issuer2'},
+ verifiableCredential: {},
+ },
+ vc3: {
+ vcMetadata: {id: '3', issuer: 'Issuer3'},
+ verifiableCredential: {},
+ },
+ },
+ },
+ },
+ };
+
+ const result = selectVerifiableCredentialsData(mockState);
+ expect(result.length).toBe(3);
+ expect(result[0].issuer).toBe('Issuer1');
+ expect(result[1].issuer).toBe('Issuer2');
+ expect(result[2].issuer).toBe('Issuer3');
+ });
+ });
+});
diff --git a/routes/routesConstants.test.ts b/routes/routesConstants.test.ts
new file mode 100644
index 00000000..9dbf9348
--- /dev/null
+++ b/routes/routesConstants.test.ts
@@ -0,0 +1,139 @@
+import {
+ BOTTOM_TAB_ROUTES,
+ SCAN_ROUTES,
+ REQUEST_ROUTES,
+ SETTINGS_ROUTES,
+ AUTH_ROUTES,
+} from './routesConstants';
+
+describe('routesConstants', () => {
+ describe('BOTTOM_TAB_ROUTES', () => {
+ it('should be defined', () => {
+ expect(BOTTOM_TAB_ROUTES).toBeDefined();
+ });
+
+ it('should have home route', () => {
+ expect(BOTTOM_TAB_ROUTES.home).toBe('home');
+ });
+
+ it('should have share route', () => {
+ expect(BOTTOM_TAB_ROUTES.share).toBe('share');
+ });
+
+ it('should have history route', () => {
+ expect(BOTTOM_TAB_ROUTES.history).toBe('history');
+ });
+
+ it('should have settings route', () => {
+ expect(BOTTOM_TAB_ROUTES.settings).toBe('settings');
+ });
+
+ it('should have exactly 4 routes', () => {
+ expect(Object.keys(BOTTOM_TAB_ROUTES)).toHaveLength(4);
+ });
+ });
+
+ describe('SCAN_ROUTES', () => {
+ it('should be defined', () => {
+ expect(SCAN_ROUTES).toBeDefined();
+ });
+
+ it('should have ScanScreen route', () => {
+ expect(SCAN_ROUTES.ScanScreen).toBe('ScanScreen');
+ });
+
+ it('should have SendVcScreen route', () => {
+ expect(SCAN_ROUTES.SendVcScreen).toBe('SendVcScreen');
+ });
+
+ it('should have SendVPScreen route', () => {
+ expect(SCAN_ROUTES.SendVPScreen).toBe('SendVPScreen');
+ });
+
+ it('should have exactly 3 routes', () => {
+ expect(Object.keys(SCAN_ROUTES)).toHaveLength(3);
+ });
+ });
+
+ describe('REQUEST_ROUTES', () => {
+ it('should be defined', () => {
+ expect(REQUEST_ROUTES).toBeDefined();
+ });
+
+ it('should have Request route', () => {
+ expect(REQUEST_ROUTES.Request).toBe('Request');
+ });
+
+ it('should have RequestScreen route', () => {
+ expect(REQUEST_ROUTES.RequestScreen).toBe('RequestScreen');
+ });
+
+ it('should have ReceiveVcScreen route', () => {
+ expect(REQUEST_ROUTES.ReceiveVcScreen).toBe('ReceiveVcScreen');
+ });
+
+ it('should have exactly 3 routes', () => {
+ expect(Object.keys(REQUEST_ROUTES)).toHaveLength(3);
+ });
+ });
+
+ describe('SETTINGS_ROUTES', () => {
+ it('should be defined', () => {
+ expect(SETTINGS_ROUTES).toBeDefined();
+ });
+
+ it('should have KeyManagement route', () => {
+ expect(SETTINGS_ROUTES.KeyManagement).toBe('KeyManagement');
+ });
+
+ it('should have exactly 1 route', () => {
+ expect(Object.keys(SETTINGS_ROUTES)).toHaveLength(1);
+ });
+ });
+
+ describe('AUTH_ROUTES', () => {
+ it('should be defined', () => {
+ expect(AUTH_ROUTES).toBeDefined();
+ });
+
+ it('should have AuthView route', () => {
+ expect(AUTH_ROUTES.AuthView).toBe('AuthView');
+ });
+
+ it('should have exactly 1 route', () => {
+ expect(Object.keys(AUTH_ROUTES)).toHaveLength(1);
+ });
+ });
+
+ describe('Route constants structure', () => {
+ it('all BOTTOM_TAB_ROUTES values should be strings', () => {
+ Object.values(BOTTOM_TAB_ROUTES).forEach(route => {
+ expect(typeof route).toBe('string');
+ });
+ });
+
+ it('all SCAN_ROUTES values should be strings', () => {
+ Object.values(SCAN_ROUTES).forEach(route => {
+ expect(typeof route).toBe('string');
+ });
+ });
+
+ it('all REQUEST_ROUTES values should be strings', () => {
+ Object.values(REQUEST_ROUTES).forEach(route => {
+ expect(typeof route).toBe('string');
+ });
+ });
+
+ it('all SETTINGS_ROUTES values should be strings', () => {
+ Object.values(SETTINGS_ROUTES).forEach(route => {
+ expect(typeof route).toBe('string');
+ });
+ });
+
+ it('all AUTH_ROUTES values should be strings', () => {
+ Object.values(AUTH_ROUTES).forEach(route => {
+ expect(typeof route).toBe('string');
+ });
+ });
+ });
+});
diff --git a/shared/Utils.test.ts b/shared/Utils.test.ts
new file mode 100644
index 00000000..fb29ffa8
--- /dev/null
+++ b/shared/Utils.test.ts
@@ -0,0 +1,405 @@
+import {
+ getVCsOrderedByPinStatus,
+ VCShareFlowType,
+ VCItemContainerFlowType,
+ CameraPosition,
+ isMosipVC,
+ parseJSON,
+ isNetworkError,
+ UUID,
+ formatTextWithGivenLimit,
+ DEEPLINK_FLOWS,
+ base64ToByteArray,
+ createCacheObject,
+ isCacheExpired,
+ getVerifierKey,
+} from './Utils';
+import {VCMetadata} from './VCMetadata';
+
+describe('getVCsOrderedByPinStatus', () => {
+ it('should be defined', () => {
+ expect(getVCsOrderedByPinStatus).toBeDefined();
+ });
+
+ it('should return pinned VCs first', () => {
+ const vcMetadatas = [
+ new VCMetadata({id: '1', isPinned: false}),
+ new VCMetadata({id: '2', isPinned: true}),
+ new VCMetadata({id: '3', isPinned: false}),
+ new VCMetadata({id: '4', isPinned: true}),
+ ];
+
+ const result = getVCsOrderedByPinStatus(vcMetadatas);
+
+ expect(result[0].isPinned).toBe(true);
+ expect(result[1].isPinned).toBe(true);
+ expect(result[2].isPinned).toBe(false);
+ expect(result[3].isPinned).toBe(false);
+ });
+
+ it('should handle empty array', () => {
+ const result = getVCsOrderedByPinStatus([]);
+ expect(result).toEqual([]);
+ });
+
+ it('should handle all pinned VCs', () => {
+ const vcMetadatas = [
+ new VCMetadata({id: '1', isPinned: true}),
+ new VCMetadata({id: '2', isPinned: true}),
+ ];
+
+ const result = getVCsOrderedByPinStatus(vcMetadatas);
+ expect(result.every(vc => vc.isPinned)).toBe(true);
+ });
+
+ it('should handle all unpinned VCs', () => {
+ const vcMetadatas = [
+ new VCMetadata({id: '1', isPinned: false}),
+ new VCMetadata({id: '2', isPinned: false}),
+ ];
+
+ const result = getVCsOrderedByPinStatus(vcMetadatas);
+ expect(result.every(vc => !vc.isPinned)).toBe(true);
+ });
+});
+
+describe('VCShareFlowType enum', () => {
+ it('should have SIMPLE_SHARE defined', () => {
+ expect(VCShareFlowType.SIMPLE_SHARE).toBe('simple share');
+ });
+
+ it('should have MINI_VIEW_SHARE defined', () => {
+ expect(VCShareFlowType.MINI_VIEW_SHARE).toBe('mini view share');
+ });
+
+ it('should have MINI_VIEW_SHARE_WITH_SELFIE defined', () => {
+ expect(VCShareFlowType.MINI_VIEW_SHARE_WITH_SELFIE).toBe(
+ 'mini view share with selfie',
+ );
+ });
+
+ it('should have MINI_VIEW_QR_LOGIN defined', () => {
+ expect(VCShareFlowType.MINI_VIEW_QR_LOGIN).toBe('mini view qr login');
+ });
+
+ it('should have OPENID4VP defined', () => {
+ expect(VCShareFlowType.OPENID4VP).toBe('OpenID4VP');
+ });
+
+ it('should have MINI_VIEW_SHARE_OPENID4VP defined', () => {
+ expect(VCShareFlowType.MINI_VIEW_SHARE_OPENID4VP).toBe(
+ 'OpenID4VP share from mini view',
+ );
+ });
+
+ it('should have MINI_VIEW_SHARE_WITH_SELFIE_OPENID4VP defined', () => {
+ expect(VCShareFlowType.MINI_VIEW_SHARE_WITH_SELFIE_OPENID4VP).toBe(
+ 'OpenID4VP share with selfie from mini view',
+ );
+ });
+});
+
+describe('VCItemContainerFlowType enum', () => {
+ it('should have QR_LOGIN defined', () => {
+ expect(VCItemContainerFlowType.QR_LOGIN).toBe('qr login');
+ });
+
+ it('should have VC_SHARE defined', () => {
+ expect(VCItemContainerFlowType.VC_SHARE).toBe('vc share');
+ });
+
+ it('should have VP_SHARE defined', () => {
+ expect(VCItemContainerFlowType.VP_SHARE).toBe('vp share');
+ });
+});
+
+describe('CameraPosition enum', () => {
+ it('should have FRONT defined', () => {
+ expect(CameraPosition.FRONT).toBe('front');
+ });
+
+ it('should have BACK defined', () => {
+ expect(CameraPosition.BACK).toBe('back');
+ });
+});
+
+describe('isMosipVC', () => {
+ it('should be defined', () => {
+ expect(isMosipVC).toBeDefined();
+ });
+
+ it('should return true for Mosip issuer', () => {
+ const result = isMosipVC('Mosip');
+ expect(result).toBe(true);
+ });
+
+ it('should return true for MosipOtp issuer', () => {
+ const result = isMosipVC('MosipOtp');
+ expect(result).toBe(true);
+ });
+
+ it('should return false for other issuers', () => {
+ expect(isMosipVC('SomeOtherIssuer')).toBe(false);
+ expect(isMosipVC('')).toBe(false);
+ expect(isMosipVC('mosip')).toBe(false);
+ });
+});
+
+describe('parseJSON', () => {
+ it('should be defined', () => {
+ expect(parseJSON).toBeDefined();
+ });
+
+ it('should parse valid JSON string', () => {
+ const jsonStr = '{"key": "value"}';
+ const result = parseJSON(jsonStr);
+ expect(result).toEqual({key: 'value'});
+ });
+
+ it('should handle object input', () => {
+ const obj = {key: 'value'};
+ const result = parseJSON(obj);
+ expect(result).toEqual({key: 'value'});
+ });
+
+ it('should handle invalid JSON gracefully', () => {
+ const invalidJson = '{invalid json}';
+ const result = parseJSON(invalidJson);
+ expect(result).toBeDefined();
+ });
+
+ it('should handle nested objects', () => {
+ const jsonStr = '{"key": {"nested": "value"}}';
+ const result = parseJSON(jsonStr);
+ expect(result.key.nested).toBe('value');
+ });
+});
+
+describe('isNetworkError', () => {
+ it('should be defined', () => {
+ expect(isNetworkError).toBeDefined();
+ });
+
+ it('should return true for network request failed error', () => {
+ const error = 'Network request failed';
+ expect(isNetworkError(error)).toBe(true);
+ });
+
+ it('should return false for other errors', () => {
+ expect(isNetworkError('Some other error')).toBe(false);
+ expect(isNetworkError('')).toBe(false);
+ });
+
+ it('should handle partial matches', () => {
+ const error = 'Error: Network request failed - timeout';
+ expect(isNetworkError(error)).toBe(true);
+ });
+});
+
+describe('UUID', () => {
+ it('should be defined', () => {
+ expect(UUID).toBeDefined();
+ });
+
+ it('should generate a valid UUID', () => {
+ const uuid = UUID.generate();
+ expect(uuid).toBeDefined();
+ expect(typeof uuid).toBe('string');
+ expect(uuid.length).toBeGreaterThan(0);
+ });
+
+ it('should generate unique UUIDs', () => {
+ const uuid1 = UUID.generate();
+ const uuid2 = UUID.generate();
+ expect(uuid1).not.toBe(uuid2);
+ });
+
+ it('should match UUID format', () => {
+ const uuid = UUID.generate();
+ const uuidRegex =
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
+ expect(uuidRegex.test(uuid)).toBe(true);
+ });
+});
+
+describe('formatTextWithGivenLimit', () => {
+ it('should be defined', () => {
+ expect(formatTextWithGivenLimit).toBeDefined();
+ });
+
+ it('should truncate text longer than limit', () => {
+ const text = 'This is a very long text';
+ const result = formatTextWithGivenLimit(text, 10);
+ expect(result).toBe('This is a ...');
+ });
+
+ it('should return text as is if shorter than limit', () => {
+ const text = 'Short text';
+ const result = formatTextWithGivenLimit(text, 15);
+ expect(result).toBe('Short text');
+ });
+
+ it('should use default limit of 15 if not provided', () => {
+ const text = 'This is a longer text than 15 characters';
+ const result = formatTextWithGivenLimit(text);
+ expect(result).toBe('This is a longe...');
+ });
+
+ it('should handle empty string', () => {
+ const result = formatTextWithGivenLimit('', 10);
+ expect(result).toBe('');
+ });
+
+ it('should handle exact limit length', () => {
+ const text = 'Exactly 10';
+ const result = formatTextWithGivenLimit(text, 10);
+ expect(result).toBe('Exactly 10');
+ });
+});
+
+describe('DEEPLINK_FLOWS enum', () => {
+ it('should have QR_LOGIN defined', () => {
+ expect(DEEPLINK_FLOWS.QR_LOGIN).toBe('qrLoginFlow');
+ });
+
+ it('should have OVP defined', () => {
+ expect(DEEPLINK_FLOWS.OVP).toBe('ovpFlow');
+ });
+});
+
+describe('base64ToByteArray', () => {
+ it('should be defined', () => {
+ expect(base64ToByteArray).toBeDefined();
+ });
+
+ it('should convert base64 string to byte array', () => {
+ const base64 = 'SGVsbG8gV29ybGQ='; // "Hello World"
+ const result = base64ToByteArray(base64);
+ expect(result).toBeInstanceOf(Uint8Array);
+ expect(result.length).toBeGreaterThan(0);
+ });
+
+ it('should handle base64url encoding', () => {
+ const base64url = 'SGVsbG8gV29ybGQ'; // without padding
+ const result = base64ToByteArray(base64url);
+ expect(result).toBeInstanceOf(Uint8Array);
+ });
+
+ it('should throw error for invalid base64', () => {
+ expect(() => {
+ base64ToByteArray('!!!invalid base64!!!');
+ }).toThrow();
+ });
+
+ it('should handle strings with whitespace', () => {
+ const base64 = ' SGVsbG8gV29ybGQ= ';
+ const result = base64ToByteArray(base64);
+ expect(result).toBeInstanceOf(Uint8Array);
+ });
+
+ it('should handle URL-safe base64 characters', () => {
+ const base64 = 'SGVsbG8tV29ybGQ_'; // with - and _
+ const result = base64ToByteArray(base64);
+ expect(result).toBeInstanceOf(Uint8Array);
+ });
+
+ it('should handle empty string', () => {
+ const result = base64ToByteArray('');
+ expect(result).toBeInstanceOf(Uint8Array);
+ expect(result.length).toBe(0);
+ });
+});
+
+describe('createCacheObject', () => {
+ it('should create cache object with response and timestamp', () => {
+ const response = {data: 'test data'};
+ const result = createCacheObject(response);
+
+ expect(result).toHaveProperty('response');
+ expect(result).toHaveProperty('cachedTime');
+ expect(result.response).toBe(response);
+ expect(typeof result.cachedTime).toBe('number');
+ });
+
+ it('should use current timestamp', () => {
+ const before = Date.now();
+ const result = createCacheObject({});
+ const after = Date.now();
+
+ expect(result.cachedTime).toBeGreaterThanOrEqual(before);
+ expect(result.cachedTime).toBeLessThanOrEqual(after);
+ });
+
+ it('should handle null response', () => {
+ const result = createCacheObject(null);
+ expect(result.response).toBeNull();
+ expect(result.cachedTime).toBeDefined();
+ });
+
+ it('should handle complex objects', () => {
+ const complexResponse = {
+ data: [1, 2, 3],
+ metadata: {key: 'value'},
+ nested: {deep: {value: true}},
+ };
+ const result = createCacheObject(complexResponse);
+ expect(result.response).toBe(complexResponse);
+ });
+});
+
+describe('isCacheExpired', () => {
+ it('should return false for recent timestamp', () => {
+ const recentTimestamp = Date.now() - 1000; // 1 second ago
+ expect(isCacheExpired(recentTimestamp)).toBe(false);
+ });
+
+ it('should return true for old timestamp', () => {
+ const oldTimestamp = Date.now() - (60 * 60 * 1000 + 1000); // Over 1 hour ago
+ expect(isCacheExpired(oldTimestamp)).toBe(true);
+ });
+
+ it('should return false for current timestamp', () => {
+ const currentTimestamp = Date.now();
+ expect(isCacheExpired(currentTimestamp)).toBe(false);
+ });
+
+ it('should handle edge case at exact TTL boundary', () => {
+ const boundaryTimestamp = Date.now() - 60 * 60 * 1000; // Exactly 1 hour ago
+ const result = isCacheExpired(boundaryTimestamp);
+ expect(typeof result).toBe('boolean');
+ });
+});
+
+describe('getVerifierKey', () => {
+ it('should create verifier key with prefix', () => {
+ const verifier = 'example.com';
+ const result = getVerifierKey(verifier);
+ expect(result).toBe('trusted_verifier_example.com');
+ });
+
+ it('should handle empty string', () => {
+ const result = getVerifierKey('');
+ expect(result).toBe('trusted_verifier_');
+ });
+
+ it('should preserve verifier name exactly', () => {
+ const verifier = 'TestVerifier123';
+ const result = getVerifierKey(verifier);
+ expect(result).toBe('trusted_verifier_TestVerifier123');
+ });
+
+ it('should handle special characters', () => {
+ const verifier = 'verifier-with-dashes_and_underscores';
+ const result = getVerifierKey(verifier);
+ expect(result).toBe(
+ 'trusted_verifier_verifier-with-dashes_and_underscores',
+ );
+ });
+});
+
+describe('canonicalize', () => {
+ it('should be defined', async () => {
+ const {canonicalize} = await import('./Utils');
+ expect(canonicalize).toBeDefined();
+ });
+});
diff --git a/shared/VCFormat.test.ts b/shared/VCFormat.test.ts
new file mode 100644
index 00000000..416a321e
--- /dev/null
+++ b/shared/VCFormat.test.ts
@@ -0,0 +1,37 @@
+import {VCFormat} from './VCFormat';
+
+describe('VCFormat', () => {
+ it('should have ldp_vc format', () => {
+ expect(VCFormat.ldp_vc).toBe('ldp_vc');
+ });
+
+ it('should have mso_mdoc format', () => {
+ expect(VCFormat.mso_mdoc).toBe('mso_mdoc');
+ });
+
+ it('should have vc_sd_jwt format', () => {
+ expect(VCFormat.vc_sd_jwt).toBe('vc+sd-jwt');
+ });
+
+ it('should have dc_sd_jwt format', () => {
+ expect(VCFormat.dc_sd_jwt).toBe('dc+sd-jwt');
+ });
+
+ it('should have exactly 4 formats', () => {
+ const formatCount = Object.keys(VCFormat).length;
+ expect(formatCount).toBe(4);
+ });
+
+ it('should allow access via enum key', () => {
+ expect(VCFormat['ldp_vc']).toBe('ldp_vc');
+ expect(VCFormat['mso_mdoc']).toBe('mso_mdoc');
+ expect(VCFormat['vc_sd_jwt']).toBe('vc+sd-jwt');
+ expect(VCFormat['dc_sd_jwt']).toBe('dc+sd-jwt');
+ });
+
+ it('should have all unique values', () => {
+ const values = Object.values(VCFormat);
+ const uniqueValues = new Set(values);
+ expect(uniqueValues.size).toBe(values.length);
+ });
+});
diff --git a/shared/VCMetadata.test.ts b/shared/VCMetadata.test.ts
new file mode 100644
index 00000000..654270fd
--- /dev/null
+++ b/shared/VCMetadata.test.ts
@@ -0,0 +1,499 @@
+import {VCMetadata, parseMetadatas, getVCMetadata} from './VCMetadata';
+import {VCFormat} from './VCFormat';
+import {UUID} from './Utils';
+
+describe('VCMetadata', () => {
+ describe('constructor', () => {
+ it('should create instance with default values', () => {
+ const metadata = new VCMetadata();
+
+ expect(metadata.idType).toBe('');
+ expect(metadata.requestId).toBe('');
+ expect(metadata.isPinned).toBe(false);
+ expect(metadata.id).toBe('');
+ expect(metadata.issuer).toBe('');
+ expect(metadata.protocol).toBe('');
+ expect(metadata.timestamp).toBe('');
+ expect(metadata.isVerified).toBe(false);
+ expect(metadata.mosipIndividualId).toBe('');
+ expect(metadata.format).toBe('');
+ expect(metadata.isExpired).toBe(false);
+ });
+
+ it('should create instance with provided values', () => {
+ const metadata = new VCMetadata({
+ idType: 'UIN',
+ requestId: 'req123',
+ isPinned: true,
+ id: 'id123',
+ issuer: 'TestIssuer',
+ protocol: 'OpenId4VCI',
+ timestamp: '2024-01-01',
+ isVerified: true,
+ mosipIndividualId: 'mosip123',
+ format: 'ldp_vc',
+ downloadKeyType: 'ED25519',
+ isExpired: false,
+ credentialType: 'NationalID',
+ issuerHost: 'https://test.com',
+ });
+
+ expect(metadata.idType).toBe('UIN');
+ expect(metadata.requestId).toBe('req123');
+ expect(metadata.isPinned).toBe(true);
+ expect(metadata.id).toBe('id123');
+ expect(metadata.issuer).toBe('TestIssuer');
+ expect(metadata.protocol).toBe('OpenId4VCI');
+ expect(metadata.timestamp).toBe('2024-01-01');
+ expect(metadata.isVerified).toBe(true);
+ expect(metadata.mosipIndividualId).toBe('mosip123');
+ expect(metadata.format).toBe('ldp_vc');
+ expect(metadata.downloadKeyType).toBe('ED25519');
+ expect(metadata.isExpired).toBe(false);
+ expect(metadata.credentialType).toBe('NationalID');
+ expect(metadata.issuerHost).toBe('https://test.com');
+ });
+ });
+
+ describe('fromVC', () => {
+ it('should create VCMetadata from VC object', () => {
+ const vc = {
+ idType: 'VID',
+ requestId: 'req456',
+ id: 'vc123',
+ issuer: 'Issuer1',
+ format: VCFormat.ldp_vc,
+ };
+
+ const metadata = VCMetadata.fromVC(vc);
+
+ expect(metadata.idType).toBe('VID');
+ expect(metadata.requestId).toBe('req456');
+ expect(metadata.id).toBe('vc123');
+ expect(metadata.issuer).toBe('Issuer1');
+ expect(metadata.format).toBe(VCFormat.ldp_vc);
+ });
+
+ it('should use default format if not provided', () => {
+ const vc = {id: 'vc123'};
+ const metadata = VCMetadata.fromVC(vc);
+
+ expect(metadata.format).toBe(VCFormat.ldp_vc);
+ });
+
+ it('should handle isPinned default value', () => {
+ const vc = {id: 'vc123'};
+ const metadata = VCMetadata.fromVC(vc);
+
+ expect(metadata.isPinned).toBe(false);
+ });
+ });
+
+ describe('fromVcMetadataString', () => {
+ it('should parse JSON string to VCMetadata', () => {
+ const jsonStr = JSON.stringify({
+ id: 'vc123',
+ issuer: 'TestIssuer',
+ format: 'ldp_vc',
+ });
+
+ const metadata = VCMetadata.fromVcMetadataString(jsonStr);
+
+ expect(metadata.id).toBe('vc123');
+ expect(metadata.issuer).toBe('TestIssuer');
+ expect(metadata.format).toBe('ldp_vc');
+ });
+
+ it('should handle object input', () => {
+ const obj = {
+ id: 'vc456',
+ issuer: 'AnotherIssuer',
+ };
+
+ const metadata = VCMetadata.fromVcMetadataString(obj);
+
+ expect(metadata.id).toBe('vc456');
+ expect(metadata.issuer).toBe('AnotherIssuer');
+ });
+
+ it('should return empty VCMetadata on parse error', () => {
+ const invalidJson = '{invalid json}';
+ const metadata = VCMetadata.fromVcMetadataString(invalidJson);
+
+ expect(metadata).toBeInstanceOf(VCMetadata);
+ expect(metadata.id).toBe('');
+ });
+ });
+
+ describe('isVCKey', () => {
+ it('should return true for valid VC key', () => {
+ expect(VCMetadata.isVCKey('VC_1234567890_abc123')).toBe(true);
+ expect(VCMetadata.isVCKey('VC_timestamp_id')).toBe(true);
+ });
+
+ it('should return false for invalid VC key', () => {
+ expect(VCMetadata.isVCKey('INVALID_KEY')).toBe(false);
+ expect(VCMetadata.isVCKey('VC')).toBe(false);
+ expect(VCMetadata.isVCKey('')).toBe(false);
+ expect(VCMetadata.isVCKey('VC_')).toBe(false);
+ });
+
+ it('should handle keys with special characters properly', () => {
+ expect(VCMetadata.isVCKey('VC_123_abc-def')).toBe(true);
+ expect(VCMetadata.isVCKey('VC_123_abc_def')).toBe(true);
+ });
+ });
+
+ describe('isFromOpenId4VCI', () => {
+ it('should return true when protocol is OpenId4VCI', () => {
+ const metadata = new VCMetadata({protocol: 'OpenId4VCI'});
+ expect(metadata.isFromOpenId4VCI()).toBe(true);
+ });
+
+ it('should return false when protocol is not OpenId4VCI', () => {
+ const metadata = new VCMetadata({protocol: 'OtherProtocol'});
+ expect(metadata.isFromOpenId4VCI()).toBe(false);
+ });
+
+ it('should return false when protocol is empty', () => {
+ const metadata = new VCMetadata();
+ expect(metadata.isFromOpenId4VCI()).toBe(false);
+ });
+ });
+
+ describe('getVcKey', () => {
+ it('should generate VC key with timestamp', () => {
+ const metadata = new VCMetadata({
+ timestamp: '1234567890',
+ id: 'abc123',
+ });
+
+ expect(metadata.getVcKey()).toBe('VC_1234567890_abc123');
+ });
+
+ it('should generate VC key without timestamp', () => {
+ const metadata = new VCMetadata({
+ timestamp: '',
+ id: 'xyz789',
+ });
+
+ expect(metadata.getVcKey()).toBe('VC_xyz789');
+ });
+
+ it('should match the VC key regex pattern', () => {
+ const metadata = new VCMetadata({
+ timestamp: '1234567890',
+ id: 'test-id_123',
+ });
+
+ const key = metadata.getVcKey();
+ expect(VCMetadata.isVCKey(key)).toBe(true);
+ });
+ });
+
+ describe('equals', () => {
+ it('should return true for equal VCMetadata instances', () => {
+ const metadata1 = new VCMetadata({
+ timestamp: '1234567890',
+ id: 'abc123',
+ });
+ const metadata2 = new VCMetadata({
+ timestamp: '1234567890',
+ id: 'abc123',
+ });
+
+ expect(metadata1.equals(metadata2)).toBe(true);
+ });
+
+ it('should return false for different VCMetadata instances', () => {
+ const metadata1 = new VCMetadata({
+ timestamp: '1234567890',
+ id: 'abc123',
+ });
+ const metadata2 = new VCMetadata({
+ timestamp: '0987654321',
+ id: 'xyz789',
+ });
+
+ expect(metadata1.equals(metadata2)).toBe(false);
+ });
+
+ it('should return true when comparing instance with itself', () => {
+ const metadata = new VCMetadata({
+ timestamp: '1234567890',
+ id: 'abc123',
+ });
+
+ expect(metadata.equals(metadata)).toBe(true);
+ });
+ });
+
+ describe('vcKeyRegExp', () => {
+ it('should be defined as a RegExp', () => {
+ expect(VCMetadata.vcKeyRegExp).toBeInstanceOf(RegExp);
+ });
+ });
+});
+
+describe('parseMetadatas', () => {
+ it('should be defined', () => {
+ expect(parseMetadatas).toBeDefined();
+ });
+
+ it('should parse array of metadata objects', () => {
+ const metadataObjects = [
+ {id: 'vc1', issuer: 'Issuer1'},
+ {id: 'vc2', issuer: 'Issuer2'},
+ {id: 'vc3', issuer: 'Issuer3'},
+ ];
+
+ const result = parseMetadatas(metadataObjects);
+
+ expect(result).toHaveLength(3);
+ expect(result[0]).toBeInstanceOf(VCMetadata);
+ expect(result[0].id).toBe('vc1');
+ expect(result[1].id).toBe('vc2');
+ expect(result[2].id).toBe('vc3');
+ });
+
+ it('should handle empty array', () => {
+ const result = parseMetadatas([]);
+ expect(result).toEqual([]);
+ });
+
+ it('should create VCMetadata instances for each object', () => {
+ const metadataObjects = [
+ {id: 'test1', format: 'ldp_vc', isPinned: true},
+ {id: 'test2', format: 'mso_mdoc', isPinned: false},
+ ];
+
+ const result = parseMetadatas(metadataObjects);
+
+ expect(result[0].id).toBe('test1');
+ expect(result[0].format).toBe('ldp_vc');
+ expect(result[0].isPinned).toBe(true);
+
+ expect(result[1].id).toBe('test2');
+ expect(result[1].format).toBe('mso_mdoc');
+ expect(result[1].isPinned).toBe(false);
+ });
+});
+
+describe('getVCMetadata', () => {
+ beforeEach(() => {
+ jest.spyOn(UUID, 'generate').mockReturnValue('test-uuid-12345');
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it('should create VCMetadata with generated credential ID', () => {
+ const mockContext: any = {
+ selectedIssuer: {
+ credential_issuer_host: 'https://issuer.example.com',
+ issuer_id: 'TestIssuer',
+ protocol: 'OpenId4VCI',
+ },
+ timestamp: '1234567890',
+ vcMetadata: {
+ isVerified: false,
+ isExpired: false,
+ },
+ verifiableCredential: null,
+ credentialWrapper: {
+ format: VCFormat.ldp_vc,
+ },
+ selectedCredentialType: null,
+ };
+
+ const result = getVCMetadata(mockContext, 'ED25519');
+
+ expect(result.requestId).toContain('test-uuid-12345');
+ expect(result.requestId).toContain('issuer');
+ expect(result.issuer).toBe('TestIssuer');
+ expect(result.protocol).toBe('OpenId4VCI');
+ expect(result.timestamp).toBe('1234567890');
+ expect(result.downloadKeyType).toBe('ED25519');
+ expect(result.format).toBe(VCFormat.ldp_vc);
+ });
+
+ it('should handle credential_issuer when credential_issuer_host is not available', () => {
+ const mockContext: any = {
+ selectedIssuer: {
+ credential_issuer: 'https://backup.example.com',
+ issuer_id: 'BackupIssuer',
+ protocol: 'OpenId4VCI',
+ },
+ timestamp: '',
+ vcMetadata: {},
+ verifiableCredential: null,
+ credentialWrapper: {
+ format: VCFormat.mso_mdoc,
+ },
+ selectedCredentialType: null,
+ };
+
+ const result = getVCMetadata(mockContext, 'RSA');
+
+ expect(result.issuer).toBe('BackupIssuer');
+ expect(result.issuerHost).toBe('https://backup.example.com');
+ expect(result.format).toBe(VCFormat.mso_mdoc);
+ });
+
+ it('should use credential_issuer as fallback for issuer_id', () => {
+ const mockContext: any = {
+ selectedIssuer: {
+ credential_issuer_host: 'https://issuer.test.com',
+ credential_issuer: 'FallbackIssuer',
+ protocol: 'OIDC',
+ },
+ timestamp: '9876543210',
+ vcMetadata: {
+ isVerified: true,
+ isExpired: false,
+ },
+ verifiableCredential: null,
+ credentialWrapper: {
+ format: VCFormat.vc_sd_jwt,
+ },
+ selectedCredentialType: null,
+ };
+
+ const result = getVCMetadata(mockContext, 'ECDSA');
+
+ expect(result.issuer).toBe('FallbackIssuer');
+ expect(result.isVerified).toBe(true);
+ expect(result.downloadKeyType).toBe('ECDSA');
+ });
+
+ it('should extract issuer name from URL', () => {
+ const mockContext: any = {
+ selectedIssuer: {
+ credential_issuer_host: 'https://subdomain.example.org',
+ issuer_id: 'ExampleOrg',
+ protocol: 'OpenId4VCI',
+ },
+ timestamp: '',
+ vcMetadata: {},
+ verifiableCredential: null,
+ credentialWrapper: {format: VCFormat.ldp_vc},
+ selectedCredentialType: null,
+ };
+
+ const result = getVCMetadata(mockContext, 'ED25519');
+
+ expect(result.requestId).toContain('subdomain');
+ });
+
+ it('should handle invalid URL gracefully', () => {
+ const mockContext: any = {
+ selectedIssuer: {
+ credential_issuer_host: 'not-a-valid-url',
+ issuer_id: 'TestIssuer',
+ protocol: 'OpenId4VCI',
+ },
+ timestamp: '',
+ vcMetadata: {},
+ verifiableCredential: null,
+ credentialWrapper: {format: VCFormat.ldp_vc},
+ selectedCredentialType: null,
+ };
+
+ const result = getVCMetadata(mockContext, 'ED25519');
+
+ expect(result.requestId).toContain('not-a-valid-url');
+ expect(result.issuerHost).toBe('not-a-valid-url');
+ });
+
+ it('should handle Mosip VC with UIN', () => {
+ const mockContext: any = {
+ selectedIssuer: {
+ credential_issuer_host: 'https://mosip.example.com',
+ issuer_id: 'Mosip',
+ protocol: 'OpenId4VCI',
+ },
+ timestamp: '',
+ vcMetadata: {},
+ verifiableCredential: {
+ credential: {
+ credentialSubject: {
+ UIN: '1234567890',
+ },
+ },
+ },
+ credentialWrapper: {format: VCFormat.ldp_vc},
+ selectedCredentialType: null,
+ };
+
+ const result = getVCMetadata(mockContext, 'ED25519');
+
+ expect(result.mosipIndividualId).toBe('1234567890');
+ });
+
+ it('should handle Mosip VC with VID', () => {
+ const mockContext: any = {
+ selectedIssuer: {
+ credential_issuer_host: 'https://mosip.example.com',
+ issuer_id: 'Mosip',
+ protocol: 'OpenId4VCI',
+ },
+ timestamp: '',
+ vcMetadata: {},
+ verifiableCredential: {
+ credential: {
+ credentialSubject: {
+ VID: '9876543210',
+ },
+ },
+ },
+ credentialWrapper: {format: VCFormat.ldp_vc},
+ selectedCredentialType: null,
+ };
+
+ const result = getVCMetadata(mockContext, 'ED25519');
+
+ expect(result.mosipIndividualId).toBe('9876543210');
+ });
+
+ it('should set credential type when provided', () => {
+ const mockContext: any = {
+ selectedIssuer: {
+ credential_issuer_host: 'https://issuer.example.com',
+ issuer_id: 'TestIssuer',
+ protocol: 'OpenId4VCI',
+ },
+ timestamp: '1234567890',
+ vcMetadata: {},
+ verifiableCredential: null,
+ credentialWrapper: {format: VCFormat.mso_mdoc},
+ selectedCredentialType: 'org.iso.18013.5.1.mDL',
+ };
+
+ const result = getVCMetadata(mockContext, 'RSA');
+
+ expect(result.credentialType).toBeDefined();
+ expect(result.format).toBe(VCFormat.mso_mdoc);
+ });
+
+ it('should handle different key types', () => {
+ const mockContext: any = {
+ selectedIssuer: {
+ credential_issuer_host: 'https://issuer.example.com',
+ issuer_id: 'TestIssuer',
+ protocol: 'OpenId4VCI',
+ },
+ timestamp: '',
+ vcMetadata: {},
+ verifiableCredential: null,
+ credentialWrapper: {format: VCFormat.vc_sd_jwt},
+ selectedCredentialType: null,
+ };
+
+ const resultRSA = getVCMetadata(mockContext, 'RS256');
+ expect(resultRSA.downloadKeyType).toBe('RS256');
+
+ const resultEC = getVCMetadata(mockContext, 'ES256');
+ expect(resultEC.downloadKeyType).toBe('ES256');
+ });
+});
diff --git a/shared/api.test.ts b/shared/api.test.ts
new file mode 100644
index 00000000..f748daf2
--- /dev/null
+++ b/shared/api.test.ts
@@ -0,0 +1,216 @@
+import {API_URLS} from './api';
+
+describe('API_URLS configuration', () => {
+ describe('trustedVerifiersList', () => {
+ it('should have GET method', () => {
+ expect(API_URLS.trustedVerifiersList.method).toBe('GET');
+ });
+
+ it('should build correct URL', () => {
+ expect(API_URLS.trustedVerifiersList.buildURL()).toBe(
+ '/v1/mimoto/verifiers',
+ );
+ });
+ });
+
+ describe('issuersList', () => {
+ it('should have GET method', () => {
+ expect(API_URLS.issuersList.method).toBe('GET');
+ });
+
+ it('should build correct URL', () => {
+ expect(API_URLS.issuersList.buildURL()).toBe('/v1/mimoto/issuers');
+ });
+ });
+
+ describe('issuerConfig', () => {
+ it('should have GET method', () => {
+ expect(API_URLS.issuerConfig.method).toBe('GET');
+ });
+
+ it('should build correct URL with issuer id', () => {
+ expect(API_URLS.issuerConfig.buildURL('test-issuer')).toBe(
+ '/v1/mimoto/issuers/test-issuer',
+ );
+ });
+ });
+
+ describe('issuerWellknownConfig', () => {
+ it('should have GET method', () => {
+ expect(API_URLS.issuerWellknownConfig.method).toBe('GET');
+ });
+
+ it('should build correct URL with credential issuer', () => {
+ expect(
+ API_URLS.issuerWellknownConfig.buildURL('https://example.com'),
+ ).toBe('https://example.com/.well-known/openid-credential-issuer');
+ });
+ });
+
+ describe('authorizationServerMetadataConfig', () => {
+ it('should have GET method', () => {
+ expect(API_URLS.authorizationServerMetadataConfig.method).toBe('GET');
+ });
+
+ it('should build correct URL with authorization server URL', () => {
+ expect(
+ API_URLS.authorizationServerMetadataConfig.buildURL(
+ 'https://auth.example.com',
+ ),
+ ).toBe('https://auth.example.com/.well-known/oauth-authorization-server');
+ });
+ });
+
+ describe('allProperties', () => {
+ it('should have GET method', () => {
+ expect(API_URLS.allProperties.method).toBe('GET');
+ });
+
+ it('should build correct URL', () => {
+ expect(API_URLS.allProperties.buildURL()).toBe(
+ '/v1/mimoto/allProperties',
+ );
+ });
+ });
+
+ describe('getIndividualId', () => {
+ it('should have POST method', () => {
+ expect(API_URLS.getIndividualId.method).toBe('POST');
+ });
+
+ it('should build correct URL', () => {
+ expect(API_URLS.getIndividualId.buildURL()).toBe(
+ '/v1/mimoto/aid/get-individual-id',
+ );
+ });
+ });
+
+ describe('reqIndividualOTP', () => {
+ it('should have POST method', () => {
+ expect(API_URLS.reqIndividualOTP.method).toBe('POST');
+ });
+
+ it('should build correct URL', () => {
+ expect(API_URLS.reqIndividualOTP.buildURL()).toBe(
+ '/v1/mimoto/req/individualId/otp',
+ );
+ });
+ });
+
+ describe('walletBinding', () => {
+ it('should have POST method', () => {
+ expect(API_URLS.walletBinding.method).toBe('POST');
+ });
+
+ it('should build correct URL', () => {
+ expect(API_URLS.walletBinding.buildURL()).toBe(
+ '/v1/mimoto/wallet-binding',
+ );
+ });
+ });
+
+ describe('bindingOtp', () => {
+ it('should have POST method', () => {
+ expect(API_URLS.bindingOtp.method).toBe('POST');
+ });
+
+ it('should build correct URL', () => {
+ expect(API_URLS.bindingOtp.buildURL()).toBe('/v1/mimoto/binding-otp');
+ });
+ });
+
+ describe('requestOtp', () => {
+ it('should have POST method', () => {
+ expect(API_URLS.requestOtp.method).toBe('POST');
+ });
+
+ it('should build correct URL', () => {
+ expect(API_URLS.requestOtp.buildURL()).toBe('/v1/mimoto/req/otp');
+ });
+ });
+
+ describe('credentialRequest', () => {
+ it('should have POST method', () => {
+ expect(API_URLS.credentialRequest.method).toBe('POST');
+ });
+
+ it('should build correct URL', () => {
+ expect(API_URLS.credentialRequest.buildURL()).toBe(
+ '/v1/mimoto/credentialshare/request',
+ );
+ });
+ });
+
+ describe('credentialStatus', () => {
+ it('should have GET method', () => {
+ expect(API_URLS.credentialStatus.method).toBe('GET');
+ });
+
+ it('should build correct URL with id', () => {
+ expect(API_URLS.credentialStatus.buildURL('request-123')).toBe(
+ '/v1/mimoto/credentialshare/request/status/request-123',
+ );
+ });
+ });
+
+ describe('credentialDownload', () => {
+ it('should have POST method', () => {
+ expect(API_URLS.credentialDownload.method).toBe('POST');
+ });
+
+ it('should build correct URL', () => {
+ expect(API_URLS.credentialDownload.buildURL()).toBe(
+ '/v1/mimoto/credentialshare/download',
+ );
+ });
+ });
+
+ describe('linkTransaction', () => {
+ it('should have POST method', () => {
+ expect(API_URLS.linkTransaction.method).toBe('POST');
+ });
+
+ it('should build correct URL', () => {
+ expect(API_URLS.linkTransaction.buildURL()).toBe(
+ '/v1/esignet/linked-authorization/v2/link-transaction',
+ );
+ });
+ });
+
+ describe('authenticate', () => {
+ it('should have POST method', () => {
+ expect(API_URLS.authenticate.method).toBe('POST');
+ });
+
+ it('should build correct URL', () => {
+ expect(API_URLS.authenticate.buildURL()).toBe(
+ '/v1/esignet/linked-authorization/v2/authenticate',
+ );
+ });
+ });
+
+ describe('sendConsent', () => {
+ it('should have POST method', () => {
+ expect(API_URLS.sendConsent.method).toBe('POST');
+ });
+
+ it('should build correct URL', () => {
+ expect(API_URLS.sendConsent.buildURL()).toBe(
+ '/v1/esignet/linked-authorization/v2/consent',
+ );
+ });
+ });
+
+ describe('googleAccountProfileInfo', () => {
+ it('should have GET method', () => {
+ expect(API_URLS.googleAccountProfileInfo.method).toBe('GET');
+ });
+
+ it('should build correct URL with access token', () => {
+ const accessToken = 'test-token-123';
+ expect(API_URLS.googleAccountProfileInfo.buildURL(accessToken)).toBe(
+ `https://www.googleapis.com/oauth2/v1/userinfo?alt=json&access_token=${accessToken}`,
+ );
+ });
+ });
+});
diff --git a/shared/commonUtil.test.ts b/shared/commonUtil.test.ts
index 40b241a0..c6c80fac 100644
--- a/shared/commonUtil.test.ts
+++ b/shared/commonUtil.test.ts
@@ -1,4 +1,3 @@
-import {useState} from 'react';
import testIDProps, {
bytesToMB,
faceMatchConfig,
@@ -12,8 +11,20 @@ import testIDProps, {
logState,
removeWhiteSpace,
sleep,
+ getRandomInt,
+ getMosipIdentifier,
+ isTranslationKeyFound,
+ getAccountType,
+ BYTES_IN_MEGABYTE,
} from './commonUtil';
-import {argon2iConfig} from './constants';
+import {
+ argon2iConfig,
+ GOOGLE_DRIVE_NAME,
+ ICLOUD_DRIVE_NAME,
+ GMAIL,
+ APPLE,
+} from './constants';
+import {CredentialSubject} from '../machines/VerifiableCredential/VCMetaMachine/vc.d';
describe('hashData', () => {
it('should expose a function', () => {
@@ -74,6 +85,27 @@ describe('removeWhiteSpace', () => {
const response = removeWhiteSpace('React Native Unit Testing');
expect(response).toBe('ReactNativeUnitTesting');
});
+
+ it('should handle empty string', () => {
+ expect(removeWhiteSpace('')).toBe('');
+ });
+
+ it('should handle string with only spaces', () => {
+ expect(removeWhiteSpace(' ')).toBe('');
+ });
+
+ it('should handle string with tabs and newlines', () => {
+ expect(removeWhiteSpace('Hello\tWorld\n')).toBe('HelloWorld');
+ });
+
+ it('should handle string with multiple types of whitespace', () => {
+ expect(removeWhiteSpace(' Test \t String \n ')).toBe('TestString');
+ });
+
+ it('should remove all whitespace from string', () => {
+ const result = removeWhiteSpace('Hello World Test');
+ expect(result).toBe('HelloWorldTest');
+ });
});
describe('logState', () => {
@@ -97,17 +129,27 @@ describe('getMaskedText', () => {
const maskedTxt = getMaskedText(id);
expect(maskedTxt).toBe('******7890');
});
-});
-describe('faceMatchConfig', () => {
- it('should expose a function', () => {
- expect(faceMatchConfig).toBeDefined();
+ it('should handle exactly 4 characters', () => {
+ expect(getMaskedText('1234')).toBe('1234');
});
- // it('faceMatchConfig should return expected output', () => {
- // // const retValue = faceMatchConfig(resp);
- // expect(false).toBeTruthy();
- // });
+ it('should handle long strings', () => {
+ const longString = '12345678901234567890';
+ const masked = getMaskedText(longString);
+ expect(masked.endsWith('7890')).toBe(true);
+ expect(masked.length).toBe(longString.length);
+ });
+
+ it('should handle short strings', () => {
+ const result = getMaskedText('ABCDEF');
+ expect(result).toBe('**CDEF');
+ });
+
+ it('should handle exactly 4 characters (ABCD)', () => {
+ const result = getMaskedText('ABCD');
+ expect(result).toBe('ABCD');
+ });
});
describe('getBackupFileName', () => {
@@ -116,26 +158,19 @@ describe('getBackupFileName', () => {
});
});
-describe('bytesToMB', () => {
- it('bytesToMB returns a string', () => {
- expect(bytesToMB(0)).toBe('0');
- });
-
- it('10^6 bytes is 1MB', () => {
- expect(bytesToMB(1e6)).toBe('1.000');
- });
-});
-
describe('getDriveName', () => {
it('should expose a function', () => {
expect(getDriveName).toBeDefined();
});
- it('getDriveName should return Google Drive on Android', () => {
- expect(getDriveName()).toBe('Google Drive');
+ it('should return a string', () => {
+ const result = getDriveName();
+ expect(typeof result).toBe('string');
});
- it('getDriveName should return Google Drive on Android', () => {
- expect(getDriveName()).toBe('Google Drive');
+
+ it('should return Google Drive for Android or iCloud for iOS', () => {
+ const result = getDriveName();
+ expect([GOOGLE_DRIVE_NAME, ICLOUD_DRIVE_NAME]).toContain(result);
});
});
@@ -149,6 +184,19 @@ describe('sleep : The promise resolves after a certain time', () => {
const promise = sleep(time);
expect(promise).toBeInstanceOf(Promise);
});
+
+ it('should delay for specified milliseconds', async () => {
+ const start = Date.now();
+ await sleep(100);
+ const end = Date.now();
+ const elapsed = end - start;
+ expect(elapsed).toBeGreaterThanOrEqual(90); // Allow small margin
+ });
+
+ it('should resolve after timeout', async () => {
+ const promise = sleep(50);
+ await expect(promise).resolves.toBeUndefined();
+ });
});
describe('getScreenHeight', () => {
@@ -160,4 +208,240 @@ describe('getScreenHeight', () => {
const height = getScreenHeight();
expect(typeof height).toBe('object');
});
+
+ it('should return a value', () => {
+ const height = getScreenHeight();
+ expect(height).toBeDefined();
+ });
+});
+
+describe('getRandomInt', () => {
+ it('should expose a function', () => {
+ expect(getRandomInt).toBeDefined();
+ });
+
+ it('should return a number within the specified range', () => {
+ const min = 1;
+ const max = 10;
+ const result = getRandomInt(min, max);
+ expect(result).toBeGreaterThanOrEqual(min);
+ expect(result).toBeLessThanOrEqual(max);
+ expect(Number.isInteger(result)).toBe(true);
+ });
+
+ it('should return min when min and max are equal', () => {
+ const value = 5;
+ const result = getRandomInt(value, value);
+ expect(result).toBe(value);
+ });
+
+ it('should handle negative ranges', () => {
+ const result = getRandomInt(-10, -1);
+ expect(result).toBeGreaterThanOrEqual(-10);
+ expect(result).toBeLessThanOrEqual(-1);
+ });
+
+ it('should handle larger ranges', () => {
+ const result = getRandomInt(100, 200);
+ expect(result).toBeGreaterThanOrEqual(100);
+ expect(result).toBeLessThanOrEqual(200);
+ });
+});
+
+describe('getMosipIdentifier', () => {
+ it('should expose a function', () => {
+ expect(getMosipIdentifier).toBeDefined();
+ });
+
+ it('should return UIN when UIN is present', () => {
+ const credentialSubject = {
+ UIN: '123456789',
+ VID: '987654321',
+ } as Partial;
+ const result = getMosipIdentifier(credentialSubject as CredentialSubject);
+ expect(result).toBe('123456789');
+ });
+
+ it('should return VID when UIN is not present', () => {
+ const credentialSubject = {VID: '987654321'} as Partial;
+ const result = getMosipIdentifier(credentialSubject as CredentialSubject);
+ expect(result).toBe('987654321');
+ });
+
+ it('should return undefined when neither UIN nor VID is present', () => {
+ const credentialSubject = {} as Partial;
+ const result = getMosipIdentifier(credentialSubject as CredentialSubject);
+ expect(result).toBeUndefined();
+ });
+
+ it('should prioritize UIN over VID', () => {
+ const credSubject = {
+ UIN: '1111111111',
+ VID: '2222222222',
+ } as CredentialSubject;
+ expect(getMosipIdentifier(credSubject)).toBe('1111111111');
+ });
+});
+
+describe('isTranslationKeyFound', () => {
+ it('should expose a function', () => {
+ expect(isTranslationKeyFound).toBeDefined();
+ });
+
+ it('should return true when translation key is found', () => {
+ const mockT = jest.fn(() => 'Translated text');
+ const result = isTranslationKeyFound('someKey', mockT);
+ expect(result).toBe(true);
+ });
+
+ it('should return false when translation key is not found', () => {
+ const mockT = jest.fn((key: string) => key);
+ const result = isTranslationKeyFound('someKey', mockT);
+ expect(result).toBe(false);
+ });
+
+ it('should return true when key is translated', () => {
+ const mockT = () => 'Translated value';
+ expect(isTranslationKeyFound('any.key', mockT)).toBe(true);
+ });
+
+ it('should return false when translation key not found', () => {
+ const mockT = (key: string) => key; // returns same key
+ expect(isTranslationKeyFound('some.unknown.key', mockT)).toBe(false);
+ });
+
+ it('should return true for errors.notFound key when translation is found', () => {
+ const mockT = (key: string) => {
+ if (key === 'errors.notFound') return 'Error Not Found';
+ return key;
+ };
+ expect(isTranslationKeyFound('errors.notFound', mockT)).toBe(true);
+ });
+});
+
+describe('getAccountType', () => {
+ it('should expose a function', () => {
+ expect(getAccountType).toBeDefined();
+ });
+
+ it('should return a string', () => {
+ const result = getAccountType();
+ expect(typeof result).toBe('string');
+ });
+
+ it('should return Gmail for Android or Apple for iOS', () => {
+ const result = getAccountType();
+ expect([GMAIL, APPLE]).toContain(result);
+ });
+});
+
+describe('faceMatchConfig', () => {
+ it('should expose a function', () => {
+ expect(faceMatchConfig).toBeDefined();
+ });
+
+ it('should return a valid configuration object', () => {
+ const config = faceMatchConfig();
+ expect(config).toBeDefined();
+ expect(config.withFace).toBeDefined();
+ expect(config.withFace.encoder).toBeDefined();
+ expect(config.withFace.matcher).toBeDefined();
+ expect(config.withFace.encoder.tfModel).toBeDefined();
+ expect(config.withFace.matcher.threshold).toBe(1);
+ });
+
+ it('should return config with correct structure', () => {
+ const config = faceMatchConfig();
+ expect(config).toHaveProperty('withFace');
+ expect(config.withFace).toHaveProperty('encoder');
+ expect(config.withFace).toHaveProperty('matcher');
+ expect(config.withFace.encoder.tfModel).toHaveProperty('path');
+ expect(config.withFace.encoder.tfModel).toHaveProperty('modelChecksum');
+ expect(config.withFace.matcher.threshold).toBe(1);
+ });
+});
+
+describe('BYTES_IN_MEGABYTE', () => {
+ it('should be defined', () => {
+ expect(BYTES_IN_MEGABYTE).toBeDefined();
+ });
+
+ it('should equal 1,000,000', () => {
+ expect(BYTES_IN_MEGABYTE).toBe(1000000);
+ });
+
+ it('should be 1000 * 1000', () => {
+ expect(BYTES_IN_MEGABYTE).toBe(1000 * 1000);
+ });
+
+ it('should be a number', () => {
+ expect(typeof BYTES_IN_MEGABYTE).toBe('number');
+ });
+
+ it('should be positive', () => {
+ expect(BYTES_IN_MEGABYTE).toBeGreaterThan(0);
+ });
+});
+
+describe('bytesToMB', () => {
+ it('bytesToMB returns a string', () => {
+ expect(bytesToMB(0)).toBe('0');
+ });
+
+ it('10^6 bytes is 1MB', () => {
+ expect(bytesToMB(1e6)).toBe('1.000');
+ });
+
+ it('should return "0" for negative bytes', () => {
+ expect(bytesToMB(-100)).toBe('0');
+ });
+
+ it('should convert 1,000,000 bytes to "1.000" MB', () => {
+ expect(bytesToMB(1000000)).toBe('1.000');
+ });
+
+ it('should convert 2,500,000 bytes to "2.500" MB', () => {
+ expect(bytesToMB(2500000)).toBe('2.500');
+ });
+
+ it('should convert 500,000 bytes to "0.500" MB', () => {
+ expect(bytesToMB(500000)).toBe('0.500');
+ });
+
+ it('should handle large byte values', () => {
+ expect(bytesToMB(10000000)).toBe('10.000');
+ });
+
+ it('should handle small byte values', () => {
+ expect(bytesToMB(1000)).toBe('0.001');
+ });
+
+ it('should return three decimal places', () => {
+ const result = bytesToMB(1234567);
+ expect(result).toMatch(/^\d+\.\d{3}$/);
+ });
+
+ it('should handle fractional megabytes', () => {
+ expect(bytesToMB(1234567)).toBe('1.235');
+ });
+
+ it('should handle very small values', () => {
+ expect(bytesToMB(100)).toBe('0.000');
+ });
+
+ it('should handle exactly one byte', () => {
+ expect(bytesToMB(1)).toBe('0.000');
+ });
+
+ it('should convert bytes to megabytes', () => {
+ const bytes = BYTES_IN_MEGABYTE * 5; // 5 MB
+ const result = bytesToMB(bytes);
+ expect(result).toBe('5.000');
+ });
+
+ it('should handle fractional megabytes with BYTES_IN_MEGABYTE constant', () => {
+ const bytes = BYTES_IN_MEGABYTE * 2.5;
+ const result = bytesToMB(bytes);
+ expect(result).toBe('2.500');
+ });
});
diff --git a/shared/cryptoutil/KeyTypes.test.ts b/shared/cryptoutil/KeyTypes.test.ts
new file mode 100644
index 00000000..9deb65cc
--- /dev/null
+++ b/shared/cryptoutil/KeyTypes.test.ts
@@ -0,0 +1,37 @@
+import {KeyTypes} from './KeyTypes';
+
+describe('KeyTypes', () => {
+ it('should have RS256 key type', () => {
+ expect(KeyTypes.RS256).toBe('RS256');
+ });
+
+ it('should have ES256 key type', () => {
+ expect(KeyTypes.ES256).toBe('ES256');
+ });
+
+ it('should have ES256K key type', () => {
+ expect(KeyTypes.ES256K).toBe('ES256K');
+ });
+
+ it('should have ED25519 key type', () => {
+ expect(KeyTypes.ED25519).toBe('Ed25519');
+ });
+
+ it('should have exactly 4 key types', () => {
+ const keyTypeCount = Object.keys(KeyTypes).length;
+ expect(keyTypeCount).toBe(4);
+ });
+
+ it('should allow access via enum key', () => {
+ expect(KeyTypes['RS256']).toBe('RS256');
+ expect(KeyTypes['ES256']).toBe('ES256');
+ expect(KeyTypes['ES256K']).toBe('ES256K');
+ expect(KeyTypes['ED25519']).toBe('Ed25519');
+ });
+
+ it('should have all unique values', () => {
+ const values = Object.values(KeyTypes);
+ const uniqueValues = new Set(values);
+ expect(uniqueValues.size).toBe(values.length);
+ });
+});
diff --git a/shared/error/BiometricCancellationError.test.ts b/shared/error/BiometricCancellationError.test.ts
new file mode 100644
index 00000000..d5b2a1bf
--- /dev/null
+++ b/shared/error/BiometricCancellationError.test.ts
@@ -0,0 +1,31 @@
+import {BiometricCancellationError} from './BiometricCancellationError';
+
+describe('BiometricCancellationError', () => {
+ it('should create an error instance with the correct message', () => {
+ const errorMessage = 'User cancelled biometric authentication';
+ const error = new BiometricCancellationError(errorMessage);
+
+ expect(error).toBeInstanceOf(Error);
+ expect(error).toBeInstanceOf(BiometricCancellationError);
+ expect(error.message).toBe(errorMessage);
+ });
+
+ it('should have the correct error name', () => {
+ const error = new BiometricCancellationError('Test error');
+
+ expect(error.name).toBe('BiometricCancellationError');
+ });
+
+ it('should maintain the error stack trace', () => {
+ const error = new BiometricCancellationError('Stack trace test');
+
+ expect(error.stack).toBeDefined();
+ });
+
+ it('should handle empty message', () => {
+ const error = new BiometricCancellationError('');
+
+ expect(error.message).toBe('');
+ expect(error.name).toBe('BiometricCancellationError');
+ });
+});
diff --git a/shared/error/UnsupportedVCFormat.test.ts b/shared/error/UnsupportedVCFormat.test.ts
new file mode 100644
index 00000000..67c63d24
--- /dev/null
+++ b/shared/error/UnsupportedVCFormat.test.ts
@@ -0,0 +1,38 @@
+import {UnsupportedVcFormat} from './UnsupportedVCFormat';
+
+describe('UnsupportedVcFormat', () => {
+ it('should create an error instance with the correct format message', () => {
+ const format = 'jwt_vc_json';
+ const error = new UnsupportedVcFormat(format);
+
+ expect(error).toBeInstanceOf(Error);
+ expect(error).toBeInstanceOf(UnsupportedVcFormat);
+ expect(error.message).toBe(format);
+ });
+
+ it('should have the correct error name', () => {
+ const error = new UnsupportedVcFormat('ldp_vc');
+
+ expect(error.name).toBe('UnsupportedVcFormat');
+ });
+
+ it('should maintain the error stack trace', () => {
+ const error = new UnsupportedVcFormat('custom_format');
+
+ expect(error.stack).toBeDefined();
+ });
+
+ it('should handle empty format string', () => {
+ const error = new UnsupportedVcFormat('');
+
+ expect(error.message).toBe('');
+ expect(error.name).toBe('UnsupportedVcFormat');
+ });
+
+ it('should handle complex format strings', () => {
+ const format = 'application/vc+sd-jwt';
+ const error = new UnsupportedVcFormat(format);
+
+ expect(error.message).toBe(format);
+ });
+});
diff --git a/shared/javascript.test.ts b/shared/javascript.test.ts
new file mode 100644
index 00000000..4114b8ae
--- /dev/null
+++ b/shared/javascript.test.ts
@@ -0,0 +1,73 @@
+import {groupBy} from './javascript';
+
+describe('javascript utils', () => {
+ describe('groupBy', () => {
+ it('should group elements based on predicate', () => {
+ const array = [1, 2, 3, 4, 5, 6];
+ const predicate = (num: number) => num % 2 === 0;
+
+ const [trueElements, falseElements] = groupBy(array, predicate);
+
+ expect(trueElements).toEqual([2, 4, 6]);
+ expect(falseElements).toEqual([1, 3, 5]);
+ });
+
+ it('should return empty arrays for empty input', () => {
+ const array: number[] = [];
+ const predicate = (num: number) => num > 0;
+
+ const [trueElements, falseElements] = groupBy(array, predicate);
+
+ expect(trueElements).toEqual([]);
+ expect(falseElements).toEqual([]);
+ });
+
+ it('should put all elements in true group when predicate always returns true', () => {
+ const array = ['a', 'b', 'c'];
+ const predicate = () => true;
+
+ const [trueElements, falseElements] = groupBy(array, predicate);
+
+ expect(trueElements).toEqual(['a', 'b', 'c']);
+ expect(falseElements).toEqual([]);
+ });
+
+ it('should put all elements in false group when predicate always returns false', () => {
+ const array = ['a', 'b', 'c'];
+ const predicate = () => false;
+
+ const [trueElements, falseElements] = groupBy(array, predicate);
+
+ expect(trueElements).toEqual([]);
+ expect(falseElements).toEqual(['a', 'b', 'c']);
+ });
+
+ it('should handle complex objects', () => {
+ const array = [
+ {name: 'John', age: 30},
+ {name: 'Jane', age: 25},
+ {name: 'Bob', age: 35},
+ ];
+ const predicate = (person: {name: string; age: number}) =>
+ person.age >= 30;
+
+ const [trueElements, falseElements] = groupBy(array, predicate);
+
+ expect(trueElements).toEqual([
+ {name: 'John', age: 30},
+ {name: 'Bob', age: 35},
+ ]);
+ expect(falseElements).toEqual([{name: 'Jane', age: 25}]);
+ });
+
+ it('should handle undefined array', () => {
+ const array = undefined as any;
+ const predicate = (num: number) => num > 0;
+
+ const [trueElements, falseElements] = groupBy(array, predicate);
+
+ expect(trueElements).toEqual([]);
+ expect(falseElements).toEqual([]);
+ });
+ });
+});
diff --git a/shared/openId4VCI/Utils.test.ts b/shared/openId4VCI/Utils.test.ts
new file mode 100644
index 00000000..31c9e9ce
--- /dev/null
+++ b/shared/openId4VCI/Utils.test.ts
@@ -0,0 +1,407 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import {
+ Protocols,
+ Issuers,
+ isActivationNeeded,
+ ACTIVATION_NEEDED,
+ Issuers_Key_Ref,
+ getDisplayObjectForCurrentLanguage,
+ removeBottomSectionFields,
+ getMatchingCredentialIssuerMetadata,
+ selectCredentialRequestKey,
+ updateCredentialInformation,
+} from './Utils';
+import {VCFormat} from '../VCFormat';
+
+// Mock VCProcessor
+jest.mock('../../components/VC/common/VCProcessor', () => ({
+ VCProcessor: {
+ processForRendering: jest.fn().mockResolvedValue({
+ processedData: 'mocked-processed-credential',
+ }),
+ },
+}));
+
+describe('openId4VCI Utils', () => {
+ describe('Protocols', () => {
+ it('should have OpenId4VCI protocol defined', () => {
+ expect(Protocols.OpenId4VCI).toBe('OpenId4VCI');
+ });
+
+ it('should have OTP protocol defined', () => {
+ expect(Protocols.OTP).toBe('OTP');
+ });
+ });
+
+ describe('Issuers', () => {
+ it('should have MosipOtp issuer defined', () => {
+ expect(Issuers.MosipOtp).toBe('MosipOtp');
+ });
+
+ it('should have Mosip issuer defined', () => {
+ expect(Issuers.Mosip).toBe('Mosip');
+ });
+ });
+
+ describe('ACTIVATION_NEEDED', () => {
+ it('should contain Mosip', () => {
+ expect(ACTIVATION_NEEDED).toContain(Issuers.Mosip);
+ });
+
+ it('should contain MosipOtp', () => {
+ expect(ACTIVATION_NEEDED).toContain(Issuers.MosipOtp);
+ });
+
+ it('should have exactly 2 issuers', () => {
+ expect(ACTIVATION_NEEDED).toHaveLength(2);
+ });
+ });
+
+ describe('isActivationNeeded', () => {
+ it('should return true for Mosip issuer', () => {
+ expect(isActivationNeeded('Mosip')).toBe(true);
+ });
+
+ it('should return true for MosipOtp issuer', () => {
+ expect(isActivationNeeded('MosipOtp')).toBe(true);
+ });
+
+ it('should return false for other issuers', () => {
+ expect(isActivationNeeded('SomeOtherIssuer')).toBe(false);
+ });
+
+ it('should return false for empty string', () => {
+ expect(isActivationNeeded('')).toBe(false);
+ });
+
+ it('should return false for undefined', () => {
+ expect(isActivationNeeded(undefined as any)).toBe(false);
+ });
+
+ it('should be case sensitive', () => {
+ expect(isActivationNeeded('mosip')).toBe(false);
+ expect(isActivationNeeded('MOSIP')).toBe(false);
+ });
+ });
+
+ describe('Issuers_Key_Ref', () => {
+ it('should have correct key reference', () => {
+ expect(Issuers_Key_Ref).toBe('OpenId4VCI_KeyPair');
+ });
+ });
+
+ describe('getDisplayObjectForCurrentLanguage', () => {
+ it('should return display object for current language', () => {
+ const display = [
+ {language: 'en', name: 'English Name', logo: 'en-logo.png'},
+ {language: 'hi', name: 'Hindi Name', logo: 'hi-logo.png'},
+ ] as any;
+
+ const result = getDisplayObjectForCurrentLanguage(display);
+ expect(result).toBeDefined();
+ expect(result.name).toBeDefined();
+ });
+
+ it('should return first display object when language not found', () => {
+ const display = [
+ {language: 'fr', name: 'French Name', logo: 'fr-logo.png'},
+ {language: 'de', name: 'German Name', logo: 'de-logo.png'},
+ ] as any;
+
+ const result = getDisplayObjectForCurrentLanguage(display);
+ expect(result).toBeDefined();
+ expect(result.name).toBe('French Name');
+ });
+
+ it('should return empty object when display array is empty', () => {
+ const result = getDisplayObjectForCurrentLanguage([]);
+ expect(result).toEqual({});
+ });
+
+ it('should return empty object when display is null', () => {
+ const result = getDisplayObjectForCurrentLanguage(null as any);
+ expect(result).toEqual({});
+ });
+
+ it('should handle locale key instead of language key', () => {
+ const display = [
+ {locale: 'en-US', name: 'English Name', logo: 'en-logo.png'},
+ {locale: 'hi-IN', name: 'Hindi Name', logo: 'hi-logo.png'},
+ ] as any;
+
+ const result = getDisplayObjectForCurrentLanguage(display);
+ expect(result).toBeDefined();
+ });
+
+ it('should fallback to en-US when current language not found', () => {
+ const display = [
+ {language: 'fr', name: 'French Name'},
+ {language: 'en-US', name: 'English US Name'},
+ ] as any;
+
+ const result = getDisplayObjectForCurrentLanguage(display);
+ expect(result.name).toBe('English US Name');
+ });
+ });
+
+ describe('removeBottomSectionFields', () => {
+ it('should remove bottom section fields for SD-JWT format', () => {
+ const fields = ['name', 'age', 'photo', 'signature', 'address'];
+ const result = removeBottomSectionFields(fields, VCFormat.vc_sd_jwt);
+
+ expect(result).toBeDefined();
+ expect(Array.isArray(result)).toBe(true);
+ });
+
+ it('should remove bottom section fields for DC-SD-JWT format', () => {
+ const fields = ['name', 'age', 'photo', 'signature'];
+ const result = removeBottomSectionFields(fields, VCFormat.dc_sd_jwt);
+
+ expect(result).toBeDefined();
+ expect(Array.isArray(result)).toBe(true);
+ });
+
+ it('should remove address field for LDP format', () => {
+ const fields = ['name', 'age', 'address', 'photo'];
+ const result = removeBottomSectionFields(fields, VCFormat.ldp_vc);
+
+ expect(result).toBeDefined();
+ expect(result).not.toContain('address');
+ });
+
+ it('should handle empty fields array', () => {
+ const result = removeBottomSectionFields([], VCFormat.ldp_vc);
+ expect(result).toEqual([]);
+ });
+ });
+
+ describe('getMatchingCredentialIssuerMetadata', () => {
+ it('should return matching credential configuration', () => {
+ const wellknown = {
+ credential_configurations_supported: {
+ MOSIPVerifiableCredential: {
+ format: 'ldp_vc',
+ order: ['name', 'age'],
+ },
+ AnotherCredential: {
+ format: 'jwt_vc',
+ },
+ },
+ };
+
+ const result = getMatchingCredentialIssuerMetadata(
+ wellknown,
+ 'MOSIPVerifiableCredential',
+ );
+
+ expect(result).toBeDefined();
+ expect(result.format).toBe('ldp_vc');
+ expect(result.order).toEqual(['name', 'age']);
+ });
+
+ it('should throw error when credential type not found', () => {
+ const wellknown = {
+ credential_configurations_supported: {
+ SomeCredential: {format: 'ldp_vc'},
+ },
+ };
+
+ expect(() => {
+ getMatchingCredentialIssuerMetadata(wellknown, 'NonExistentCredential');
+ }).toThrow();
+ });
+
+ it('should handle multiple credential configurations', () => {
+ const wellknown = {
+ credential_configurations_supported: {
+ Credential1: {format: 'ldp_vc'},
+ Credential2: {format: 'jwt_vc'},
+ Credential3: {format: 'mso_mdoc'},
+ },
+ };
+
+ const result = getMatchingCredentialIssuerMetadata(
+ wellknown,
+ 'Credential2',
+ );
+
+ expect(result).toBeDefined();
+ expect(result.format).toBe('jwt_vc');
+ });
+ });
+
+ describe('selectCredentialRequestKey', () => {
+ it('should select first supported key type', () => {
+ const proofSigningAlgos = ['RS256', 'ES256'];
+ const keyOrder = {'0': 'RS256', '1': 'ES256', '2': 'Ed25519'};
+
+ const result = selectCredentialRequestKey(proofSigningAlgos, keyOrder);
+ expect(result).toBe('RS256');
+ });
+
+ it('should return first key when no match found', () => {
+ const proofSigningAlgos = ['UNKNOWN_ALGO'];
+ const keyOrder = {'0': 'RS256', '1': 'ES256'};
+
+ const result = selectCredentialRequestKey(proofSigningAlgos, keyOrder);
+ expect(result).toBe('RS256');
+ });
+
+ it('should handle empty proofSigningAlgos', () => {
+ const keyOrder = {'0': 'RS256', '1': 'ES256'};
+
+ const result = selectCredentialRequestKey([], keyOrder);
+ expect(result).toBe('RS256');
+ });
+
+ it('should select matching key from middle of order', () => {
+ const proofSigningAlgos = ['ES256'];
+ const keyOrder = {'0': 'RS256', '1': 'ES256', '2': 'Ed25519'};
+
+ const result = selectCredentialRequestKey(proofSigningAlgos, keyOrder);
+ expect(result).toBe('ES256');
+ });
+ });
+
+ describe('updateCredentialInformation', () => {
+ it('should update credential information for MSO_MDOC format', async () => {
+ const mockContext = {
+ selectedCredentialType: {
+ id: 'TestCredential',
+ format: VCFormat.mso_mdoc,
+ },
+ selectedIssuer: {
+ display: [{language: 'en', logo: 'test-logo.png'}],
+ },
+ vcMetadata: {
+ id: 'test-id',
+ },
+ };
+
+ const mockCredential = {
+ credential: 'test-credential-data',
+ } as any;
+
+ const result = await updateCredentialInformation(
+ mockContext,
+ mockCredential,
+ );
+
+ expect(result).toBeDefined();
+ expect(result.format).toBe(VCFormat.mso_mdoc);
+ expect(result.verifiableCredential).toBeDefined();
+ expect(result.verifiableCredential.credentialConfigurationId).toBe(
+ 'TestCredential',
+ );
+ expect(result.generatedOn).toBeInstanceOf(Date);
+ });
+
+ it('should update credential information for SD-JWT format', async () => {
+ const mockContext = {
+ selectedCredentialType: {
+ id: 'SDJWTCredential',
+ format: VCFormat.vc_sd_jwt,
+ },
+ selectedIssuer: {
+ display: [{language: 'en', logo: 'sd-jwt-logo.png'}],
+ },
+ vcMetadata: {
+ id: 'sd-jwt-id',
+ },
+ };
+
+ const mockCredential = {
+ credential: 'sd-jwt-credential-data',
+ } as any;
+
+ const result = await updateCredentialInformation(
+ mockContext,
+ mockCredential,
+ );
+
+ expect(result).toBeDefined();
+ expect(result.format).toBe(VCFormat.vc_sd_jwt);
+ expect(result.vcMetadata.format).toBe(VCFormat.vc_sd_jwt);
+ });
+
+ it('should update credential information for DC-SD-JWT format', async () => {
+ const mockContext = {
+ selectedCredentialType: {
+ id: 'DCSDJWTCredential',
+ format: VCFormat.dc_sd_jwt,
+ },
+ selectedIssuer: {
+ display: [{language: 'en', logo: 'dc-logo.png'}],
+ },
+ vcMetadata: {
+ id: 'dc-jwt-id',
+ },
+ };
+
+ const mockCredential = {
+ credential: 'dc-sd-jwt-credential-data',
+ } as any;
+
+ const result = await updateCredentialInformation(
+ mockContext,
+ mockCredential,
+ );
+
+ expect(result).toBeDefined();
+ expect(result.format).toBe(VCFormat.dc_sd_jwt);
+ });
+
+ it('should handle credential without logo in display', async () => {
+ const mockContext = {
+ selectedCredentialType: {
+ id: 'NoLogoCredential',
+ format: VCFormat.ldp_vc,
+ },
+ selectedIssuer: {
+ display: [{language: 'en'}],
+ },
+ vcMetadata: {},
+ };
+
+ const mockCredential = {
+ credential: 'no-logo-credential',
+ } as any;
+
+ const result = await updateCredentialInformation(
+ mockContext,
+ mockCredential,
+ );
+
+ expect(result).toBeDefined();
+ expect(result.verifiableCredential.issuerLogo).toBe('');
+ });
+
+ it('should include vcMetadata with format', async () => {
+ const mockContext = {
+ selectedCredentialType: {
+ id: 'MetadataTest',
+ format: VCFormat.ldp_vc,
+ },
+ selectedIssuer: {
+ display: [],
+ },
+ vcMetadata: {
+ id: 'metadata-id',
+ },
+ };
+
+ const mockCredential = {
+ credential: 'metadata-test',
+ } as any;
+
+ const result = await updateCredentialInformation(
+ mockContext,
+ mockCredential,
+ );
+
+ expect(result.vcMetadata).toBeDefined();
+ expect(result.vcMetadata.format).toBe(VCFormat.ldp_vc);
+ expect(result.vcMetadata.id).toBe('metadata-id');
+ });
+ });
+});
diff --git a/shared/sharing/imageUtils.test.ts b/shared/sharing/imageUtils.test.ts
new file mode 100644
index 00000000..fcc5a816
--- /dev/null
+++ b/shared/sharing/imageUtils.test.ts
@@ -0,0 +1,97 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import {shareImageToAllSupportedApps} from './imageUtils';
+import RNShare from 'react-native-share';
+
+jest.mock('react-native-share', () => ({
+ open: jest.fn(),
+}));
+
+describe('imageUtils', () => {
+ describe('shareImageToAllSupportedApps', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should return true when sharing is successful', async () => {
+ const mockShareOptions = {
+ url: 'file://path/to/image.jpg',
+ type: 'image/jpeg',
+ };
+
+ (RNShare.open as jest.Mock).mockResolvedValue({success: true});
+
+ const result = await shareImageToAllSupportedApps(mockShareOptions);
+
+ expect(result).toBe(true);
+ expect(RNShare.open).toHaveBeenCalledWith(mockShareOptions);
+ });
+
+ it('should return false when sharing fails', async () => {
+ const mockShareOptions = {
+ url: 'file://path/to/image.jpg',
+ type: 'image/jpeg',
+ };
+
+ (RNShare.open as jest.Mock).mockResolvedValue({success: false});
+
+ const result = await shareImageToAllSupportedApps(mockShareOptions);
+
+ expect(result).toBe(false);
+ });
+
+ it('should return false when an exception occurs', async () => {
+ const mockShareOptions = {
+ url: 'file://path/to/image.jpg',
+ type: 'image/jpeg',
+ };
+
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
+ (RNShare.open as jest.Mock).mockRejectedValue(new Error('Share failed'));
+
+ const result = await shareImageToAllSupportedApps(mockShareOptions);
+
+ expect(result).toBe(false);
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
+ 'Exception while sharing image::',
+ expect.any(Error),
+ );
+
+ consoleErrorSpy.mockRestore();
+ });
+
+ it('should handle different share options', async () => {
+ const mockShareOptions = {
+ url: 'file://path/to/qr-code.png',
+ type: 'image/png',
+ title: 'Share QR Code',
+ };
+
+ (RNShare.open as jest.Mock).mockResolvedValue({success: true});
+
+ const result = await shareImageToAllSupportedApps(mockShareOptions);
+
+ expect(result).toBe(true);
+ expect(RNShare.open).toHaveBeenCalledWith(mockShareOptions);
+ });
+
+ it('should handle user dismissing share dialog', async () => {
+ const mockShareOptions = {
+ url: 'file://path/to/image.jpg',
+ };
+
+ (RNShare.open as jest.Mock).mockRejectedValue({
+ message: 'User did not share',
+ });
+
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
+ const result = await shareImageToAllSupportedApps(mockShareOptions);
+
+ expect(result).toBe(false);
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
+ 'Exception while sharing image::',
+ expect.objectContaining({message: 'User did not share'}),
+ );
+ consoleErrorSpy.mockRestore();
+ });
+ });
+});