Skip to content

Commit 65ea79f

Browse files
authored
Merge pull request #5 from algorandfoundation/ci/add-fixtures
test: adds tests, CI and EAS builds
2 parents 9be8020 + 26e20ad commit 65ea79f

File tree

12 files changed

+9363
-2187
lines changed

12 files changed

+9363
-2187
lines changed

.github/workflows/ci.yml

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
name: CI
2+
3+
on:
4+
5+
pull_request:
6+
branches: [ main ]
7+
8+
jobs:
9+
lint-and-test:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- name: Checkout repository
13+
uses: actions/checkout@v4
14+
15+
- name: Setup Node.js
16+
uses: actions/setup-node@v4
17+
with:
18+
node-version: 20
19+
cache: 'npm'
20+
21+
- name: Install dependencies
22+
run: npm install --legacy-peer-deps
23+
24+
- name: Lint
25+
run: npm run lint
26+
27+
- name: Test
28+
run: npm test
29+
30+
eas-build-pre-check:
31+
runs-on: ubuntu-latest
32+
steps:
33+
- name: Checkout repository
34+
uses: actions/checkout@v4
35+
36+
- name: Setup Node.js
37+
uses: actions/setup-node@v4
38+
with:
39+
node-version: 20
40+
cache: 'npm'
41+
42+
- name: Install dependencies
43+
run: npm install --legacy-peer-deps
44+
45+
- name: Setup EAS
46+
uses: expo/expo-github-action@v8
47+
with:
48+
eas-version: latest
49+
token: ${{ secrets.EXPO_TOKEN }}
50+
51+
- name: Validate EAS Config
52+
run: npx eas-cli build:list --limit=1 || true # Basic check if EAS is accessible

__tests__/LandingScreen-test.tsx

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import React from 'react';
2+
import { render } from '@testing-library/react-native';
3+
import LandingScreen from '../app/landing';
4+
5+
// Mock expo-router
6+
jest.mock('expo-router', () => ({
7+
useRouter: () => ({
8+
push: jest.fn(),
9+
replace: jest.fn(),
10+
back: jest.fn(),
11+
}),
12+
}));
13+
14+
// Mock expo-constants
15+
jest.mock('expo-constants', () => ({
16+
expoConfig: {
17+
extra: {
18+
provider: {
19+
name: 'Rocca',
20+
primaryColor: '#3B82F6',
21+
secondaryColor: '#E1EFFF',
22+
accentColor: '#10B981',
23+
welcomeMessage: 'Your identity, rewarded.',
24+
showRewards: true,
25+
showFeeDelegation: true,
26+
showIdentityManagement: true,
27+
},
28+
},
29+
},
30+
}));
31+
32+
// Mock useProvider hook
33+
jest.mock('@/hooks/useProvider', () => ({
34+
useProvider: () => ({
35+
key: null,
36+
identity: null,
37+
account: null,
38+
identities: [{ did: 'did:key:z6Mkh...' }],
39+
accounts: [{ address: 'ADDR123...', balance: 100 }],
40+
}),
41+
}));
42+
43+
// Mock MaterialIcons
44+
jest.mock('@expo/vector-icons', () => ({
45+
MaterialIcons: 'MaterialIcons',
46+
}));
47+
48+
describe('<LandingScreen />', () => {
49+
it('renders correctly with mocked provider data', () => {
50+
const { getByText } = render(<LandingScreen />);
51+
52+
// Check for welcome message
53+
expect(getByText('Your identity, rewarded.')).toBeTruthy();
54+
55+
// Check for balance (mocked as 100)
56+
expect(getByText('$100')).toBeTruthy();
57+
58+
// Check for identity DID (partial check because it might be truncated in UI)
59+
// In landing.tsx: {activeIdentity?.did || 'No identity found'}
60+
expect(getByText('did:key:z6Mkh...')).toBeTruthy();
61+
});
62+
63+
it('renders provider services when enabled', () => {
64+
const { getByText } = render(<LandingScreen />);
65+
66+
expect(getByText('Rewards')).toBeTruthy();
67+
expect(getByText('Free Fees')).toBeTruthy();
68+
expect(getByText('Security')).toBeTruthy();
69+
});
70+
});

