dev fixes for release 2.9.1 (#1428)

* update lock

* fix types

* bump version

* fix nested react requires

* fix heavy tests

* address fake mocks

* fix test

* remove last borked react test
This commit is contained in:
Justin Hernandez
2025-11-18 21:01:02 -03:00
committed by GitHub
parent 195fda1365
commit c50db06eee
24 changed files with 362 additions and 121 deletions

View File

@@ -118,26 +118,83 @@ vi.mock('react-native', () => ({
## Best Practices
1. **Always use ES6 `import` statements** - Never use `require('react-native')` in test files
1. **Always use ES6 `import` statements** - Never use `require('react')` or `require('react-native')` in test files
2. **Put all imports at the top of the file** - No dynamic imports in hooks
3. **Avoid `jest.resetModules()`** - Only use when absolutely necessary for module initialization tests
4. **Use setup file mocks** - React Native is already mocked in `jest.setup.js` (Jest) or `tests/setup.ts` (Vitest)
## Automated Enforcement
The project has multiple layers of protection against nested require() patterns:
### 1. ESLint Rule (app/.eslintrc.cjs)
ESLint will fail on `require('react')` and `require('react-native')` in test files:
```javascript
'no-restricted-syntax': [
'error',
{
selector: "CallExpression[callee.name='require'][arguments.0.value='react']",
message: "Do not use require('react') in tests..."
},
{
selector: "CallExpression[callee.name='require'][arguments.0.value='react-native']",
message: "Do not use require('react-native') in tests..."
}
]
```
Run `yarn lint` to check for violations.
### 2. Validation Script (app/scripts/check-test-requires.cjs)
Automated script to detect nested require patterns:
```bash
node scripts/check-test-requires.cjs
```
This script:
- Scans all test files for `require('react')` and `require('react-native')`
- Reports exact file locations and line numbers
- Exits with error code 1 if issues found
### 3. CI Fast-Fail Check
GitHub Actions runs the validation script before tests:
```yaml
- name: Check for nested require() in tests
run: node scripts/check-test-requires.cjs
working-directory: ./app
```
This prevents wasting CI time on tests that will OOM.
## Quick Checklist
Before committing test changes:
- [ ] No `require('react-native')` calls in test files (use `import` instead)
- [ ] All imports at top of file (not in hooks)
- [ ] Search for nested patterns: `grep -r "require('react-native')" app/tests/`
- [ ] No `require('react')` calls in test files (use `import React from 'react'` instead)
- [ ] No `require('react-native')` calls in test files (use `import { ... } from 'react-native'` instead)
- [ ] All imports at top of file (not in hooks or jest.mock() factories)
- [ ] Run validation: `node scripts/check-test-requires.cjs`
- [ ] Run lint: `yarn lint`
## Detection
**Signs of nested require issues**: CI OOM errors, test timeouts, memory spikes, "Call stack size exceeded" errors
**Signs of nested require issues**: CI OOM errors, test timeouts, memory spikes, "Call stack size exceeded" errors, tests hiding actual failures
**Fix**: Search for `require('react-native')` in tests → Replace with `import` statements
**Fix**:
1. Search for `require('react')` and `require('react-native')` in tests
2. Replace with `import` statements at the top of the file
3. Run `node scripts/check-test-requires.cjs` to verify
## Related Files
- `app/.eslintrc.cjs` - ESLint rules blocking nested requires
- `app/scripts/check-test-requires.cjs` - Validation script
- `.github/workflows/mobile-ci.yml` - CI enforcement
- `app/jest.setup.js` - Jest setup with React Native mocks
- `packages/mobile-sdk-alpha/tests/setup.ts` - Vitest setup with React Native mocks
- `app/jest.config.cjs` - Jest configuration

View File

@@ -183,6 +183,9 @@ jobs:
else
echo "✅ All required dependency files exist"
fi
- name: Check for nested require() in tests
run: node scripts/check-test-requires.cjs
working-directory: ./app
- name: App Tests
env:
# Increase Node.js memory to prevent hermes-parser WASM memory errors

View File

@@ -213,12 +213,54 @@ module.exports = {
rules: {
// Allow console logging and relaxed typing in tests
'no-console': 'off',
// Allow require() imports in tests for mocking
// Allow require() imports in tests for mocking, but block react/react-native
'@typescript-eslint/no-require-imports': 'off',
// Block require('react') and require('react-native') to prevent OOM issues
'no-restricted-syntax': [
'error',
{
selector:
"CallExpression[callee.name='require'][arguments.0.value='react']",
message:
"Do not use require('react') in tests. Use 'import React from \"react\"' at the top of the file to avoid out-of-memory issues in CI.",
},
{
selector:
"CallExpression[callee.name='require'][arguments.0.value='react-native']",
message:
"Do not use require('react-native') in tests. Use 'import { ... } from \"react-native\"' at the top of the file to avoid out-of-memory issues in CI.",
},
],
// Allow any types in tests for mocking
'@typescript-eslint/no-explicit-any': 'off',
},
},
{
files: ['tests/**/*.js'],
env: {
jest: true,
},
rules: {
// Allow console logging in test JS files
'no-console': 'off',
// Block require('react') and require('react-native') to prevent OOM issues
'no-restricted-syntax': [
'error',
{
selector:
"CallExpression[callee.name='require'][arguments.0.value='react']",
message:
"Do not use require('react') in tests. Use 'import React from \"react\"' at the top of the file to avoid out-of-memory issues in CI.",
},
{
selector:
"CallExpression[callee.name='require'][arguments.0.value='react-native']",
message:
"Do not use require('react-native') in tests. Use 'import { ... } from \"react-native\"' at the top of the file to avoid out-of-memory issues in CI.",
},
],
},
},
{
// Allow console logging in scripts
files: ['scripts/**/*.cjs', 'scripts/*.cjs'],

View File

@@ -209,22 +209,32 @@ yarn mobile-local-deploy
## Test Memory Optimization
**CRITICAL**: Never create nested `require('react-native')` calls in tests. This causes out-of-memory (OOM) errors in CI/CD pipelines.
**CRITICAL**: Never create nested `require('react')` or `require('react-native')` calls in tests. This causes out-of-memory (OOM) errors in CI/CD pipelines that hide actual test failures.
### Automated Enforcement
The project has multiple layers of protection:
1. **ESLint Rule**: Blocks `require('react')` and `require('react-native')` in test files
2. **Pre-commit Script**: Run `node scripts/check-test-requires.cjs` to validate
3. **CI Fast-Fail**: GitHub Actions checks for nested requires before running tests
### Quick Check
Before committing, verify no nested requires:
```bash
# Check for require('react-native') in test files
grep -r "require('react-native')" app/tests/
# Automated check (recommended)
node scripts/check-test-requires.cjs
# Review results - ensure no nested patterns (require inside beforeEach/afterEach or inside modules that are required in tests)
# Manual check
grep -r "require('react')" app/tests/
grep -r "require('react-native')" app/tests/
```
### Best Practices
- Use ES6 `import` statements instead of `require()` when possible
- Avoid dynamic `require()` calls in `beforeEach`/`afterEach` hooks
- Prefer top-level imports over nested requires
- React Native is already mocked in `jest.setup.js` - use imports in test files
- **Always use ES6 `import` statements** - Never use `require('react')` or `require('react-native')` in test files
- Put all imports at the top of the file - No dynamic imports in hooks
- Avoid `require()` calls in `beforeEach`/`afterEach` hooks
- React and React Native are already mocked in `jest.setup.js` - use imports in test files
### Detailed Guidelines
See `.cursor/rules/test-memory-optimization.mdc` for comprehensive guidelines, examples, and anti-patterns.

View File

@@ -1,10 +1,7 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.7)
base64
nkf
rexml
CFPropertyList (3.0.8)
activesupport (7.2.3)
base64
benchmark (>= 0.3)
@@ -25,7 +22,7 @@ GEM
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.4.0)
aws-partitions (1.1181.0)
aws-partitions (1.1183.0)
aws-sdk-core (3.237.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
@@ -232,7 +229,7 @@ GEM
mini_magick (4.13.2)
mini_mime (1.1.5)
mini_portile2 (2.8.9)
minitest (5.26.1)
minitest (5.26.2)
molinillo (0.8.0)
multi_json (1.17.0)
multipart-post (2.4.1)
@@ -241,7 +238,6 @@ GEM
nap (1.1.0)
naturally (2.3.0)
netrc (0.11.0)
nkf (0.2.0)
nokogiri (1.18.10)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
@@ -315,4 +311,4 @@ RUBY VERSION
ruby 3.2.7p253
BUNDLED WITH
2.6.9
2.4.19

View File

@@ -135,7 +135,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 121
versionName "2.9.0"
versionName "2.9.1"
manifestPlaceholders = [appAuthRedirectScheme: 'com.proofofpassportapp']
externalNativeBuild {
cmake {

View File

@@ -21,7 +21,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2.9.0</string>
<string>2.9.1</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>

View File

@@ -546,7 +546,7 @@
"$(PROJECT_DIR)",
"$(PROJECT_DIR)/MoproKit/Libs",
);
MARKETING_VERSION = 2.9.0;
MARKETING_VERSION = 2.9.1;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -686,7 +686,7 @@
"$(PROJECT_DIR)",
"$(PROJECT_DIR)/MoproKit/Libs",
);
MARKETING_VERSION = 2.9.0;
MARKETING_VERSION = 2.9.1;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",

View File

@@ -1,6 +1,6 @@
{
"name": "@selfxyz/mobile-app",
"version": "2.9.0",
"version": "2.9.1",
"private": true,
"type": "module",
"scripts": {

View File

@@ -0,0 +1,145 @@
// 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.
/**
* Check for nested require('react') and require('react-native') in test files
* These patterns cause out-of-memory errors in CI/CD pipelines
*
* Usage: node scripts/check-test-requires.cjs
* Exit code: 0 if no issues found, 1 if issues found
*/
const fs = require('fs');
const path = require('path');
const TESTS_DIR = path.join(__dirname, '..', 'tests');
const FORBIDDEN_PATTERNS = [
{
pattern: /require\(['"]react['"]\)/g,
name: "require('react')",
fix: 'Use \'import React from "react"\' at the top of the file instead',
},
{
pattern: /require\(['"]react-native['"]\)/g,
name: "require('react-native')",
fix: 'Use \'import { ... } from "react-native"\' at the top of the file instead',
},
];
/**
* Recursively find all test files in directory
*/
function findTestFiles(dir, files = []) {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
// Skip node_modules and other common directories
if (
!['node_modules', '.git', 'coverage', 'dist', 'build'].includes(
entry.name,
)
) {
findTestFiles(fullPath, files);
}
} else if (
entry.isFile() &&
(entry.name.endsWith('.ts') ||
entry.name.endsWith('.tsx') ||
entry.name.endsWith('.js') ||
entry.name.endsWith('.jsx'))
) {
files.push(fullPath);
}
}
return files;
}
/**
* Check a file for forbidden require patterns
*/
function checkFile(filePath) {
const content = fs.readFileSync(filePath, 'utf-8');
const issues = [];
for (const { pattern, name, fix } of FORBIDDEN_PATTERNS) {
const matches = content.matchAll(pattern);
for (const match of matches) {
const lines = content.substring(0, match.index).split('\n');
const lineNumber = lines.length;
const columnNumber = lines[lines.length - 1].length + 1;
issues.push({
file: path.relative(process.cwd(), filePath),
line: lineNumber,
column: columnNumber,
pattern: name,
fix: fix,
});
}
}
return issues;
}
/**
* Main execution
*/
function main() {
console.log('🔍 Checking for nested require() in test files...\n');
if (!fs.existsSync(TESTS_DIR)) {
console.error(`❌ Tests directory not found: ${TESTS_DIR}`);
process.exit(1);
}
const testFiles = findTestFiles(TESTS_DIR);
console.log(`Found ${testFiles.length} test files to check\n`);
let totalIssues = 0;
const issuesByFile = new Map();
for (const file of testFiles) {
const issues = checkFile(file);
if (issues.length > 0) {
issuesByFile.set(file, issues);
totalIssues += issues.length;
}
}
if (totalIssues === 0) {
console.log('✅ No nested require() patterns found in test files!');
process.exit(0);
}
// Report issues
console.error(
`❌ Found ${totalIssues} nested require() pattern(s) that cause OOM in CI:\n`,
);
for (const [file, issues] of issuesByFile.entries()) {
console.error(`\n${path.relative(process.cwd(), file)}:`);
for (const issue of issues) {
console.error(` Line ${issue.line}:${issue.column} - ${issue.pattern}`);
console.error(` Fix: ${issue.fix}`);
}
}
console.error(
'\n⚠ These patterns cause out-of-memory errors in CI/CD pipelines.',
);
console.error(
' Use ES6 imports at the top of files instead of require() calls.',
);
console.error(
' See .cursor/rules/test-memory-optimization.mdc for details.\n',
);
process.exit(1);
}
main();

View File

@@ -110,7 +110,7 @@ const CloudBackupScreen: React.FC<CloudBackupScreenProps> = ({
onButtonPress: () => {
// setTimeout to ensure modal closes before navigation to prevent navigation conflicts when the modal tries to goBack()
setTimeout(() => {
navigation.navigate({ name: 'CountryPicker', params: {} });
navigation.navigate('CountryPicker');
}, 100);
},
onModalDismiss: () => {},

View File

@@ -3,7 +3,7 @@
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
// Minimal JS mock for @selfxyz/mobile-sdk-alpha/components used in tests
const React = require('react');
// CRITICAL: Do NOT import React to avoid OOM issues in CI
const getTextFromChildren = ch => {
if (typeof ch === 'string') return ch;
@@ -13,41 +13,50 @@ const getTextFromChildren = ch => {
return '';
};
const Caption = ({ children }) =>
React.createElement(React.Fragment, null, children);
const Description = ({ children }) =>
React.createElement(React.Fragment, null, children);
const Title = ({ children }) =>
React.createElement(React.Fragment, null, children);
// Simple mock components that return plain objects instead of using React.createElement
export const Caption = ({ children }) => ({
type: 'Caption',
props: { children },
});
// Use React.createElement directly instead of requiring react-native to avoid memory issues
const PrimaryButton = ({ children, onPress, disabled, testID }) => {
export const Description = ({ children }) => ({
type: 'Description',
props: { children },
});
export const PrimaryButton = ({ children, onPress, disabled, testID }) => {
const buttonText = getTextFromChildren(children);
const id =
testID || `button-${buttonText.toLowerCase().replace(/\s+/g, '-')}`;
return React.createElement(
'View',
{ onPress, disabled, testID: id, accessibilityRole: 'button' },
children,
);
return {
type: 'PrimaryButton',
props: {
children,
onPress,
disabled,
testID: id,
accessibilityRole: 'button',
},
};
};
const SecondaryButton = ({ children, onPress, disabled, testID }) => {
export const SecondaryButton = ({ children, onPress, disabled, testID }) => {
const buttonText = getTextFromChildren(children);
const id =
testID || `button-${buttonText.toLowerCase().replace(/\s+/g, '-')}`;
return React.createElement(
'View',
{ onPress, disabled, testID: id, accessibilityRole: 'button' },
children,
);
return {
type: 'SecondaryButton',
props: {
children,
onPress,
disabled,
testID: id,
accessibilityRole: 'button',
},
};
};
module.exports = {
__esModule: true,
Caption,
Description,
Title,
PrimaryButton,
SecondaryButton,
};
export const Title = ({ children }) => ({
type: 'Title',
props: { children },
});

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 React from 'react';
import { Text } from 'react-native';
import { render } from '@testing-library/react-native';

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 React from 'react';
import { act, render } from '@testing-library/react-native';
import { PassportCamera as NativePassportCamera } from '@/components/native/PassportCamera';

View File

@@ -39,13 +39,7 @@ jest.mock('@/utils/points', () => ({
},
}));
jest.mock('@/stores/userStore', () => {
const actual = jest.requireActual('@/stores/userStore');
return {
__esModule: true,
default: actual.default,
};
});
// userStore is used as-is, no mock needed
const mockNavigate = jest.fn();
const mockUseNavigation = useNavigation as jest.MockedFunction<

View File

@@ -13,13 +13,7 @@ jest.mock('@react-navigation/native', () => ({
useNavigation: jest.fn(),
}));
jest.mock('@/stores/userStore', () => {
const actual = jest.requireActual('@/stores/userStore');
return {
__esModule: true,
default: actual.default,
};
});
// userStore is used as-is, no mock needed
const mockNavigate = jest.fn();
const mockUseNavigation = useNavigation as jest.MockedFunction<

View File

@@ -6,7 +6,7 @@
* @jest-environment node
*/
import React, { useEffect } from 'react';
import { useEffect } from 'react';
import { Text } from 'react-native';
import { render, screen } from '@testing-library/react-native';

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 React, { useEffect, useState } from 'react';
import { useEffect, useState } from 'react';
import { Text } from 'react-native';
import { render, waitFor } from '@testing-library/react-native';

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 React from 'react';
import { Text } from 'react-native';
import { render, waitFor } from '@testing-library/react-native';

View File

@@ -3,7 +3,7 @@
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
// Mock ConfirmIdentificationScreen to avoid PixelRatio issues
import React, { type ReactNode } from 'react';
import type { ReactNode } from 'react';
import { renderHook } from '@testing-library/react-native';
import { useSelfClient } from '@selfxyz/mobile-sdk-alpha';

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 React from 'react';
import { useNavigation, useRoute } from '@react-navigation/native';
import { render, waitFor } from '@testing-library/react-native';
@@ -15,24 +14,31 @@ jest.mock('@react-navigation/native', () => ({
// Mock Tamagui components to avoid theme provider requirement
jest.mock('tamagui', () => {
const ReactMock = require('react');
// Use React.createElement directly instead of requiring react-native to avoid memory issues
const YStack = ReactMock.forwardRef(({ children, ...props }: any, ref: any) =>
ReactMock.createElement('View', { ref, ...props }, children),
const View: any = 'View';
const Text: any = 'Text';
const createViewComponent = (displayName: string) => {
const MockComponent = ({ children, ...props }: any) => (
<View {...props} testID={displayName}>
{children}
</View>
);
MockComponent.displayName = displayName;
return MockComponent;
};
const MockYStack = createViewComponent('YStack');
const MockView = createViewComponent('View');
const MockText = ({ children, ...props }: any) => (
<Text {...props}>{children}</Text>
);
YStack.displayName = 'YStack';
const Text = ReactMock.forwardRef(({ children, ...props }: any, ref: any) =>
ReactMock.createElement('Text', { ref, ...props }, children),
);
Text.displayName = 'Text';
const View = ReactMock.forwardRef(({ children, ...props }: any, ref: any) =>
ReactMock.createElement('View', { ref, ...props }, children),
);
View.displayName = 'View';
MockText.displayName = 'Text';
return {
YStack,
Text,
View,
__esModule: true,
YStack: MockYStack,
View: MockView,
Text: MockText,
};
});

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 React from 'react';
import { Linking } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import {
@@ -15,17 +14,15 @@ import {
import { WebViewScreen } from '@/screens/shared/WebViewScreen';
jest.mock('@react-navigation/native', () => ({
...jest.requireActual('@react-navigation/native'),
useNavigation: jest.fn(),
useFocusEffect: jest.fn(),
}));
jest.mock('react-native-webview', () => {
const ReactMock = require('react');
// Use React.createElement directly instead of requiring react-native to avoid memory issues
const MockWebView = ReactMock.forwardRef((props: any, _ref) => {
return ReactMock.createElement('View', { testID: 'webview', ...props });
});
// Lightweight host component so React can render while keeping props inspectable
const MockWebView = ({ testID = 'webview', ...props }: any) => (
<mock-webview testID={testID} {...props} />
);
MockWebView.displayName = 'MockWebView';
return {
__esModule: true,

View File

@@ -23,20 +23,15 @@ jest.mock('@/providers/authProvider', () => ({
jest.mock('@/utils/points/utils', () => ({
getPointsAddress: jest.fn(),
}));
jest.mock('ethers', () => {
const actualEthers = jest.requireActual('ethers');
return {
...actualEthers,
ethers: {
...actualEthers.ethers,
Wallet: jest.fn(),
Signature: {
from: jest.fn(),
},
getBytes: jest.fn(),
jest.mock('ethers', () => ({
ethers: {
Wallet: jest.fn(),
Signature: {
from: jest.fn(),
},
};
});
getBytes: jest.fn(),
},
}));
const mockAxios = axios as jest.Mocked<typeof axios>;
const mockUnsafeGetPrivateKey = unsafe_getPrivateKey as jest.MockedFunction<

View File

@@ -37,13 +37,9 @@ jest.mock('@robinbobin/react-native-google-drive-api-wrapper', () => ({
},
}));
jest.mock('@/utils/cloudBackup/google', () => {
const originalModule = jest.requireActual('@/utils/cloudBackup/google');
return {
...originalModule,
createGDrive: jest.fn(),
};
});
jest.mock('@/utils/cloudBackup/google', () => ({
createGDrive: jest.fn(),
}));
jest.mock('ethers', () => ({
ethers: {