diff --git a/src/__tests__/EditProfileScreen.test.tsx b/src/__tests__/EditProfileScreen.test.tsx
new file mode 100644
index 0000000..545a892
--- /dev/null
+++ b/src/__tests__/EditProfileScreen.test.tsx
@@ -0,0 +1,225 @@
+import './__mocks__/setup';
+import React from 'react';
+import renderer, { act } from 'react-test-renderer';
+import { Text, TextInput, TouchableOpacity, Alert } from 'react-native';
+
+jest.mock('../services/api', () => ({
+ updateProfile: jest.fn(),
+}));
+
+jest.mock('../navigation/useAppNavigation', () => ({
+ useRootNavigation: jest.fn(),
+}));
+
+import EditProfileScreen from '../screens/EditProfileScreen';
+import { useUserStore } from '../store/userStore';
+import { updateProfile } from '../services/api';
+import { useRootNavigation } from '../navigation/useAppNavigation';
+
+const mockUpdateProfile = updateProfile as jest.Mock;
+const mockUseRootNavigation = useRootNavigation as jest.Mock;
+
+function inputByPlaceholder(
+ tree: renderer.ReactTestRenderer,
+ placeholder: string,
+): renderer.ReactTestInstance {
+ const input = tree.root
+ .findAllByType(TextInput)
+ .find(i => i.props.placeholder === placeholder);
+ if (!input) {
+ throw new Error(
+ `Could not find an input with placeholder "${placeholder}"`,
+ );
+ }
+ return input;
+}
+
+function buttonWithText(
+ tree: renderer.ReactTestRenderer,
+ label: string,
+): renderer.ReactTestInstance {
+ const button = tree.root
+ .findAllByType(TouchableOpacity)
+ .find(node =>
+ node.findAllByType(Text).some(text => text.props.children === label),
+ );
+ if (!button) {
+ throw new Error(`Could not find a button labelled "${label}"`);
+ }
+ return button;
+}
+
+const INITIAL_PROFILE = {
+ id: 'u1',
+ wallet: 'GABC',
+ name: 'Old Name',
+ bio: 'Old bio',
+ stats: {
+ treesPlanted: 1,
+ plasticCollected: 0,
+ co2Reduced: 0,
+ },
+};
+
+describe('EditProfileScreen', () => {
+ let navigate: jest.Mock;
+ let goBack: jest.Mock;
+ let alertSpy: jest.SpyInstance;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+
+ navigate = jest.fn();
+ goBack = jest.fn();
+ mockUseRootNavigation.mockReturnValue({ navigate, goBack });
+
+ useUserStore.getState().setProfile({ ...INITIAL_PROFILE });
+
+ mockUpdateProfile.mockResolvedValue({ name: 'New Name', bio: 'New bio' });
+
+ alertSpy = jest.spyOn(Alert, 'alert').mockImplementation(() => undefined);
+ });
+
+ afterEach(() => {
+ alertSpy.mockRestore();
+ });
+
+ function render() {
+ let tree!: renderer.ReactTestRenderer;
+ act(() => {
+ tree = renderer.create();
+ });
+ return tree;
+ }
+
+ it('initializes the inputs from the current profile', () => {
+ const tree = render();
+ expect(inputByPlaceholder(tree, 'Your name').props.value).toBe('Old Name');
+ expect(
+ inputByPlaceholder(tree, 'Tell others about yourself').props.value,
+ ).toBe('Old bio');
+ });
+
+ it('updates local state when the name input changes', () => {
+ const tree = render();
+ act(() => {
+ inputByPlaceholder(tree, 'Your name').props.onChangeText('New Name');
+ });
+ expect(inputByPlaceholder(tree, 'Your name').props.value).toBe('New Name');
+ });
+
+ it('updates local state when the bio input changes', () => {
+ const tree = render();
+ act(() => {
+ inputByPlaceholder(tree, 'Tell others about yourself').props.onChangeText(
+ 'A fresh bio',
+ );
+ });
+ expect(
+ inputByPlaceholder(tree, 'Tell others about yourself').props.value,
+ ).toBe('A fresh bio');
+ });
+
+ it('saves and calls api.updateProfile with the trimmed payload', async () => {
+ const tree = render();
+ act(() => {
+ inputByPlaceholder(tree, 'Your name').props.onChangeText(' New Name ');
+ });
+ act(() => {
+ inputByPlaceholder(tree, 'Tell others about yourself').props.onChangeText(
+ ' New bio ',
+ );
+ });
+
+ await act(async () => {
+ await buttonWithText(tree, 'Save Changes').props.onPress();
+ });
+
+ expect(mockUpdateProfile).toHaveBeenCalledTimes(1);
+ expect(mockUpdateProfile).toHaveBeenCalledWith({
+ name: 'New Name',
+ bio: 'New bio',
+ });
+ });
+
+ it('omits bio from the payload when it is empty', async () => {
+ const tree = render();
+ act(() => {
+ inputByPlaceholder(tree, 'Your name').props.onChangeText('New Name');
+ });
+ act(() => {
+ inputByPlaceholder(tree, 'Tell others about yourself').props.onChangeText(
+ ' ',
+ );
+ });
+
+ await act(async () => {
+ await buttonWithText(tree, 'Save Changes').props.onPress();
+ });
+
+ expect(mockUpdateProfile).toHaveBeenCalledWith({
+ name: 'New Name',
+ bio: undefined,
+ });
+ });
+
+ it('updates userStore.profile on success and navigates back', async () => {
+ const tree = render();
+ act(() => {
+ inputByPlaceholder(tree, 'Your name').props.onChangeText('New Name');
+ });
+ act(() => {
+ inputByPlaceholder(tree, 'Tell others about yourself').props.onChangeText(
+ 'New bio',
+ );
+ });
+
+ await act(async () => {
+ await buttonWithText(tree, 'Save Changes').props.onPress();
+ });
+
+ const profile = useUserStore.getState().profile;
+ expect(profile?.name).toBe('New Name');
+ expect(profile?.bio).toBe('New bio');
+ expect(goBack).toHaveBeenCalledTimes(1);
+ });
+
+ it('shows an error alert and keeps the profile on API failure', async () => {
+ mockUpdateProfile.mockRejectedValue(new Error('Boom'));
+
+ const tree = render();
+ act(() => {
+ inputByPlaceholder(tree, 'Your name').props.onChangeText('New Name');
+ });
+
+ await act(async () => {
+ await buttonWithText(tree, 'Save Changes').props.onPress();
+ });
+
+ expect(Alert.alert).toHaveBeenCalledWith('Save Failed', 'Boom');
+ expect(useUserStore.getState().profile?.name).toBe('Old Name');
+ expect(goBack).not.toHaveBeenCalled();
+ });
+
+ it('blocks save and alerts when the name is empty', async () => {
+ const tree = render();
+ act(() => {
+ inputByPlaceholder(tree, 'Your name').props.onChangeText(' ');
+ });
+
+ await act(async () => {
+ await buttonWithText(tree, 'Save Changes').props.onPress();
+ });
+
+ expect(mockUpdateProfile).not.toHaveBeenCalled();
+ expect(Alert.alert).toHaveBeenCalledWith('Error', 'Name cannot be empty');
+ });
+
+ it('navigates back when Cancel is pressed', () => {
+ const tree = render();
+ act(() => {
+ buttonWithText(tree, 'Cancel').props.onPress();
+ });
+ expect(goBack).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/src/__tests__/ProfileScreen.test.tsx b/src/__tests__/ProfileScreen.test.tsx
new file mode 100644
index 0000000..5a9f304
--- /dev/null
+++ b/src/__tests__/ProfileScreen.test.tsx
@@ -0,0 +1,188 @@
+import './__mocks__/setup';
+import React from 'react';
+import renderer, { act } from 'react-test-renderer';
+import { Text, TouchableOpacity } from 'react-native';
+
+jest.mock('../hooks/useStellarWallet', () => ({
+ useStellarWallet: jest.fn(),
+}));
+
+jest.mock('../navigation/useAppNavigation', () => ({
+ useRootNavigation: jest.fn(),
+}));
+
+import ProfileScreen from '../screens/ProfileScreen';
+import { useUserStore } from '../store/userStore';
+import { useWalletStore } from '../store/walletStore';
+import { useRootNavigation } from '../navigation/useAppNavigation';
+import { useStellarWallet } from '../hooks/useStellarWallet';
+
+const mockUseRootNavigation = useRootNavigation as jest.Mock;
+const mockUseStellarWallet = useStellarWallet as jest.Mock;
+const mockDisconnectWallet = jest.fn();
+
+function textValues(tree: renderer.ReactTestRenderer): string[] {
+ return tree.root
+ .findAllByType(Text)
+ .flatMap(node =>
+ (Array.isArray(node.props.children)
+ ? node.props.children
+ : [node.props.children]
+ ).map(child =>
+ typeof child === 'string' || typeof child === 'number'
+ ? String(child)
+ : '',
+ ),
+ )
+ .filter(t => t.length > 0);
+}
+
+function buttonWithText(
+ tree: renderer.ReactTestRenderer,
+ label: string,
+): renderer.ReactTestInstance {
+ const button = tree.root
+ .findAllByType(TouchableOpacity)
+ .find(node =>
+ node.findAllByType(Text).some(text => text.props.children === label),
+ );
+ if (!button) {
+ throw new Error(`Could not find a button labelled "${label}"`);
+ }
+ return button;
+}
+
+const PUBLIC_KEY = 'GABCDEFGHIJKLMNOPQRSTUVWXYZ123456';
+
+function seedStores() {
+ useWalletStore.getState().connect(PUBLIC_KEY, 'inapp');
+ useUserStore.getState().setProfile({
+ id: 'u1',
+ wallet: PUBLIC_KEY,
+ name: 'Ada Lovelace',
+ bio: 'Building a greener world',
+ stats: {
+ treesPlanted: 12,
+ plasticCollected: 30,
+ co2Reduced: 500,
+ },
+ });
+}
+
+describe('ProfileScreen', () => {
+ let navigate: jest.Mock;
+ let goBack: jest.Mock;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+
+ navigate = jest.fn();
+ goBack = jest.fn();
+ mockUseRootNavigation.mockReturnValue({ navigate, goBack });
+
+ mockUseStellarWallet.mockImplementation(() => ({
+ disconnectWallet: mockDisconnectWallet,
+ }));
+ mockDisconnectWallet.mockImplementation(() =>
+ useWalletStore.getState().disconnect(),
+ );
+
+ act(() => {
+ seedStores();
+ });
+ });
+
+ let currentTree: renderer.ReactTestRenderer | null = null;
+
+ function render(): renderer.ReactTestRenderer {
+ act(() => {
+ currentTree = renderer.create();
+ });
+ return currentTree!;
+ }
+
+ afterEach(() => {
+ act(() => {
+ currentTree?.unmount();
+ currentTree = null;
+ });
+ });
+
+ it('renders the profile name from userStore', () => {
+ const tree = render();
+ expect(textValues(tree)).toContain('Ada Lovelace');
+ });
+
+ it('renders the truncated wallet address from walletStore', () => {
+ const tree = render();
+ // truncatePublicKey(PUBLIC_KEY, 6) -> "GABC...23456"
+ expect(textValues(tree).some(t => t.includes(PUBLIC_KEY.slice(0, 6)))).toBe(
+ true,
+ );
+ expect(textValues(tree).some(t => t.includes(PUBLIC_KEY.slice(-6)))).toBe(
+ true,
+ );
+ });
+
+ it('renders the bio when present', () => {
+ const tree = render();
+ expect(textValues(tree)).toContain('Building a greener world');
+ });
+
+ it('renders ImpactStats driven by profile.stats', () => {
+ const tree = render();
+ const texts = textValues(tree);
+ expect(texts).toContain('12');
+ expect(texts).toContain('30kg');
+ expect(texts).toContain('500kg');
+ });
+
+ it('renders AchievementGrid driven by profile.stats', () => {
+ const tree = render();
+ const texts = textValues(tree);
+ expect(texts).toContain('Achievements');
+ // 5 achievements earned: 2 tree, 1 plastic, 2 co2 (see achievements util).
+ expect(texts.some(t => t.includes('unlocked'))).toBe(true);
+ });
+
+ it('disconnect clears both the wallet store and the user store', () => {
+ const tree = render();
+ expect(useWalletStore.getState().isConnected).toBe(true);
+ expect(useUserStore.getState().profile).not.toBeNull();
+
+ act(() => {
+ buttonWithText(tree, 'Disconnect & Sign Out').props.onPress();
+ });
+
+ expect(mockDisconnectWallet).toHaveBeenCalledTimes(1);
+ expect(useWalletStore.getState().isConnected).toBe(false);
+ expect(useUserStore.getState().profile).toBeNull();
+ });
+
+ it('navigates to EditProfile when the settings row is pressed', () => {
+ const tree = render();
+ act(() => {
+ buttonWithText(tree, 'Edit Profile').props.onPress();
+ });
+ expect(navigate).toHaveBeenCalledWith('EditProfile');
+ });
+
+ it('navigates to NotificationPreferences when the settings row is pressed', () => {
+ const tree = render();
+ act(() => {
+ buttonWithText(tree, 'Notification Preferences').props.onPress();
+ });
+ expect(navigate).toHaveBeenCalledWith('NotificationPreferences');
+ });
+
+ it('renders the EmptyState when the wallet is not connected', () => {
+ act(() => {
+ currentTree?.unmount();
+ currentTree = null;
+ useWalletStore.getState().disconnect();
+ });
+ const tree = render();
+ expect(textValues(tree)).toContain('No profile');
+ expect(textValues(tree)).toContain('Connect a wallet to view your profile');
+ });
+});