__tests__/Logo-test.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import React from 'react';
2+
import { render } from '@testing-library/react-native';
3+
import Logo from '../components/Logo';
4+
5+
describe('<Logo />', () => {
6+
it('renders correctly with default props', () => {
7+
const { getByText } = render(<Logo />);
8+
// Default name is 'Rocca', so it should show 'R'
9+
expect(getByText('R')).toBeTruthy();
10+
});
11+
12+
it('renders correctly with custom size', () => {
13+
const { getByText } = render(<Logo size={100} />);
14+
expect(getByText('R')).toBeTruthy();
15+
});
16+
});
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import React from 'react';
2+
import { render, fireEvent, waitFor } from '@testing-library/react-native';
3+
import OnboardingScreen from '../app/onboarding';
4+
5+
// Mock expo-router
6+
jest.mock('expo-router', () => ({
7+
useRouter: () => ({
8+
push: jest.fn(),
9+
replace: jest.fn(),
10+
back: jest.fn(),
11+
}),
12+
}));
13+
14+
// Mock expo-constants
15+
jest.mock('expo-constants', () => ({
16+
expoConfig: {
17+
extra: {
18+
provider: {
19+
name: 'Rocca',
20+
primaryColor: '#3B82F6',
21+
secondaryColor: '#E1EFFF',
22+
},
23+
},
24+
},
25+
}));
26+
27+
// Mock useProvider hook
28+
jest.mock('@/hooks/useProvider', () => ({
29+
useProvider: () => ({
30+
keys: [],
31+
key: null,
32+
identity: null,
33+
account: null,
34+
identities: [],
35+
accounts: [],
36+
provider: {
37+
keystore: {
38+
generateKey: jest.fn().mockResolvedValue({ id: 'key1' }),
39+
}
40+
}
41+
}),
42+
}));
43+
44+
// Mock bip39
45+
jest.mock('@scure/bip39', () => ({
46+
generateMnemonic: jest.fn().mockReturnValue('apple banana cherry date elderberry fig grape honeydew iceberg jackfruit kiwi lemon'),
47+
mnemonicToSeed: jest.fn().mockResolvedValue(new Uint8Array(64)),
48+
wordlist: { english: [] },
49+
}));
50+
51+
// Mock Reanimated
52+
jest.mock('react-native-reanimated', () => {
53+
const Reanimated = require('react-native-reanimated/mock');
54+
Reanimated.default.call = () => {};
55+
return Reanimated;
56+
});
57+
58+
// Mock MaterialIcons
59+
jest.mock('@expo/vector-icons', () => ({
60+
MaterialIcons: 'MaterialIcons',
61+
}));
62+
63+
describe('<OnboardingScreen />', () => {
64+
it('renders welcome step initially', () => {
65+
const { getByText } = render(<OnboardingScreen />);
66+
67+
expect(getByText('Welcome to Rocca')).toBeTruthy();
68+
expect(getByText('Create Wallet')).toBeTruthy();
69+
});
70+
71+
it('transitions to generate step when clicking Create Wallet', async () => {
72+
const { getByText, findByText } = render(<OnboardingScreen />);
73+
74+
fireEvent.press(getByText('Create Wallet'));
75+
76+
expect(await findByText('Secure Your Identity.')).toBeTruthy();
77+
expect(await findByText('View Secret')).toBeTruthy();
78+
});
79+
80+
it('shows the recovery phrase after generation', async () => {
81+
const { getByText, findByText } = render(<OnboardingScreen />);
82+
83+
fireEvent.press(getByText('Create Wallet'));
84+
85+
// Wait for the transition and then press "View Secret"
86+
const revealButton = await findByText('View Secret');
87+
fireEvent.press(revealButton);
88+
89+
// Now it should show "Verify Recovery Phrase"
90+
expect(await findByText('Verify Recovery Phrase')).toBeTruthy();
91+
});
92+
});

