SELF-1192: fix oom tests (#1429)

* fix oom tests?

* update tests

* try fixing tests again

* fix: unblock mobile app jest runner

* fix corrupt yarn lock

* Reduce heavy React Native usage in tests (#1436)

* Reduce heavy React Native usage in tests

* Stabilize mobile tests

* prettier

* ignore podfile.lock

* fix test and gitleaks

* fix path

* update

* fix tests

* address tamagui concern
This commit is contained in:
Justin Hernandez
2025-11-20 11:59:00 -03:00
committed by GitHub
parent 6cbbacdf84
commit cadd7ae5b7
23 changed files with 2661 additions and 1661 deletions

View File

@@ -13,7 +13,7 @@ jobs:
- name: Install gitleaks
uses: gitleaks/gitleaks-action@v2.3.9
with:
config-path: .gitleaks.toml
config-path: gitleaks-override.toml
fail: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -6,7 +6,6 @@
f506113a22e5b147132834e4659f5af308448389:app/tests/utils/deeplinks.test.ts:generic-api-key:183
5a67b5cc50f291401d1da4e51706d0cfcf1c2316:app/tests/utils/deeplinks.test.ts:generic-api-key:182
0e4555eee6589aa9cca68f451227b149277d8c90:app/tests/src/utils/points/api.test.ts:generic-api-key:34
feb433e3553f8a7fa6c724b2de5a3e32ef079880:app/ios/Podfile.lock:generic-api-key:2594
3d0e1b4589680df2451031913d067b1b91dafa60:app/ios/Podfile.lock:generic-api-key:2594
3d0e1b4589680df2451031913d067b1b91dafa60:app/tests/utils/deeplinks.test.ts:generic-api-key:208
circuits/circuits/gcp_jwt_verifier/example_jwt.txt:jwt:1
3a3392417065169c48329fd2463b83e5a43b10db:app/ios/Podfile.lock:generic-api-key:2586

71
app/babel.config.test.cjs Normal file
View File

@@ -0,0 +1,71 @@
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
// Babel config for Jest tests that excludes hermes-parser to avoid WebAssembly issues
// Based on React Native babel preset but with hermes parser plugin removed
module.exports = {
presets: [
[
'@babel/preset-env',
{
targets: {
node: 'current',
},
},
],
'@babel/preset-typescript',
[
'@babel/preset-react',
{
runtime: 'automatic',
},
],
],
plugins: [
// Module resolver for @ alias
[
'module-resolver',
{
root: ['./src'],
alias: { '@': './src' },
},
],
// Core React Native transforms (minimal set needed for tests)
['@babel/plugin-transform-class-properties', { loose: true }],
['@babel/plugin-transform-classes', { loose: true }],
['@babel/plugin-transform-private-methods', { loose: true }],
['@babel/plugin-transform-private-property-in-object', { loose: true }],
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-syntax-export-default-from',
'@babel/plugin-transform-export-namespace-from',
'@babel/plugin-transform-unicode-regex',
['@babel/plugin-transform-destructuring', { useBuiltIns: true }],
'@babel/plugin-transform-spread',
[
'@babel/plugin-transform-object-rest-spread',
{ loose: true, useBuiltIns: true },
],
['@babel/plugin-transform-optional-chaining', { loose: true }],
['@babel/plugin-transform-nullish-coalescing-operator', { loose: true }],
['@babel/plugin-transform-logical-assignment-operators', { loose: true }],
// Flow type stripping to support React Native's Flow-based sources
['@babel/plugin-syntax-flow'],
['@babel/plugin-transform-flow-strip-types', { allowDeclareFields: true }],
// Environment variable support
[
'module:react-native-dotenv',
{
moduleName: '@env',
path: '.env',
blacklist: null,
whitelist: null,
safe: false,
allowUndefined: true,
},
],
],
};

View File

@@ -1465,7 +1465,7 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- react-native-app-auth (8.0.3):
- react-native-app-auth (8.1.0):
- AppAuth (>= 1.7.6)
- React-Core
- react-native-biometrics (3.0.1):
@@ -1560,7 +1560,7 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- react-native-safe-area-context (5.6.1):
- react-native-safe-area-context (5.6.2):
- DoubleConversion
- glog
- hermes-engine
@@ -1573,8 +1573,8 @@ PODS:
- React-featureflags
- React-graphics
- React-ImageManager
- react-native-safe-area-context/common (= 5.6.1)
- react-native-safe-area-context/fabric (= 5.6.1)
- react-native-safe-area-context/common (= 5.6.2)
- react-native-safe-area-context/fabric (= 5.6.2)
- React-NativeModulesApple
- React-RCTFabric
- React-rendererdebug
@@ -1583,7 +1583,7 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- react-native-safe-area-context/common (5.6.1):
- react-native-safe-area-context/common (5.6.2):
- DoubleConversion
- glog
- hermes-engine
@@ -1604,7 +1604,7 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- react-native-safe-area-context/fabric (5.6.1):
- react-native-safe-area-context/fabric (5.6.2):
- DoubleConversion
- glog
- hermes-engine
@@ -2021,7 +2021,7 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- RNLocalize (3.5.3):
- RNLocalize (3.6.0):
- DoubleConversion
- glog
- hermes-engine
@@ -2131,7 +2131,7 @@ PODS:
- ReactCommon/turbomodule/core
- Sentry/HybridSDK (= 8.53.2)
- Yoga
- RNSVG (15.14.0):
- RNSVG (15.15.0):
- DoubleConversion
- glog
- hermes-engine
@@ -2151,9 +2151,9 @@ PODS:
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- RNSVG/common (= 15.14.0)
- RNSVG/common (= 15.15.0)
- Yoga
- RNSVG/common (15.14.0):
- RNSVG/common (15.15.0):
- DoubleConversion
- glog
- hermes-engine
@@ -2583,7 +2583,7 @@ SPEC CHECKSUMS:
React-logger: c4052eb941cca9a097ef01b59543a656dc088559
React-Mapbuffer: 9343a5c14536d4463c80f09a960653d754daae21
React-microtasksnativemodule: c7cafd8f4470cf8a4578ee605daa4c74d3278bf8
react-native-app-auth: eb42594042a25455119a8c57194b4fd25b9352f4
react-native-app-auth: e21c8ee920876b960e38c9381971bd189ebea06b
react-native-biometrics: 43ed5b828646a7862dbc7945556446be00798e7d
react-native-blur: 6334d934a9b5e67718b8f5725c44cc0a12946009
react-native-cloud-storage: 8d89f2bc574cf11068dfd90933905974087fb9e9
@@ -2592,7 +2592,7 @@ SPEC CHECKSUMS:
react-native-netinfo: cec9c4e86083cb5b6aba0e0711f563e2fbbff187
react-native-nfc-manager: 66a00e5ddab9704efebe19d605b1b8afb0bb1bd7
react-native-passkey: 8853c3c635164864da68a6dbbcec7148506c3bcf
react-native-safe-area-context: 90a89cb349c7f8168a707e6452288c2f665b9fd1
react-native-safe-area-context: a7aad44fe544b55e2369a3086e16a01be60ce398
react-native-sqlite-storage: 0c84826214baaa498796c7e46a5ccc9a82e114ed
react-native-webview: 3f45e19f0ffc3701168768a6c37695e0f252410e
React-nativeconfig: 415626a63057638759bcc75e0a96e2e07771a479
@@ -2631,11 +2631,11 @@ SPEC CHECKSUMS:
RNGestureHandler: a63b531307e5b2e6ea21d053a1a7ad4cf9695c57
RNInAppBrowser: 6d3eb68d471b9834335c664704719b8be1bfdb20
RNKeychain: 471ceef8c13f15a5534c3cd2674dbbd9d0680e52
RNLocalize: 7683e450496a5aea9a2dab3745bfefa7341d3f5e
RNLocalize: 4f5e4a46d2bccd04ccb96721e438dcb9de17c2e0
RNReactNativeHapticFeedback: e526ac4a7ca9fb23c7843ea4fd7d823166054c73
RNScreens: 806e1449a8ec63c2a4e4cf8a63cc80203ccda9b8
RNSentry: 6ad982be2c8e32dab912afb4132b6a0d88484ea0
RNSVG: e1cf5a9a5aa12c69f2ec47031defbd87ae7fb697
RNSVG: 39476f26bbbe72ffe6194c6fc8f6acd588087957
segment-analytics-react-native: a0c29c75ede1989118b50cac96b9495ea5c91a1d
Sentry: 59993bffde4a1ac297ba6d268dc4bbce068d7c1b
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748

View File

@@ -3,8 +3,18 @@
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
module.exports = {
preset: 'react-native',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'cjs', 'json', 'node'],
moduleFileExtensions: [
'ios.js',
'android.js',
'native.js',
'ts',
'tsx',
'js',
'jsx',
'cjs',
'json',
'node',
],
transformIgnorePatterns: [
'node_modules/(?!(react-native|@react-native|@react-navigation|@react-native-community|@segment/analytics-react-native|@openpassport|react-native-keychain|react-native-check-version|react-native-nfc-manager|react-native-passport-reader|react-native-gesture-handler|uuid|@stablelib|@react-native-google-signin|react-native-cloud-storage|@react-native-clipboard|@react-native-firebase|@selfxyz|@sentry|@anon-aadhaar|react-native-svg|react-native-svg-circle-country-flags)/)',
],
@@ -16,6 +26,7 @@ module.exports = {
testPathIgnorePatterns: [
'/node_modules/',
'/scripts/tests/', // Node.js native test runner tests
'/babel\\.config\\.test\\.cjs',
],
moduleNameMapper: {
'^@env$': '<rootDir>/tests/__setup__/@env.js',
@@ -50,7 +61,7 @@ module.exports = {
'<rootDir>/../common/node_modules/@anon-aadhaar/core/dist/index.js',
},
transform: {
'\\.[jt]sx?$': 'babel-jest',
'\\.[jt]sx?$': ['babel-jest', { configFile: './babel.config.test.cjs' }],
},
globals: {
'ts-jest': {

View File

@@ -282,17 +282,13 @@ jest.mock(
// Mock react-native-gesture-handler to prevent getConstants errors
jest.mock('react-native-gesture-handler', () => {
const React = require('react');
// Avoid requiring React to prevent nested require memory issues
// Mock the components directly without requiring react-native
// to avoid triggering hermes-parser WASM errors
const MockScrollView = props =>
React.createElement('ScrollView', props, props.children);
const MockTouchableOpacity = props =>
React.createElement('TouchableOpacity', props, props.children);
const MockTouchableHighlight = props =>
React.createElement('TouchableHighlight', props, props.children);
const MockFlatList = props => React.createElement('FlatList', props);
// Mock the components as simple pass-through functions
const MockScrollView = jest.fn(props => props.children || null);
const MockTouchableOpacity = jest.fn(props => props.children || null);
const MockTouchableHighlight = jest.fn(props => props.children || null);
const MockFlatList = jest.fn(props => null);
return {
...jest.requireActual('react-native-gesture-handler/jestSetup'),
@@ -306,13 +302,11 @@ jest.mock('react-native-gesture-handler', () => {
// Mock react-native-safe-area-context
jest.mock('react-native-safe-area-context', () => {
const React = require('react');
// Use React.createElement directly instead of requiring react-native to avoid memory issues
// Avoid requiring React to prevent nested require memory issues
return {
__esModule: true,
SafeAreaProvider: ({ children }) =>
React.createElement('View', null, children),
SafeAreaView: ({ children }) => React.createElement('View', null, children),
SafeAreaProvider: jest.fn(({ children }) => children || null),
SafeAreaView: jest.fn(({ children }) => children || null),
useSafeAreaInsets: () => ({ top: 0, bottom: 0, left: 0, right: 0 }),
};
});
@@ -903,53 +897,106 @@ jest.mock('react-native-localize', () => ({
languageTag: 'en-US',
isRTL: false,
}),
default: {
getLocales: jest.fn().mockReturnValue([
{
countryCode: 'US',
languageTag: 'en-US',
languageCode: 'en',
isRTL: false,
},
]),
getCountry: jest.fn().mockReturnValue('US'),
getTimeZone: jest.fn().mockReturnValue('America/New_York'),
getCurrencies: jest.fn().mockReturnValue(['USD']),
getTemperatureUnit: jest.fn().mockReturnValue('celsius'),
getFirstWeekDay: jest.fn().mockReturnValue(0),
uses24HourClock: jest.fn().mockReturnValue(false),
usesMetricSystem: jest.fn().mockReturnValue(false),
findBestAvailableLanguage: jest.fn().mockReturnValue({
languageTag: 'en-US',
isRTL: false,
}),
},
}));
// Ensure mobile-sdk-alpha's bundled react-native-localize dependency is mocked as well
jest.mock(
'../packages/mobile-sdk-alpha/node_modules/react-native-localize',
() => ({
getLocales: jest.fn().mockReturnValue([
{
countryCode: 'US',
languageTag: 'en-US',
languageCode: 'en',
isRTL: false,
},
]),
getCountry: jest.fn().mockReturnValue('US'),
getTimeZone: jest.fn().mockReturnValue('America/New_York'),
getCurrencies: jest.fn().mockReturnValue(['USD']),
getTemperatureUnit: jest.fn().mockReturnValue('celsius'),
getFirstWeekDay: jest.fn().mockReturnValue(0),
uses24HourClock: jest.fn().mockReturnValue(false),
usesMetricSystem: jest.fn().mockReturnValue(false),
findBestAvailableLanguage: jest.fn().mockReturnValue({
languageTag: 'en-US',
isRTL: false,
}),
default: {
getLocales: jest.fn().mockReturnValue([
{
countryCode: 'US',
languageTag: 'en-US',
languageCode: 'en',
isRTL: false,
},
]),
getCountry: jest.fn().mockReturnValue('US'),
getTimeZone: jest.fn().mockReturnValue('America/New_York'),
getCurrencies: jest.fn().mockReturnValue(['USD']),
getTemperatureUnit: jest.fn().mockReturnValue('celsius'),
getFirstWeekDay: jest.fn().mockReturnValue(0),
uses24HourClock: jest.fn().mockReturnValue(false),
usesMetricSystem: jest.fn().mockReturnValue(false),
findBestAvailableLanguage: jest.fn().mockReturnValue({
languageTag: 'en-US',
isRTL: false,
}),
},
}),
);
jest.mock('./src/utils/notifications/notificationService', () =>
require('./tests/__setup__/notificationServiceMock.js'),
);
// Mock react-native-svg
jest.mock('react-native-svg', () => {
const React = require('react');
// Avoid requiring React to prevent nested require memory issues
// Mock SvgXml component that handles XML strings
const SvgXml = React.forwardRef(
({ xml, width, height, style, ...props }, ref) => {
return React.createElement('div', {
ref,
style: {
width: width || 'auto',
height: height || 'auto',
display: 'inline-block',
...style,
},
dangerouslySetInnerHTML: { __html: xml },
...props,
});
},
);
const SvgXml = jest.fn(() => null);
SvgXml.displayName = 'SvgXml';
return {
__esModule: true,
default: SvgXml,
SvgXml,
Svg: props => React.createElement('Svg', props, props.children),
Circle: props => React.createElement('Circle', props, props.children),
Path: props => React.createElement('Path', props, props.children),
G: props => React.createElement('G', props, props.children),
Rect: props => React.createElement('Rect', props, props.children),
Defs: props => React.createElement('Defs', props, props.children),
LinearGradient: props =>
React.createElement('LinearGradient', props, props.children),
Stop: props => React.createElement('Stop', props, props.children),
ClipPath: props => React.createElement('ClipPath', props, props.children),
Polygon: props => React.createElement('Polygon', props, props.children),
Polyline: props => React.createElement('Polyline', props, props.children),
Line: props => React.createElement('Line', props, props.children),
Text: props => React.createElement('Text', props, props.children),
TSpan: props => React.createElement('TSpan', props, props.children),
Svg: jest.fn(() => null),
Circle: jest.fn(() => null),
Path: jest.fn(() => null),
G: jest.fn(() => null),
Rect: jest.fn(() => null),
Defs: jest.fn(() => null),
LinearGradient: jest.fn(() => null),
Stop: jest.fn(() => null),
ClipPath: jest.fn(() => null),
Polygon: jest.fn(() => null),
Polyline: jest.fn(() => null),
Line: jest.fn(() => null),
Text: jest.fn(() => null),
TSpan: jest.fn(() => null),
};
});
@@ -998,12 +1045,11 @@ jest.mock('@react-navigation/core', () => {
});
// Mock react-native-webview globally to avoid ESM parsing and native behaviors
// Note: Individual test files can override this with their own more specific mocks
jest.mock('react-native-webview', () => {
const React = require('react');
// Use React.createElement directly instead of requiring react-native to avoid memory issues
const MockWebView = React.forwardRef((props, ref) => {
return React.createElement('View', { ref, testID: 'webview', ...props });
});
// Avoid requiring React to prevent nested require memory issues
// Return a simple pass-through mock - tests can override with JSX mocks if needed
const MockWebView = jest.fn(() => null);
MockWebView.displayName = 'MockWebView';
return {
__esModule: true,
@@ -1014,15 +1060,12 @@ jest.mock('react-native-webview', () => {
// Mock ExpandableBottomLayout to simple containers to avoid SDK internals in tests
jest.mock('@/layouts/ExpandableBottomLayout', () => {
const React = require('react');
// Use React.createElement directly instead of requiring react-native to avoid memory issues
const Layout = ({ children }) => React.createElement('View', null, children);
const TopSection = ({ children }) =>
React.createElement('View', null, children);
const BottomSection = ({ children }) =>
React.createElement('View', null, children);
const FullSection = ({ children }) =>
React.createElement('View', null, children);
// Avoid requiring React to prevent nested require memory issues
// These need to pass through children so WebView is rendered
const Layout = ({ children, ...props }) => children;
const TopSection = ({ children, ...props }) => children;
const BottomSection = ({ children, ...props }) => children;
const FullSection = ({ children, ...props }) => children;
return {
__esModule: true,
ExpandableBottomLayout: { Layout, TopSection, BottomSection, FullSection },
@@ -1031,21 +1074,26 @@ jest.mock('@/layouts/ExpandableBottomLayout', () => {
// Mock mobile-sdk-alpha components used by NavBar (Button, XStack)
jest.mock('@selfxyz/mobile-sdk-alpha/components', () => {
const React = require('react');
// Use React.createElement directly instead of requiring react-native to avoid memory issues
const Button = ({ children, onPress, icon, ...props }) =>
React.createElement(
'TouchableOpacity',
{ onPress, ...props, testID: 'msdk-button' },
icon
? React.createElement('View', { testID: 'msdk-button-icon' }, icon)
: null,
children,
// Avoid requiring React to prevent nested require memory issues
// Create mock components that work with React testing library
// Button needs to render a host element with onPress so tests can interact with it
const Button = jest.fn(({ testID, icon, onPress, children, ...props }) => {
// Render as a mock-touchable-opacity host element so fireEvent.press works
// This allows tests to query by testID and press the button
return (
<mock-touchable-opacity testID={testID} onPress={onPress} {...props}>
{icon || children || null}
</mock-touchable-opacity>
);
const XStack = ({ children, ...props }) =>
React.createElement('View', { ...props, testID: 'msdk-xstack' }, children);
const Text = ({ children, ...props }) =>
React.createElement('Text', { ...props }, children);
});
Button.displayName = 'MockButton';
const XStack = jest.fn(({ children, ...props }) => children || null);
XStack.displayName = 'MockXStack';
const Text = jest.fn(({ children, ...props }) => children || null);
Text.displayName = 'MockText';
return {
__esModule: true,
Button,
@@ -1055,18 +1103,110 @@ jest.mock('@selfxyz/mobile-sdk-alpha/components', () => {
};
});
// Mock Tamagui to avoid hermes-parser WASM memory issues during transformation
jest.mock('tamagui', () => {
// Avoid requiring React to prevent nested require memory issues
// Create mock components that work with React testing library
// Helper to create a simple pass-through mock component
const createMockComponent = displayName => {
const Component = jest.fn(props => props.children || null);
Component.displayName = displayName;
return Component;
};
// Mock styled function - simplified version that returns the component
const styled = jest.fn(Component => Component);
// Create all Tamagui component mocks
const Button = createMockComponent('MockButton');
const XStack = createMockComponent('MockXStack');
const YStack = createMockComponent('MockYStack');
const ZStack = createMockComponent('MockZStack');
const Text = createMockComponent('MockText');
const View = createMockComponent('MockView');
const ScrollView = createMockComponent('MockScrollView');
const Spinner = createMockComponent('MockSpinner');
const Image = createMockComponent('MockImage');
const Card = createMockComponent('MockCard');
const Separator = createMockComponent('MockSeparator');
const TextArea = createMockComponent('MockTextArea');
const Input = createMockComponent('MockInput');
const Anchor = createMockComponent('MockAnchor');
// Mock Select component with nested components
const Select = Object.assign(createMockComponent('MockSelect'), {
Trigger: createMockComponent('MockSelectTrigger'),
Value: createMockComponent('MockSelectValue'),
Content: createMockComponent('MockSelectContent'),
Item: createMockComponent('MockSelectItem'),
Group: createMockComponent('MockSelectGroup'),
Label: createMockComponent('MockSelectLabel'),
Viewport: createMockComponent('MockSelectViewport'),
ScrollUpButton: createMockComponent('MockSelectScrollUpButton'),
ScrollDownButton: createMockComponent('MockSelectScrollDownButton'),
});
// Mock Sheet component with nested components
const Sheet = Object.assign(createMockComponent('MockSheet'), {
Frame: createMockComponent('MockSheetFrame'),
Overlay: createMockComponent('MockSheetOverlay'),
Handle: createMockComponent('MockSheetHandle'),
ScrollView: createMockComponent('MockSheetScrollView'),
});
// Mock Adapt component
const Adapt = createMockComponent('MockAdapt');
// Mock TamaguiProvider - simple pass-through that renders children
const TamaguiProvider = jest.fn(({ children }) => children || null);
TamaguiProvider.displayName = 'MockTamaguiProvider';
// Mock configuration factory functions
const createFont = jest.fn(() => ({}));
const createTamagui = jest.fn(() => ({}));
return {
__esModule: true,
styled,
Button,
XStack,
YStack,
ZStack,
Text,
View,
ScrollView,
Spinner,
Image,
Card,
Separator,
TextArea,
Input,
Anchor,
Select,
Sheet,
Adapt,
TamaguiProvider,
createFont,
createTamagui,
// Provide default exports for other common components
default: jest.fn(() => null),
};
});
// Mock Tamagui lucide icons to simple components to avoid theme context
jest.mock('@tamagui/lucide-icons', () => {
const React = require('react');
// Use React.createElement directly instead of requiring react-native to avoid memory issues
// Avoid requiring React to prevent nested require memory issues
// Return mock components that can be queried by testID
const makeIcon = name => {
const Icon = ({ size, color, opacity }) =>
React.createElement('View', {
testID: `icon-${name}`,
size,
color,
opacity,
});
// Use a mock element tag that React can render
const Icon = props => ({
$$typeof: Symbol.for('react.element'),
type: `mock-icon-${name}`,
props: { testID: `icon-${name}`, ...props },
key: null,
ref: null,
});
Icon.displayName = `MockIcon(${name})`;
return Icon;
};
@@ -1074,14 +1214,13 @@ jest.mock('@tamagui/lucide-icons', () => {
__esModule: true,
ExternalLink: makeIcon('external-link'),
X: makeIcon('x'),
Clipboard: makeIcon('clipboard'),
};
});
// Mock WebViewFooter to avoid SDK rendering complexity
jest.mock('@/components/WebViewFooter', () => {
const React = require('react');
// Use React.createElement directly instead of requiring react-native to avoid memory issues
const WebViewFooter = () =>
React.createElement('View', { testID: 'webview-footer' });
// Avoid requiring React to prevent nested require memory issues
const WebViewFooter = jest.fn(() => null);
return { __esModule: true, WebViewFooter };
});

View File

@@ -56,17 +56,18 @@
"sync-versions": "bundle exec fastlane ios sync_version && bundle exec fastlane android sync_version",
"tag:release": "node scripts/tag.cjs release",
"tag:remove": "node scripts/tag.cjs remove",
"test": "yarn build:deps && jest --passWithNoTests && node --test scripts/tests/*.cjs",
"test": "yarn build:deps && yarn jest:run --passWithNoTests && node --test scripts/tests/*.cjs",
"test:build": "yarn build:deps && yarn types && node ./scripts/bundle-analyze-ci.cjs ios && yarn test",
"test:ci": "jest --passWithNoTests && node --test scripts/tests/*.cjs",
"test:coverage": "jest --coverage --passWithNoTests",
"test:coverage:ci": "jest --coverage --passWithNoTests --ci --coverageReporters=lcov --coverageReporters=text --coverageReporters=json",
"test:ci": "yarn jest:run --passWithNoTests && node --test scripts/tests/*.cjs",
"test:coverage": "yarn jest:run --coverage --passWithNoTests",
"test:coverage:ci": "yarn jest:run --coverage --passWithNoTests --ci --coverageReporters=lcov --coverageReporters=text --coverageReporters=json",
"test:e2e:android": "./scripts/mobile-ci-build-android.sh && maestro test tests/e2e/launch.android.flow.yaml",
"test:e2e:ios": "xcodebuild -workspace ios/OpenPassport.xcworkspace -scheme OpenPassport -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build && maestro test tests/e2e/launch.ios.flow.yaml",
"test:fastlane": "bundle exec ruby -Itest fastlane/test/helpers_test.rb",
"test:tree-shaking": "node ./scripts/test-tree-shaking.cjs",
"test:web-build": "jest tests/web-build-render.test.ts --testTimeout=180000",
"test:web-build": "yarn jest:run tests/web-build-render.test.ts --testTimeout=180000",
"types": "tsc --noEmit",
"jest:run": "node ./node_modules/jest/bin/jest.js",
"watch:sdk": "yarn workspace @selfxyz/mobile-sdk-alpha watch",
"web": "vite",
"web:build": "yarn build:deps && vite build",
@@ -168,9 +169,13 @@
},
"devDependencies": {
"@babel/core": "^7.28.3",
"@babel/plugin-syntax-flow": "^7.27.1",
"@babel/plugin-transform-classes": "^7.27.1",
"@babel/plugin-transform-export-namespace-from": "^7.27.1",
"@babel/plugin-transform-flow-strip-types": "^7.27.1",
"@babel/plugin-transform-private-methods": "^7.27.1",
"@babel/preset-env": "^7.28.3",
"@babel/preset-react": "^7.27.1",
"@react-native-community/cli": "^16.0.3",
"@react-native/babel-preset": "0.76.9",
"@react-native/eslint-config": "0.76.9",

View File

@@ -120,6 +120,11 @@ const notifyDocumentChange = (isMock: boolean) => {
// Global flag to track if native modules are ready
let nativeModulesReady = false;
// Test-only helper so unit tests can reset module-level state without re-importing
export function __resetPassportProviderTestState() {
nativeModulesReady = false;
}
export const PassportContext = createContext<IPassportContext>({
getData: () => Promise.resolve(null),
getSelectedData: () => Promise.resolve(null),

View File

@@ -266,9 +266,13 @@ const handleResponseAndroid = (response: AndroidScanResponse): PassportData => {
const dgHashesObj = JSON.parse(dataGroupHashes);
const dg1HashString = dgHashesObj['1'];
const dg1Hash = Array.from(Buffer.from(dg1HashString, 'hex'));
const dg1Hash = dg1HashString
? Array.from(Buffer.from(dg1HashString, 'hex'))
: [];
const dg2HashString = dgHashesObj['2'];
const dg2Hash = Array.from(Buffer.from(dg2HashString, 'hex'));
const dg2Hash = dg2HashString
? Array.from(Buffer.from(dg2HashString, 'hex'))
: [];
const pem =
'-----BEGIN CERTIFICATE-----' +
documentSigningCertificate +

View File

@@ -2,7 +2,7 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import { Text } from 'react-native';
import type { ReactNode } from 'react';
import { render } from '@testing-library/react-native';
import ErrorBoundary from '@/components/ErrorBoundary';
@@ -18,11 +18,19 @@ jest.mock('@/Sentry', () => ({
captureException: jest.fn(),
}));
const MockText = ({
children,
testID,
}: {
children?: ReactNode;
testID?: string;
}) => <mock-text testID={testID}>{children}</mock-text>;
const ProblemChild = () => {
throw new Error('boom');
};
const GoodChild = () => <Text>Good child</Text>;
const GoodChild = () => <MockText testID="good-child">Good child</MockText>;
describe('ErrorBoundary', () => {
beforeEach(() => {
@@ -87,13 +95,13 @@ describe('ErrorBoundary', () => {
});
it('renders children normally when no error occurs', () => {
const { getByText } = render(
const { getByTestId } = render(
<ErrorBoundary>
<GoodChild />
</ErrorBoundary>,
);
expect(getByText('Good child')).toBeTruthy();
expect(getByTestId('good-child')).toHaveTextContent('Good child');
});
it('captures error details correctly', () => {

View File

@@ -6,11 +6,12 @@
* @jest-environment node
*/
import type { ReactNode } from 'react';
import { useEffect } from 'react';
import { Text } from 'react-native';
import { render, screen } from '@testing-library/react-native';
import { LoggerProvider, useLogger } from '@/providers/loggerProvider';
import { AppLogger, NfcLogger } from '@/utils/logger';
// Mock the native logger bridge
jest.mock('@/utils/logger/nativeLoggerBridge', () => ({
@@ -97,6 +98,14 @@ jest.mock('@/utils/logger', () => ({
},
}));
const MockText = ({
children,
testID,
}: {
children?: ReactNode;
testID?: string;
}) => <mock-text testID={testID}>{children}</mock-text>;
// Test component that uses the logger
const TestComponent = () => {
const loggers = useLogger();
@@ -108,9 +117,9 @@ const TestComponent = () => {
}, [loggers]);
return (
<Text testID="test-component">
<MockText testID="test-component">
Test Component - AppLogger Level: {loggers.logLevels.info}
</Text>
</MockText>
);
};
@@ -128,12 +137,11 @@ describe('LoggerProvider', () => {
// Verify the component renders without errors and shows context values
expect(screen.getByTestId('test-component')).toBeTruthy();
expect(
screen.getByText('Test Component - AppLogger Level: 1'),
).toBeTruthy();
expect(screen.getByTestId('test-component')).toHaveTextContent(
/Test Component - AppLogger Level:\s*1/,
);
// Verify that logger methods were called with expected arguments
const { AppLogger, NfcLogger } = require('@/utils/logger');
expect(AppLogger.info).toHaveBeenCalledWith('Test message');
expect(NfcLogger.debug).toHaveBeenCalledWith('NFC test');
});
@@ -141,11 +149,13 @@ describe('LoggerProvider', () => {
it('should initialize and allow loggers to be called', () => {
render(
<LoggerProvider>
<Text>Test</Text>
<MockText testID="logger-provider-text">Test</MockText>
</LoggerProvider>,
);
// The TestComponent is rendered in other tests; here we just assert provider renders without errors
expect(screen.getByText('Test')).toBeTruthy();
expect(screen.getByTestId('logger-provider-text')).toHaveTextContent(
'Test',
);
});
it('should throw error when useLogger is used outside LoggerProvider', () => {
@@ -165,12 +175,14 @@ describe('LoggerProvider', () => {
// The nativeLoggerBridge import should be called when LoggerProvider is rendered
render(
<LoggerProvider>
<Text>Test</Text>
<MockText testID="logger-provider-text">Test</MockText>
</LoggerProvider>,
);
// Verify that the LoggerProvider renders without errors
expect(screen.getByText('Test')).toBeTruthy();
expect(screen.getByTestId('logger-provider-text')).toHaveTextContent(
'Test',
);
});
it('should provide logLevels constant', () => {

View File

@@ -2,30 +2,41 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import type { ReactNode } from 'react';
import { useEffect, useState } from 'react';
import { Text } from 'react-native';
import { render, waitFor } from '@testing-library/react-native';
import { SelfClientProvider } from '@selfxyz/mobile-sdk-alpha';
// Import after mocking
import {
__resetPassportProviderTestState,
initializeNativeModules,
loadDocumentCatalogDirectlyFromKeychain,
migrateFromLegacyStorage,
PassportProvider,
usePassport,
} from '@/providers/passportDataProvider';
import { mockAdapters } from '../../utils/selfClientProvider';
// Mock react-native-keychain before importing the module
const mockKeychain = {
getGenericPassword: jest.fn(),
setGenericPassword: jest.fn(),
resetGenericPassword: jest.fn(),
};
const listeners = new Map();
jest.mock('react-native-keychain', () => mockKeychain);
jest.mock('react-native-keychain', () => {
const mockKeychain = {
getGenericPassword: jest.fn(),
setGenericPassword: jest.fn(),
resetGenericPassword: jest.fn(),
};
return mockKeychain;
});
const mockKeychain = jest.requireMock('react-native-keychain') as {
getGenericPassword: jest.Mock;
setGenericPassword: jest.Mock;
resetGenericPassword: jest.Mock;
};
// Mock the auth provider
const mockAuthProvider = {
@@ -36,6 +47,14 @@ jest.mock('@/providers/authProvider', () => ({
useAuth: () => mockAuthProvider,
}));
const MockText = ({
children,
testID,
}: {
children?: ReactNode;
testID?: string;
}) => <mock-text testID={testID}>{children}</mock-text>;
// Test component that uses the passport hook and extracts context values
const TestComponent = () => {
const passportContext = usePassport();
@@ -53,15 +72,17 @@ const TestComponent = () => {
return (
<>
<Text testID="context-functions-count">
<MockText testID="context-functions-count">
{contextValues.length} functions available
</Text>
<Text testID="context-functions-list">{contextValues.join(',')}</Text>
<Text testID="getData-available">getData available</Text>
<Text testID="setData-available">setData available</Text>
<Text testID="loadDocumentCatalog-available">
</MockText>
<MockText testID="context-functions-list">
{contextValues.join(',')}
</MockText>
<MockText testID="getData-available">getData available</MockText>
<MockText testID="setData-available">setData available</MockText>
<MockText testID="loadDocumentCatalog-available">
loadDocumentCatalog available
</Text>
</MockText>
</>
);
};
@@ -73,20 +94,20 @@ const MultipleConsumersTest = () => {
return (
<>
<Text testID="consumer1-functions">
<MockText testID="consumer1-functions">
{
Object.keys(context1).filter(
key => typeof context1[key as keyof typeof context1] === 'function',
).length
}
</Text>
<Text testID="consumer2-functions">
</MockText>
<MockText testID="consumer2-functions">
{
Object.keys(context2).filter(
key => typeof context2[key as keyof typeof context2] === 'function',
).length
}
</Text>
</MockText>
</>
);
};
@@ -103,7 +124,9 @@ const ErrorBoundaryTest = () => {
}
};
return <Text testID="error-test-result">{testContextFunction()}</Text>;
return (
<MockText testID="error-test-result">{testContextFunction()}</MockText>
);
};
// Component to test context updates
@@ -118,7 +141,7 @@ const ContextUpdateTest = () => {
return () => clearInterval(interval);
}, []);
return <Text testID="update-count">{updateCount}</Text>;
return <MockText testID="update-count">{updateCount}</MockText>;
};
describe('PassportDataProvider', () => {
@@ -126,6 +149,7 @@ describe('PassportDataProvider', () => {
jest.clearAllMocks();
console.log = jest.fn();
console.warn = jest.fn();
__resetPassportProviderTestState();
});
afterEach(() => {
@@ -327,16 +351,9 @@ describe('PassportDataProvider', () => {
});
describe('initializeNativeModules', () => {
let initializeNativeModulesLocal: any;
beforeEach(() => {
jest.clearAllMocks();
// Reset module state for each test by re-importing
jest.resetModules();
jest.doMock('react-native-keychain', () => mockKeychain);
const passportModule = require('@/providers/passportDataProvider');
initializeNativeModulesLocal = passportModule.initializeNativeModules;
__resetPassportProviderTestState();
});
it('should return true immediately if native modules are already ready', async () => {
@@ -346,7 +363,7 @@ describe('PassportDataProvider', () => {
});
// First call should initialize
const firstResult = await initializeNativeModulesLocal();
const firstResult = await initializeNativeModules();
expect(firstResult).toBe(true);
expect(mockKeychain.getGenericPassword).toHaveBeenCalledTimes(1);
@@ -354,7 +371,7 @@ describe('PassportDataProvider', () => {
jest.clearAllMocks();
// Subsequent calls should return immediately without hitting keychain
const secondResult = await initializeNativeModulesLocal();
const secondResult = await initializeNativeModules();
expect(secondResult).toBe(true);
expect(mockKeychain.getGenericPassword).not.toHaveBeenCalled();
});
@@ -370,7 +387,7 @@ describe('PassportDataProvider', () => {
.mockRejectedValueOnce(moduleError)
.mockResolvedValue({ password: 'test' });
const result = await initializeNativeModulesLocal(3, 10); // 3 retries, 10ms delay
const result = await initializeNativeModules(3, 10); // 3 retries, 10ms delay
expect(result).toBe(true);
expect(mockKeychain.getGenericPassword).toHaveBeenCalledTimes(3);
@@ -385,7 +402,7 @@ describe('PassportDataProvider', () => {
.fn()
.mockRejectedValue(moduleError);
const result = await initializeNativeModulesLocal(2, 10); // 2 retries, 10ms delay
const result = await initializeNativeModules(2, 10); // 2 retries, 10ms delay
expect(result).toBe(false);
expect(mockKeychain.getGenericPassword).toHaveBeenCalledTimes(2);
@@ -396,7 +413,7 @@ describe('PassportDataProvider', () => {
const otherError = new Error('Service not found');
mockKeychain.getGenericPassword = jest.fn().mockRejectedValue(otherError);
const result = await initializeNativeModulesLocal();
const result = await initializeNativeModules();
expect(result).toBe(true);
expect(mockKeychain.getGenericPassword).toHaveBeenCalledTimes(1);
@@ -404,20 +421,12 @@ describe('PassportDataProvider', () => {
});
describe('migrateFromLegacyStorage', () => {
let migrateFromLegacyStorageLocal: any;
beforeEach(() => {
jest.clearAllMocks();
jest.resetModules();
jest.doMock('react-native-keychain', () => mockKeychain);
const passportModule = require('@/providers/passportDataProvider');
migrateFromLegacyStorageLocal = passportModule.migrateFromLegacyStorage;
__resetPassportProviderTestState();
});
it('should skip migration if catalog already has documents', async () => {
// First initialize native modules to set the flag
const passportModule = require('@/providers/passportDataProvider');
mockKeychain.getGenericPassword = jest
.fn()
.mockResolvedValueOnce({ password: 'test' }) // For initializeNativeModules
@@ -426,11 +435,11 @@ describe('PassportDataProvider', () => {
}); // For loadDocumentCatalog
// Initialize native modules first
await passportModule.initializeNativeModules();
await initializeNativeModules();
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
await migrateFromLegacyStorageLocal();
await migrateFromLegacyStorage();
// Should log that migration is already completed
expect(consoleSpy).toHaveBeenCalledWith('Migration already completed');
@@ -439,8 +448,6 @@ describe('PassportDataProvider', () => {
});
it('should migrate legacy documents when catalog is empty', async () => {
// First initialize native modules to set the flag
const passportModule = require('@/providers/passportDataProvider');
mockKeychain.getGenericPassword = jest
.fn()
.mockResolvedValueOnce({ password: 'test' }) // For initializeNativeModules
@@ -453,11 +460,11 @@ describe('PassportDataProvider', () => {
.mockResolvedValue(false); // No more legacy documents
// Initialize native modules first
await passportModule.initializeNativeModules();
await initializeNativeModules();
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
await migrateFromLegacyStorageLocal();
await migrateFromLegacyStorage();
// Should log migration start and completion
expect(consoleSpy).toHaveBeenCalledWith(
@@ -469,8 +476,6 @@ describe('PassportDataProvider', () => {
});
it('should handle errors during migration gracefully', async () => {
// First initialize native modules to set the flag
const passportModule = require('@/providers/passportDataProvider');
mockKeychain.getGenericPassword = jest
.fn()
.mockResolvedValueOnce({ password: 'test' }) // For initializeNativeModules
@@ -480,11 +485,11 @@ describe('PassportDataProvider', () => {
.mockRejectedValue(new Error('Keychain error')); // Error on legacy service
// Initialize native modules first
await passportModule.initializeNativeModules();
await initializeNativeModules();
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
await migrateFromLegacyStorageLocal();
await migrateFromLegacyStorage();
// Should log error for each service that fails
expect(consoleSpy).toHaveBeenCalledWith(
@@ -497,30 +502,22 @@ describe('PassportDataProvider', () => {
});
describe('loadDocumentCatalog', () => {
let loadDocumentCatalogLocal: any;
beforeEach(() => {
jest.clearAllMocks();
jest.resetModules();
jest.doMock('react-native-keychain', () => mockKeychain);
const passportModule = require('@/providers/passportDataProvider');
loadDocumentCatalogLocal =
passportModule.loadDocumentCatalogDirectlyFromKeychain;
__resetPassportProviderTestState();
});
it('should return empty catalog when Keychain is undefined', async () => {
// Reset module registry to ensure mock takes effect
jest.resetModules();
// Mock that Keychain is undefined
jest.doMock('react-native-keychain', () => undefined);
// Re-import the module after mocking to ensure mock is applied
const passportModule = require('@/providers/passportDataProvider');
const loadDocumentCatalogLocalUndefined =
passportModule.loadDocumentCatalogDirectlyFromKeychain;
const result = await loadDocumentCatalogLocalUndefined();
const result = await new Promise((resolve, reject) => {
jest.isolateModules(() => {
jest.doMock('react-native-keychain', () => undefined);
const passportModule = require('@/providers/passportDataProvider');
passportModule
.loadDocumentCatalogDirectlyFromKeychain()
.then(resolve)
.catch(reject);
});
});
expect(result).toEqual({ documents: [] });
});
@@ -528,7 +525,7 @@ describe('PassportDataProvider', () => {
it('should return empty catalog when no catalog exists', async () => {
mockKeychain.getGenericPassword = jest.fn().mockResolvedValue(false);
const result = await loadDocumentCatalogLocal();
const result = await loadDocumentCatalogDirectlyFromKeychain();
expect(result).toEqual({ documents: [] });
});
@@ -540,7 +537,7 @@ describe('PassportDataProvider', () => {
password: JSON.stringify({ documents: [{ id: 'test' }] }),
});
const result = await loadDocumentCatalogLocal();
const result = await loadDocumentCatalogDirectlyFromKeychain();
// The function should return empty catalog due to nativeModulesReady check
expect(result).toEqual({ documents: [] });
@@ -548,7 +545,6 @@ describe('PassportDataProvider', () => {
it('should return parsed catalog when it exists and native modules are ready', async () => {
// First initialize native modules to set the flag
const passportModule = require('@/providers/passportDataProvider');
mockKeychain.getGenericPassword = jest
.fn()
.mockResolvedValueOnce({ password: 'test' }) // For initializeNativeModules
@@ -557,17 +553,16 @@ describe('PassportDataProvider', () => {
}); // For loadDocumentCatalog
// Initialize native modules first
await passportModule.initializeNativeModules();
await initializeNativeModules();
// Now test loadDocumentCatalog
const result = await loadDocumentCatalogLocal();
const result = await loadDocumentCatalogDirectlyFromKeychain();
expect(result).toEqual({ documents: [{ id: 'test' }] });
});
it('should handle malformed JSON and return empty documents array', async () => {
// First initialize native modules to set the flag
const passportModule = require('@/providers/passportDataProvider');
mockKeychain.getGenericPassword = jest
.fn()
.mockResolvedValueOnce({ password: 'test' }) // For initializeNativeModules
@@ -576,11 +571,11 @@ describe('PassportDataProvider', () => {
}); // For loadDocumentCatalog
// Initialize native modules first
await passportModule.initializeNativeModules();
await initializeNativeModules();
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
const result = await loadDocumentCatalogLocal();
const result = await loadDocumentCatalogDirectlyFromKeychain();
expect(result).toEqual({ documents: [] });
expect(consoleLogSpy).toHaveBeenCalledWith(
@@ -593,7 +588,6 @@ describe('PassportDataProvider', () => {
it('should handle invalid catalog structure and return the parsed structure', async () => {
// First initialize native modules to set the flag
const passportModule = require('@/providers/passportDataProvider');
mockKeychain.getGenericPassword = jest
.fn()
.mockResolvedValueOnce({ password: 'test' }) // For initializeNativeModules
@@ -602,9 +596,9 @@ describe('PassportDataProvider', () => {
}); // For loadDocumentCatalog
// Initialize native modules first
await passportModule.initializeNativeModules();
await initializeNativeModules();
const result = await loadDocumentCatalogLocal();
const result = await loadDocumentCatalogDirectlyFromKeychain();
// The function returns the parsed JSON as-is, even if it doesn't have the expected structure
expect(result).toEqual({ invalidField: 'test' });
@@ -612,7 +606,6 @@ describe('PassportDataProvider', () => {
it('should handle JSON parsing exceptions and return empty documents array', async () => {
// First initialize native modules to set the flag
const passportModule = require('@/providers/passportDataProvider');
mockKeychain.getGenericPassword = jest
.fn()
.mockResolvedValueOnce({ password: 'test' }) // For initializeNativeModules
@@ -621,11 +614,11 @@ describe('PassportDataProvider', () => {
}); // For loadDocumentCatalog
// Initialize native modules first
await passportModule.initializeNativeModules();
await initializeNativeModules();
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
const result = await loadDocumentCatalogLocal();
const result = await loadDocumentCatalogDirectlyFromKeychain();
expect(result).toEqual({ documents: [] });
expect(consoleLogSpy).toHaveBeenCalledWith(
@@ -638,7 +631,6 @@ describe('PassportDataProvider', () => {
it('should handle null/undefined password and return empty documents array', async () => {
// First initialize native modules to set the flag
const passportModule = require('@/providers/passportDataProvider');
mockKeychain.getGenericPassword = jest
.fn()
.mockResolvedValueOnce({ password: 'test' }) // For initializeNativeModules
@@ -647,18 +639,11 @@ describe('PassportDataProvider', () => {
}); // For loadDocumentCatalog
// Initialize native modules first
await passportModule.initializeNativeModules();
await initializeNativeModules();
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
console.log('About to call loadDocumentCatalogLocal');
const result = await loadDocumentCatalogLocal();
console.log('Called loadDocumentCatalogLocal');
console.log('Actual result:', result);
console.log('Result type:', typeof result);
console.log('Is null?', result === null);
console.log('Function name:', loadDocumentCatalogLocal.name);
const result = await loadDocumentCatalogDirectlyFromKeychain();
// When password is null, JSON.parse(null) throws TypeError, which is caught
// and the function returns empty catalog
@@ -673,7 +658,6 @@ describe('PassportDataProvider', () => {
it('should handle empty string password and return empty documents array', async () => {
// First initialize native modules to set the flag
const passportModule = require('@/providers/passportDataProvider');
mockKeychain.getGenericPassword = jest
.fn()
.mockResolvedValueOnce({ password: 'test' }) // For initializeNativeModules
@@ -682,11 +666,11 @@ describe('PassportDataProvider', () => {
}); // For loadDocumentCatalog
// Initialize native modules first
await passportModule.initializeNativeModules();
await initializeNativeModules();
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
const result = await loadDocumentCatalogLocal();
const result = await loadDocumentCatalogDirectlyFromKeychain();
expect(result).toEqual({ documents: [] });
expect(consoleLogSpy).toHaveBeenCalledWith(

View File

@@ -2,7 +2,7 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import { Text } from 'react-native';
import type { ReactNode } from 'react';
import { render, waitFor } from '@testing-library/react-native';
import {
@@ -21,12 +21,22 @@ const mockInitRemoteConfig = initRemoteConfig as jest.MockedFunction<
>;
// Test component that uses the hook
const MockText = ({
children,
testID,
}: {
children?: ReactNode;
testID: string;
}) => <mock-text testID={testID}>{children}</mock-text>;
const TestComponent = () => {
const { isInitialized, error } = useRemoteConfig();
return (
<>
<Text testID="initialized">{isInitialized ? 'true' : 'false'}</Text>
<Text testID="error">{error || 'none'}</Text>
<MockText testID="initialized">
{isInitialized ? 'true' : 'false'}
</MockText>
<MockText testID="error">{error || 'none'}</MockText>
</>
);
};

View File

@@ -6,9 +6,41 @@
import type { ReactNode } from 'react';
import { renderHook } from '@testing-library/react-native';
import { useSelfClient } from '@selfxyz/mobile-sdk-alpha';
jest.mock('@/images/512w.png', () => 'mock-512w-image');
jest.mock('@/images/nfc.png', () => 'mock-nfc-image');
jest.mock('react-native-localize', () => {
const getLocales = jest.fn(() => [
{
countryCode: 'US',
languageTag: 'en-US',
languageCode: 'en',
isRTL: false,
},
]);
const getCountry = jest.fn(() => 'US');
import { SelfClientProvider } from '@/providers/selfClientProvider';
return {
__esModule: true,
default: {
getLocales,
getCountry,
},
getLocales,
getCountry,
};
});
jest.mock('@/navigation', () => {
const navigationRef = {
isReady: jest.fn(() => false),
navigate: jest.fn(),
};
return {
__esModule: true,
navigationRef,
default: navigationRef,
};
});
jest.mock(
'@selfxyz/mobile-sdk-alpha/onboarding/confirm-identification',
@@ -17,6 +49,54 @@ jest.mock(
}),
);
jest.mock('@selfxyz/mobile-sdk-alpha', () => {
const mockClient = {
getSelfAppState: jest.fn(() => ({})),
getProtocolState: jest.fn(() => ({})),
getDeepLinksState: jest.fn(() => ({})),
};
const mockSdkProvider = ({ children }: any) => (
<mock-sdk-provider>{children}</mock-sdk-provider>
);
const createListenersMap = () => {
const map = new Map();
return {
map,
addListener: (event: string, callback: (...args: unknown[]) => void) => {
map.set(event, callback);
},
};
};
const SdkEvents = {
PROVING_PASSPORT_DATA_NOT_FOUND: 'PROVING_PASSPORT_DATA_NOT_FOUND',
PROVING_ACCOUNT_VERIFIED_SUCCESS: 'PROVING_ACCOUNT_VERIFIED_SUCCESS',
PROVING_REGISTER_ERROR_OR_FAILURE: 'PROVING_REGISTER_ERROR_OR_FAILURE',
PROVING_ACCOUNT_VERIFIED_PENDING: 'PROVING_ACCOUNT_VERIFIED_PENDING',
PROVING_ACCOUNT_VERIFIED_FAILURE: 'PROVING_ACCOUNT_VERIFIED_FAILURE',
};
return {
__esModule: true,
useSelfClient: jest.fn(() => mockClient),
SelfClientProvider: mockSdkProvider,
createListenersMap,
impactLight: jest.fn(),
reactNativeScannerAdapter: {},
SdkEvents,
webNFCScannerShim: {},
};
});
let useSelfClient: () => unknown;
let SelfClientProvider: ({ children }: { children: ReactNode }) => JSX.Element;
beforeAll(() => {
({ useSelfClient } = require('@selfxyz/mobile-sdk-alpha'));
({ SelfClientProvider } = require('@/providers/selfClientProvider'));
});
describe('SelfClientProvider', () => {
it('memoises the client instance', () => {
const wrapper = ({ children }: { children: ReactNode }) => (

View File

@@ -7,6 +7,44 @@ import { render, waitFor } from '@testing-library/react-native';
import GratificationScreen from '@/screens/app/GratificationScreen';
jest.mock('react-native', () => {
const MockView = ({ children, ...props }: any) => (
<mock-view {...props}>{children}</mock-view>
);
const MockText = ({ children, ...props }: any) => (
<mock-text {...props}>{children}</mock-text>
);
const mockDimensions = {
get: jest.fn(() => ({ width: 320, height: 640 })),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
};
return {
__esModule: true,
Dimensions: mockDimensions,
Pressable: ({ onPress, children }: any) => (
<button onClick={onPress} type="button">
{children}
</button>
),
StyleSheet: {
create: (styles: any) => styles,
flatten: (style: any) => style,
},
Text: MockText,
View: MockView,
};
});
jest.mock('react-native-edge-to-edge', () => ({
SystemBars: () => null,
}));
jest.mock('react-native-safe-area-context', () => ({
useSafeAreaInsets: jest.fn(() => ({ top: 0, bottom: 0 })),
}));
jest.mock('@react-navigation/native', () => ({
useNavigation: jest.fn(),
useRoute: jest.fn(),

View File

@@ -2,7 +2,6 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import { Linking } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import {
fireEvent,
@@ -13,11 +12,69 @@ import {
import { WebViewScreen } from '@/screens/shared/WebViewScreen';
jest.mock('react-native', () => {
const mockLinking = {
canOpenURL: jest.fn(),
openURL: jest.fn(),
};
const MockView = ({ children, ...props }: any) => (
<mock-view {...props}>{children}</mock-view>
);
const mockBackHandler = {
addEventListener: jest.fn(() => ({ remove: jest.fn() })),
removeEventListener: jest.fn(),
};
return {
ActivityIndicator: (props: any) => <mock-activity-indicator {...props} />,
BackHandler: mockBackHandler,
Linking: mockLinking,
StyleSheet: {
create: (styles: unknown) => styles,
flatten: (style: unknown) => style,
},
View: MockView,
};
});
const mockLinking = jest.requireMock('react-native').Linking as jest.Mocked<{
canOpenURL: jest.Mock;
openURL: jest.Mock;
}>;
jest.mock('@react-navigation/native', () => ({
useNavigation: jest.fn(),
useFocusEffect: jest.fn(),
}));
jest.mock('@/components/NavBar/WebViewNavBar', () => ({
WebViewNavBar: ({ children, onBackPress, ...props }: any) => (
<mock-webview-navbar {...props}>
<mock-pressable testID="icon-x" onPress={onBackPress} />
{children}
</mock-webview-navbar>
),
}));
jest.mock('@/components/WebViewFooter', () => ({
WebViewFooter: () => <mock-webview-footer />,
}));
jest.mock('@/layouts/ExpandableBottomLayout', () => ({
ExpandableBottomLayout: {
Layout: ({ children, ...props }: any) => (
<mock-expandable-layout {...props}>{children}</mock-expandable-layout>
),
TopSection: ({ children, ...props }: any) => (
<mock-expandable-top {...props}>{children}</mock-expandable-top>
),
BottomSection: ({ children, ...props }: any) => (
<mock-expandable-bottom {...props}>{children}</mock-expandable-bottom>
),
},
}));
jest.mock('react-native-webview', () => {
// Lightweight host component so React can render while keeping props inspectable
const MockWebView = ({ testID = 'webview', ...props }: any) => (
@@ -56,6 +113,8 @@ describe('WebViewScreen URL sanitization and navigation interception', () => {
canGoBack: () => true,
});
jest.spyOn(console, 'error').mockImplementation(() => {});
mockLinking.canOpenURL.mockReset();
mockLinking.openURL.mockReset();
});
afterEach(() => {
@@ -67,8 +126,7 @@ describe('WebViewScreen URL sanitization and navigation interception', () => {
render(<WebViewScreen {...createProps('https://self.xyz')} />);
// The Button component renders with msdk-button testID, find by icon
const closeButtonIcon = screen.getByTestId('icon-x');
const closeButton = closeButtonIcon.parent?.parent;
fireEvent.press(closeButton!);
fireEvent.press(closeButtonIcon);
expect(mockGoBack).toHaveBeenCalledTimes(1);
});
@@ -107,10 +165,8 @@ describe('WebViewScreen URL sanitization and navigation interception', () => {
});
it('opens allowed external schemes externally and blocks in WebView (mailto, tel)', async () => {
jest.spyOn(Linking, 'canOpenURL').mockResolvedValue(true as any);
const openSpy = jest
.spyOn(Linking, 'openURL')
.mockResolvedValue(undefined as any);
mockLinking.canOpenURL.mockResolvedValue(true as any);
mockLinking.openURL.mockResolvedValue(undefined as any);
render(<WebViewScreen {...createProps('https://self.xyz')} />);
const webview = screen.getByTestId('webview');
@@ -119,19 +175,21 @@ describe('WebViewScreen URL sanitization and navigation interception', () => {
});
expect(resultMailto).toBe(false);
await waitFor(() =>
expect(openSpy).toHaveBeenCalledWith('mailto:test@example.com'),
expect(mockLinking.openURL).toHaveBeenCalledWith(
'mailto:test@example.com',
),
);
const resultTel = await webview.props.onShouldStartLoadWithRequest?.({
url: 'tel:+123456789',
});
expect(resultTel).toBe(false);
await waitFor(() => expect(openSpy).toHaveBeenCalledWith('tel:+123456789'));
await waitFor(() =>
expect(mockLinking.openURL).toHaveBeenCalledWith('tel:+123456789'),
);
});
it('blocks disallowed external schemes and does not attempt to open', async () => {
const canOpenSpy = jest.spyOn(Linking, 'canOpenURL');
const openSpy = jest.spyOn(Linking, 'openURL');
render(<WebViewScreen {...createProps('https://self.xyz')} />);
const webview = screen.getByTestId('webview');
@@ -139,13 +197,13 @@ describe('WebViewScreen URL sanitization and navigation interception', () => {
url: 'ftp://example.com',
});
expect(result).toBe(false);
expect(canOpenSpy).not.toHaveBeenCalled();
expect(openSpy).not.toHaveBeenCalled();
expect(mockLinking.canOpenURL).not.toHaveBeenCalled();
expect(mockLinking.openURL).not.toHaveBeenCalled();
});
it('scrubs error log wording when external open fails', async () => {
jest.spyOn(Linking, 'canOpenURL').mockResolvedValue(true as any);
jest.spyOn(Linking, 'openURL').mockRejectedValue(new Error('boom'));
mockLinking.canOpenURL.mockResolvedValue(true as any);
mockLinking.openURL.mockRejectedValue(new Error('boom'));
render(<WebViewScreen {...createProps('https://self.xyz')} />);
const webview = screen.getByTestId('webview');

View File

@@ -13,14 +13,17 @@ import {
} from '@/utils/proving/validateDocument';
// Mock the analytics module to avoid side effects in tests
let mockTrackEvent: jest.Mock;
jest.mock('@/utils/analytics', () => {
mockTrackEvent = jest.fn();
const mockTrackEvent = jest.fn();
return () => ({
trackEvent: mockTrackEvent,
});
});
const mockTrackEvent = jest.requireMock('@/utils/analytics')()
.trackEvent as jest.Mock;
// Mock the passport data provider to avoid database operations
const mockGetAllDocumentsDirectlyFromKeychain = jest.fn();
const mockLoadSelectedDocumentDirectlyFromKeychain = jest.fn();

View File

@@ -3,7 +3,6 @@
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import { ethers } from 'ethers';
import { Platform } from 'react-native';
import { CloudStorage } from 'react-native-cloud-storage';
// Import after mocks
import { GDrive } from '@robinbobin/react-native-google-drive-api-wrapper';
@@ -12,6 +11,24 @@ import { renderHook } from '@testing-library/react-native';
import { useBackupMnemonic } from '@/utils/cloudBackup';
import { createGDrive } from '@/utils/cloudBackup/google';
type SupportedPlatforms = 'ios' | 'android';
jest.mock('react-native', () => {
const mockPlatform: { OS: SupportedPlatforms; select: jest.Mock } = {
OS: 'ios',
select: jest.fn(() => 'ios'),
};
return {
Platform: mockPlatform,
};
});
const mockPlatform = jest.requireMock('react-native').Platform as {
OS: SupportedPlatforms;
select: jest.Mock;
};
// Mock dependencies
jest.mock('react-native-cloud-storage', () => ({
CloudStorage: {
@@ -74,12 +91,12 @@ const mockMnemonic = {
};
describe('cloudBackup', () => {
let originalPlatform: any;
let originalPlatform: SupportedPlatforms;
let consoleSpy: jest.SpyInstance;
beforeEach(() => {
jest.clearAllMocks();
originalPlatform = Platform.OS;
originalPlatform = mockPlatform.OS;
// Suppress console.error during tests to avoid cluttering output
consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
(GDrive as jest.Mock).mockImplementation(() => mockGDriveInstance);
@@ -87,7 +104,7 @@ describe('cloudBackup', () => {
});
afterEach(() => {
Platform.OS = originalPlatform;
mockPlatform.OS = originalPlatform;
consoleSpy.mockRestore();
});
@@ -106,7 +123,7 @@ describe('cloudBackup', () => {
describe('upload function - iOS', () => {
beforeEach(() => {
Platform.OS = 'ios';
mockPlatform.OS = 'ios';
});
it('should upload mnemonic to iCloud successfully', async () => {
@@ -180,7 +197,7 @@ describe('cloudBackup', () => {
describe('upload function - Android', () => {
beforeEach(() => {
Platform.OS = 'android';
mockPlatform.OS = 'android';
});
it('should upload mnemonic to Google Drive successfully', async () => {
@@ -217,7 +234,7 @@ describe('cloudBackup', () => {
describe('download function - iOS', () => {
beforeEach(() => {
Platform.OS = 'ios';
mockPlatform.OS = 'ios';
});
it('should download and parse mnemonic from iCloud successfully', async () => {
@@ -295,7 +312,7 @@ describe('cloudBackup', () => {
describe('download function - Android', () => {
beforeEach(() => {
Platform.OS = 'android';
mockPlatform.OS = 'android';
});
it('should download and parse mnemonic from Google Drive successfully', async () => {
@@ -397,7 +414,7 @@ describe('cloudBackup', () => {
describe('disableBackup function - iOS', () => {
beforeEach(() => {
Platform.OS = 'ios';
mockPlatform.OS = 'ios';
});
it('should remove backup folder from iCloud', async () => {
@@ -414,7 +431,7 @@ describe('cloudBackup', () => {
describe('disableBackup function - Android', () => {
beforeEach(() => {
Platform.OS = 'android';
mockPlatform.OS = 'android';
});
it('should delete backup files from Google Drive', async () => {

View File

@@ -2,10 +2,35 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import { Linking } from 'react-native';
import type { SelfClient } from '@selfxyz/mobile-sdk-alpha';
import {
handleUrl,
parseAndValidateUrlParams,
setupUniversalLinkListenerInNavigation,
} from '@/utils/deeplinks';
jest.mock('react-native', () => {
const mockLinking = {
addEventListener: jest.fn(),
getInitialURL: jest.fn(),
};
return {
Linking: mockLinking,
Platform: { OS: 'ios' },
};
});
const mockLinking = jest.requireMock('react-native').Linking as jest.Mocked<{
addEventListener: jest.Mock;
getInitialURL: jest.Mock;
}>;
const mockPlatform = jest.requireMock('react-native').Platform as {
OS: string;
};
jest.mock('@/navigation', () => ({
navigationRef: {
navigate: jest.fn(),
@@ -14,35 +39,33 @@ jest.mock('@/navigation', () => ({
},
}));
const mockUserStore = { default: { getState: jest.fn() } };
jest.mock('@/stores/userStore', () => ({
__esModule: true,
...mockUserStore,
}));
jest.mock('@/stores/userStore', () => {
const mockUserStore = { default: { getState: jest.fn() } };
return {
__esModule: true,
...mockUserStore,
};
});
const mockUserStore = jest.requireMock('@/stores/userStore') as {
default: { getState: jest.Mock };
};
let setDeepLinkUserDetails: jest.Mock;
let handleUrl: (selfClient: SelfClient, url: string) => void;
let parseAndValidateUrlParams: (uri: string) => any;
let setupUniversalLinkListenerInNavigation: () => () => void;
describe('deeplinks', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.resetModules();
({
handleUrl,
parseAndValidateUrlParams,
setupUniversalLinkListenerInNavigation,
} = require('@/utils/deeplinks'));
setDeepLinkUserDetails = jest.fn();
jest.spyOn(Linking, 'getInitialURL').mockResolvedValue(null as any);
jest
.spyOn(Linking, 'addEventListener')
.mockReturnValue({ remove: jest.fn() } as any);
mockLinking.getInitialURL.mockReset();
mockLinking.addEventListener.mockReset();
mockLinking.getInitialURL.mockResolvedValue(null as any);
mockLinking.addEventListener.mockReturnValue({ remove: jest.fn() } as any);
mockUserStore.default.getState.mockReturnValue({
setDeepLinkUserDetails,
});
mockPlatform.OS = 'ios';
});
describe('handleUrl', () => {
@@ -566,11 +589,11 @@ describe('deeplinks', () => {
it('setup listener registers and cleans up', () => {
const remove = jest.fn();
(Linking.getInitialURL as jest.Mock).mockResolvedValue(undefined);
(Linking.addEventListener as jest.Mock).mockReturnValue({ remove });
mockLinking.getInitialURL.mockResolvedValue(undefined as any);
mockLinking.addEventListener.mockReturnValue({ remove });
const cleanup = setupUniversalLinkListenerInNavigation();
expect(Linking.addEventListener).toHaveBeenCalled();
expect(mockLinking.addEventListener).toHaveBeenCalled();
cleanup();
expect(remove).toHaveBeenCalled();
});

View File

@@ -2,22 +2,15 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
// Mock Platform without requiring react-native to avoid memory issues
// Use a simple object that can be modified directly
import { Buffer } from 'buffer';
import { parseScanResponse, scan } from '@/utils/nfcScanner';
import { PassportReader } from '@/utils/passportReader';
// Mock Platform without requiring react-native to avoid memory issues
// Use a closure to store the OS value, preventing test pollution
let platformOS = 'ios'; // Default to iOS
const Platform = {
get OS() {
return platformOS;
},
set OS(value: string) {
platformOS = value;
},
OS: 'ios', // Default to iOS
Version: 14,
};
@@ -25,6 +18,9 @@ jest.mock('react-native', () => ({
Platform,
}));
// Ensure the Node Buffer implementation is available to the module under test
global.Buffer = Buffer;
describe('parseScanResponse', () => {
beforeEach(() => {
jest.clearAllMocks();

8
gitleaks-override.toml Normal file
View File

@@ -0,0 +1,8 @@
[extend]
path = ".gitleaks.toml"
[allowlist]
description = "Project-specific overrides"
paths = [
'''(?:^|/)Podfile\.lock$'''
]

View File

@@ -21,7 +21,7 @@
"format": "SKIP_BUILD_DEPS=1 yarn format:root && yarn format:github && SKIP_BUILD_DEPS=1 yarn workspaces foreach --parallel -i --all --exclude self-workspace-root run format",
"format:github": "yarn prettier --parser yaml --write .github/**/*.yml --single-quote false",
"format:root": "echo 'format markdown' && yarn prettier --parser markdown --write *.md && echo 'format yaml' && yarn prettier --parser yaml --write .*.{yml,yaml} --single-quote false && yarn prettier --write scripts/**/*.{js,mjs,ts} && yarn prettier --parser json --write scripts/**/*.json",
"gitleaks": "gitleaks protect --staged --redact --config=.gitleaks.toml",
"gitleaks": "gitleaks protect --staged --redact --config=gitleaks-override.toml",
"postinstall": "node scripts/run-patch-package.cjs",
"lint": "yarn lint:headers && yarn workspaces foreach --parallel -i --all --exclude self-workspace-root run lint",
"lint:headers": "node scripts/check-duplicate-headers.cjs . && node scripts/check-license-headers.mjs . --check",

3259
yarn.lock

File diff suppressed because it is too large Load Diff