Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: CI

on:

pull_request:
branches: [ main ]

jobs:
lint-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'

- name: Install dependencies
run: npm install --legacy-peer-deps

- name: Lint
run: npm run lint

- name: Test
run: npm test

eas-build-pre-check:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'

- name: Install dependencies
run: npm install --legacy-peer-deps

- name: Setup EAS
uses: expo/expo-github-action@v8
with:
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}

- name: Validate EAS Config
run: npx eas-cli build:list --limit=1 || true # Basic check if EAS is accessible
70 changes: 70 additions & 0 deletions __tests__/LandingScreen-test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React from 'react';
import { render } from '@testing-library/react-native';
import LandingScreen from '../app/landing';

// Mock expo-router
jest.mock('expo-router', () => ({
useRouter: () => ({
push: jest.fn(),
replace: jest.fn(),
back: jest.fn(),
}),
}));

// Mock expo-constants
jest.mock('expo-constants', () => ({
expoConfig: {
extra: {
provider: {
name: 'Rocca',
primaryColor: '#3B82F6',
secondaryColor: '#E1EFFF',
accentColor: '#10B981',
welcomeMessage: 'Your identity, rewarded.',
showRewards: true,
showFeeDelegation: true,
showIdentityManagement: true,
},
},
},
}));

// Mock useProvider hook
jest.mock('@/hooks/useProvider', () => ({
useProvider: () => ({
key: null,
identity: null,
account: null,
identities: [{ did: 'did:key:z6Mkh...' }],
accounts: [{ address: 'ADDR123...', balance: 100 }],
}),
}));

// Mock MaterialIcons
jest.mock('@expo/vector-icons', () => ({
MaterialIcons: 'MaterialIcons',
}));

describe('<LandingScreen />', () => {
it('renders correctly with mocked provider data', () => {
const { getByText } = render(<LandingScreen />);

// Check for welcome message
expect(getByText('Your identity, rewarded.')).toBeTruthy();

// Check for balance (mocked as 100)
expect(getByText('$100')).toBeTruthy();

// Check for identity DID (partial check because it might be truncated in UI)
// In landing.tsx: {activeIdentity?.did || 'No identity found'}
expect(getByText('did:key:z6Mkh...')).toBeTruthy();
});

it('renders provider services when enabled', () => {
const { getByText } = render(<LandingScreen />);

expect(getByText('Rewards')).toBeTruthy();
expect(getByText('Free Fees')).toBeTruthy();
expect(getByText('Security')).toBeTruthy();
});
});
16 changes: 16 additions & 0 deletions __tests__/Logo-test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react';
import { render } from '@testing-library/react-native';
import Logo from '../components/Logo';

describe('<Logo />', () => {
it('renders correctly with default props', () => {
const { getByText } = render(<Logo />);
// Default name is 'Rocca', so it should show 'R'
expect(getByText('R')).toBeTruthy();
});

it('renders correctly with custom size', () => {
const { getByText } = render(<Logo size={100} />);
expect(getByText('R')).toBeTruthy();
});
});
92 changes: 92 additions & 0 deletions __tests__/OnboardingScreen-test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import React from 'react';
import { render, fireEvent, waitFor } from '@testing-library/react-native';
import OnboardingScreen from '../app/onboarding';

// Mock expo-router
jest.mock('expo-router', () => ({
useRouter: () => ({
push: jest.fn(),
replace: jest.fn(),
back: jest.fn(),
}),
}));

// Mock expo-constants
jest.mock('expo-constants', () => ({
expoConfig: {
extra: {
provider: {
name: 'Rocca',
primaryColor: '#3B82F6',
secondaryColor: '#E1EFFF',
},
},
},
}));

// Mock useProvider hook
jest.mock('@/hooks/useProvider', () => ({
useProvider: () => ({
keys: [],
key: null,
identity: null,
account: null,
identities: [],
accounts: [],
provider: {
keystore: {
generateKey: jest.fn().mockResolvedValue({ id: 'key1' }),
}
}
}),
}));

// Mock bip39
jest.mock('@scure/bip39', () => ({
generateMnemonic: jest.fn().mockReturnValue('apple banana cherry date elderberry fig grape honeydew iceberg jackfruit kiwi lemon'),
mnemonicToSeed: jest.fn().mockResolvedValue(new Uint8Array(64)),
wordlist: { english: [] },
}));

// Mock Reanimated
jest.mock('react-native-reanimated', () => {
const Reanimated = require('react-native-reanimated/mock');
Reanimated.default.call = () => {};
return Reanimated;
});

// Mock MaterialIcons
jest.mock('@expo/vector-icons', () => ({
MaterialIcons: 'MaterialIcons',
}));