__tests__/SeedPhrase-test.tsx

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import React from 'react';
2+
import { render, fireEvent } from '@testing-library/react-native';
3+
import SeedPhrase from '../components/SeedPhrase';
4+
5+
// Mocking Reanimated because it often has issues in Jest environments
6+
jest.mock('react-native-reanimated', () => {
7+
const Reanimated = require('react-native-reanimated/mock');
8+
Reanimated.default.call = () => {};
9+
return Reanimated;
10+
});
11+
12+
describe('<SeedPhrase />', () => {
13+
const mockPhrase = ['apple', 'banana', 'cherry', 'date', 'elderberry', 'fig', 'grape', 'honeydew', 'iceberg', 'jackfruit', 'kiwi', 'lemon'];
14+
const primaryColor = '#3B82F6';
15+
16+
it('renders all words when showSeed is true', () => {
17+
const { getByText } = render(
18+
<SeedPhrase
19+
recoveryPhrase={mockPhrase}
20+
showSeed={true}
21+
primaryColor={primaryColor}
22+
/>
23+
);
24+
25+
mockPhrase.forEach((word) => {
26+
expect(getByText(word)).toBeTruthy();
27+
});
28+
});
29+
30+
it('hides words when showSeed is false and no validation is active', () => {
31+
const { queryByText } = render(
32+
<SeedPhrase
33+
recoveryPhrase={mockPhrase}
34+
showSeed={false}
35+
primaryColor={primaryColor}
36+
/>
37+
);
38+
39+
// It should NOT show the words
40+
mockPhrase.forEach((word) => {
41+
expect(queryByText(word)).toBeNull();
42+
});
43+
});
44+
45+
it('renders input fields for words specified in validateWords', () => {
46+
const validateWords = { 0: '', 5: '' };
47+
const { getByPlaceholderText } = render(
48+
<SeedPhrase
49+
recoveryPhrase={mockPhrase}
50+
showSeed={false}
51+
validateWords={validateWords}
52+
primaryColor={primaryColor}
53+
/>
54+
);
55+
56+
expect(getByPlaceholderText('Word #1')).toBeTruthy();
57+
expect(getByPlaceholderText('Word #6')).toBeTruthy();
58+
});
59+
60+
it('calls onInputChange when typing in validation fields', () => {
61+
const validateWords = { 0: '' };
62+
const onInputChange = jest.fn();
63+
const { getByPlaceholderText } = render(
64+
<SeedPhrase
65+
recoveryPhrase={mockPhrase}
66+
showSeed={false}
67+
validateWords={validateWords}
68+
onInputChange={onInputChange}
69+
primaryColor={primaryColor}
70+
/>
71+
);
72+
73+
const input = getByPlaceholderText('Word #1');
74+
fireEvent.changeText(input, 'test');
75+
76+
expect(onInputChange).toHaveBeenCalledWith(0, 'test');
77+
});
78+
});

app.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,17 @@
5757
"showRewards": true,
5858
"showFeeDelegation": true,
5959
"showIdentityManagement": true
60+
},
61+
"router": {},
62+
"eas": {
63+
"projectId": "f1e6cb1b-642d-49fa-b276-53b4403f62d6"
6064
}
65+
},
66+
"runtimeVersion": {
67+
"policy": "appVersion"
68+
},
69+
"updates": {
70+
"url": "https://u.expo.dev/f1e6cb1b-642d-49fa-b276-53b4403f62d6"
6171
}
6272
}
6373
}

app/onboarding.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import React, { useReducer, useRef, useState } from 'react';
2-
import { View, Text, StyleSheet, TouchableOpacity, SafeAreaView, ScrollView, Alert, Image, TextInput } from 'react-native';
1+
import React, { useReducer, useRef } from 'react';
2+
import { View, Text, StyleSheet, TouchableOpacity, SafeAreaView, ScrollView, Alert, Image } from 'react-native';
33
import { useRouter } from 'expo-router';
44
import Constants from 'expo-constants';
55
import { MaterialIcons } from '@expo/vector-icons';

eas.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"build": {
3+
"development": {
4+
"developmentClient": true,
5+
"distribution": "internal"
6+
},
7+
"testing": {
8+
"distribution": "internal",
9+
"channel": "testing"
10+
},
11+
"production": {
12+
"channel": "production",
13+
"autoIncrement": true
14+
}
15+
},
16+
"submit": {
17+
"production": {}
18+
}
19+
}

jest.config.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module.exports = {
2+
preset: 'jest-expo',
3+
setupFilesAfterEnv: ['@testing-library/jest-native/extend-expect'],
4+
transformIgnorePatterns: [
5+
'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)',
6+
],
7+
moduleNameMapper: {
8+
'^@/(.*)$': '<rootDir>/$1',
9+
},
10+
};

lefthook.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# lefthook.yml
2+
3+
pre-commit:
4+
parallel: true
5+
jobs:
6+
- run: npm run lint
7+
- run: npm run test

0 commit comments

Comments
 (0)