describe('<OnboardingScreen />', () => {
it('renders welcome step initially', () => {
const { getByText } = render(<OnboardingScreen />);

expect(getByText('Welcome to Rocca')).toBeTruthy();
expect(getByText('Create Wallet')).toBeTruthy();
});

it('transitions to generate step when clicking Create Wallet', async () => {
const { getByText, findByText } = render(<OnboardingScreen />);

fireEvent.press(getByText('Create Wallet'));

expect(await findByText('Secure Your Identity.')).toBeTruthy();
expect(await findByText('View Secret')).toBeTruthy();
});

it('shows the recovery phrase after generation', async () => {
const { getByText, findByText } = render(<OnboardingScreen />);

fireEvent.press(getByText('Create Wallet'));

// Wait for the transition and then press "View Secret"
const revealButton = await findByText('View Secret');
fireEvent.press(revealButton);

// Now it should show "Verify Recovery Phrase"
expect(await findByText('Verify Recovery Phrase')).toBeTruthy();
});
});
78 changes: 78 additions & 0 deletions __tests__/SeedPhrase-test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React from 'react';
import { render, fireEvent } from '@testing-library/react-native';
import SeedPhrase from '../components/SeedPhrase';

// Mocking Reanimated because it often has issues in Jest environments
jest.mock('react-native-reanimated', () => {
const Reanimated = require('react-native-reanimated/mock');
Reanimated.default.call = () => {};
return Reanimated;
});

describe('<SeedPhrase />', () => {
const mockPhrase = ['apple', 'banana', 'cherry', 'date', 'elderberry', 'fig', 'grape', 'honeydew', 'iceberg', 'jackfruit', 'kiwi', 'lemon'];
const primaryColor = '#3B82F6';

it('renders all words when showSeed is true', () => {
const { getByText } = render(
<SeedPhrase
recoveryPhrase={mockPhrase}
showSeed={true}
primaryColor={primaryColor}
/>
);

mockPhrase.forEach((word) => {
expect(getByText(word)).toBeTruthy();
});
});

it('hides words when showSeed is false and no validation is active', () => {
const { queryByText } = render(
<SeedPhrase
recoveryPhrase={mockPhrase}
showSeed={false}
primaryColor={primaryColor}
/>
);

// It should NOT show the words
mockPhrase.forEach((word) => {
expect(queryByText(word)).toBeNull();
});
});

it('renders input fields for words specified in validateWords', () => {
const validateWords = { 0: '', 5: '' };
const { getByPlaceholderText } = render(
<SeedPhrase
recoveryPhrase={mockPhrase}
showSeed={false}
validateWords={validateWords}
primaryColor={primaryColor}
/>
);

expect(getByPlaceholderText('Word #1')).toBeTruthy();
expect(getByPlaceholderText('Word #6')).toBeTruthy();
});

it('calls onInputChange when typing in validation fields', () => {
const validateWords = { 0: '' };
const onInputChange = jest.fn();
const { getByPlaceholderText } = render(
<SeedPhrase
recoveryPhrase={mockPhrase}
showSeed={false}
validateWords={validateWords}
onInputChange={onInputChange}
primaryColor={primaryColor}
/>
);

const input = getByPlaceholderText('Word #1');
fireEvent.changeText(input, 'test');

expect(onInputChange).toHaveBeenCalledWith(0, 'test');
});
});
10 changes: 10 additions & 0 deletions app.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,17 @@
"showRewards": true,
"showFeeDelegation": true,
"showIdentityManagement": true
},
"router": {},
"eas": {
"projectId": "f1e6cb1b-642d-49fa-b276-53b4403f62d6"
}
},
"runtimeVersion": {
"policy": "appVersion"
},
"updates": {
"url": "https://u.expo.dev/f1e6cb1b-642d-49fa-b276-53b4403f62d6"
}
}
}
4 changes: 2 additions & 2 deletions app/onboarding.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useReducer, useRef, useState } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, SafeAreaView, ScrollView, Alert, Image, TextInput } from 'react-native';
import React, { useReducer, useRef } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, SafeAreaView, ScrollView, Alert, Image } from 'react-native';
import { useRouter } from 'expo-router';
import Constants from 'expo-constants';
import { MaterialIcons } from '@expo/vector-icons';
Expand Down
19 changes: 19 additions & 0 deletions eas.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"build": {
"development": {
"developmentClient": true,
"distribution": "internal"
},
"testing": {
"distribution": "internal",
"channel": "testing"
},
"production": {
"channel": "production",
"autoIncrement": true
}
},
"submit": {
"production": {}
}
}
10 changes: 10 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module.exports = {
preset: 'jest-expo',
setupFilesAfterEnv: ['@testing-library/jest-native/extend-expect'],
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|expo-router|@scure/.*|react-native-reanimated|react-native-nitro-modules)',
],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/$1',
},
};
7 changes: 7 additions & 0 deletions lefthook.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# lefthook.yml

pre-commit:
parallel: true
jobs:
- run: npm run lint
- run: npm run test
Loading
Loading