Skip to content
Draft
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
114 changes: 114 additions & 0 deletions __tests__/app/buddy.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/**
* @fileoverview Tests for the Buddy tab screen
*
* Tests the Sobers Buddy placeholder screen including:
* - Rendering the coming soon message
* - Displaying feature list
* - Personalized greeting with display name
*/

import React from 'react';
import { render, screen } from '@testing-library/react-native';
import BuddyScreen from '@/app/(app)/(tabs)/buddy';

// =============================================================================
// Mocks
// =============================================================================

const mockProfile = {
id: 'user-123',
email: 'test@example.com',
display_name: 'Test User',
sobriety_date: '2024-01-01',
ai_buddy_enabled: true,
};

jest.mock('@/contexts/AuthContext', () => ({
useAuth: () => ({
profile: mockProfile,
}),
}));

jest.mock('@/contexts/ThemeContext', () => ({
useTheme: () => ({
theme: {
primary: '#007AFF',
primaryLight: '#E5F1FF',
text: '#111827',
textSecondary: '#6b7280',
background: '#ffffff',
surface: '#ffffff',
card: '#ffffff',
border: '#e5e7eb',
fontRegular: 'JetBrainsMono-Regular',
fontMedium: 'JetBrainsMono-Medium',
fontSemiBold: 'JetBrainsMono-SemiBold',
fontBold: 'JetBrainsMono-Bold',
},
isDark: false,
}),
}));

jest.mock('@/hooks/useTabBarPadding', () => ({
useTabBarPadding: () => 0,
}));

jest.mock('@/components/navigation/SettingsButton', () => {
const React = require('react');
return {
__esModule: true,
default: () => React.createElement('View', { testID: 'settings-button' }),
};
});

jest.mock('lucide-react-native', () => ({
Bot: () => null,
}));

jest.mock('@/lib/format', () => ({
hexWithAlpha: (hex: string, _alpha: number) => hex,
}));

// =============================================================================
// Test Suite
// =============================================================================

describe('BuddyScreen', () => {
it('renders the title', () => {
render(<BuddyScreen />);

expect(screen.getByText('Sobers Buddy')).toBeTruthy();
});

it('renders the subtitle', () => {
render(<BuddyScreen />);

expect(screen.getByText('Your AI-powered accountability partner')).toBeTruthy();
});

it('renders the coming soon card', () => {
render(<BuddyScreen />);

expect(screen.getByText('Coming Soon')).toBeTruthy();
});

it('includes personalized greeting with display name', () => {
render(<BuddyScreen />);

expect(screen.getByText(/Hey Test User!/)).toBeTruthy();
});

it('renders feature list items', () => {
render(<BuddyScreen />);

expect(screen.getByText('💬 Real-time chat conversations')).toBeTruthy();
expect(screen.getByText('🎯 Personalized recovery support')).toBeTruthy();
expect(screen.getByText('🔒 Private and secure')).toBeTruthy();
expect(screen.getByText('🤝 Non-judgmental, always supportive')).toBeTruthy();
});

it('renders without crashing', () => {
const { toJSON } = render(<BuddyScreen />);
expect(toJSON()).toBeTruthy();
});
});
1 change: 1 addition & 0 deletions __tests__/app/settings.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ jest.mock('lucide-react-native', () => ({
RotateCcw: () => null,
Zap: () => null,
BookOpen: () => null,
Bot: () => null,
}));

jest.mock('@react-native-community/datetimepicker', () => {
Expand Down
75 changes: 72 additions & 3 deletions __tests__/app/tabs-layout-native.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,9 @@ jest.mock('expo-router/unstable-native-tabs', () => {
MockTrigger.Label = MockLabel;
MockTrigger.Icon = MockIcon;
MockTrigger.Badge = MockBadge;
MockTrigger.VectorIcon = () => null;
const MockVectorIcon = () => null;
MockVectorIcon.displayName = 'MockVectorIcon';
MockTrigger.VectorIcon = MockVectorIcon;

const MockBottomAccessory = ({ children }: { children?: React.ReactNode }) =>
React.createElement(View, { testID: 'bottom-accessory' }, children);
Expand Down Expand Up @@ -165,6 +167,7 @@ jest.mock('lucide-react-native', () => ({
TrendingUp: () => null,
CheckSquare: () => null,
User: () => null,
Bot: () => null,
}));

// Store original Platform.OS
Expand Down Expand Up @@ -220,6 +223,7 @@ describe('TabsLayout', () => {
renderWithProviders(<TabsLayout />);

expect(screen.getByTestId('trigger-index')).toBeTruthy();
expect(screen.getByTestId('trigger-buddy')).toBeTruthy();
expect(screen.getByTestId('trigger-program')).toBeTruthy();
expect(screen.getByTestId('trigger-journey')).toBeTruthy();
expect(screen.getByTestId('trigger-tasks')).toBeTruthy();
Expand All @@ -231,6 +235,7 @@ describe('TabsLayout', () => {
renderWithProviders(<TabsLayout />);

expect(screen.getByTestId('trigger-label-Home')).toBeTruthy();
expect(screen.getByTestId('trigger-label-Buddy')).toBeTruthy();
expect(screen.getByTestId('trigger-label-Program')).toBeTruthy();
expect(screen.getByTestId('trigger-label-Journey')).toBeTruthy();
expect(screen.getByTestId('trigger-label-Tasks')).toBeTruthy();
Expand All @@ -242,11 +247,12 @@ describe('TabsLayout', () => {
renderWithProviders(<TabsLayout />);

const icons = screen.getAllByTestId('trigger-icon');
// 5 visible tabs have icons (manage-tasks is hidden and has no icon)
expect(icons.length).toBe(5);
// 6 visible tabs have icons (manage-tasks is hidden and has no icon)
expect(icons.length).toBe(6);

// Verify icon data via captured triggers
expect(mockCapturedTriggers['index']).toBeDefined();
expect(mockCapturedTriggers['buddy']).toBeDefined();
expect(mockCapturedTriggers['program']).toBeDefined();
expect(mockCapturedTriggers['journey']).toBeDefined();
expect(mockCapturedTriggers['tasks']).toBeDefined();
Expand Down Expand Up @@ -283,6 +289,7 @@ describe('TabsLayout', () => {
renderWithProviders(<TabsLayout />);

expect(screen.getByTestId('expo-tab-screen-index')).toBeTruthy();
expect(screen.getByTestId('expo-tab-screen-buddy')).toBeTruthy();
expect(screen.getByTestId('expo-tab-screen-program')).toBeTruthy();
expect(screen.getByTestId('expo-tab-screen-journey')).toBeTruthy();
expect(screen.getByTestId('expo-tab-screen-tasks')).toBeTruthy();
Expand Down Expand Up @@ -402,12 +409,74 @@ describe('TabsLayout', () => {

// All other tabs should not be hidden
expect(screen.getByTestId('trigger-index').props['data-hidden']).toBeFalsy();
expect(screen.getByTestId('trigger-buddy').props['data-hidden']).toBeFalsy();
expect(screen.getByTestId('trigger-journey').props['data-hidden']).toBeFalsy();
expect(screen.getByTestId('trigger-tasks').props['data-hidden']).toBeFalsy();
expect(screen.getByTestId('trigger-profile').props['data-hidden']).toBeFalsy();
});
});

describe('conditional Buddy tab visibility', () => {
beforeEach(() => {
setPlatform('ios');
});

it('shows Buddy tab when ai_buddy_enabled is true', () => {
(useAuth as jest.Mock).mockReturnValue({
profile: { ...mockProfile, ai_buddy_enabled: true },
});

renderWithProviders(<TabsLayout />);

const trigger = screen.getByTestId('trigger-buddy');
expect(trigger.props['data-hidden']).toBeFalsy();
});

it('hides Buddy tab on native when ai_buddy_enabled is false', () => {
(useAuth as jest.Mock).mockReturnValue({
profile: { ...mockProfile, ai_buddy_enabled: false },
});

renderWithProviders(<TabsLayout />);

const trigger = screen.getByTestId('trigger-buddy');
expect(trigger.props['data-hidden']).toBe(true);
});

it('shows Buddy tab when ai_buddy_enabled is undefined (default on)', () => {
(useAuth as jest.Mock).mockReturnValue({
profile: { ...mockProfile, ai_buddy_enabled: undefined },
});

renderWithProviders(<TabsLayout />);

const trigger = screen.getByTestId('trigger-buddy');
expect(trigger.props['data-hidden']).toBeFalsy();
});

it('hides Buddy tab on web when ai_buddy_enabled is false', () => {
setPlatform('web');
(useAuth as jest.Mock).mockReturnValue({
profile: { ...mockProfile, ai_buddy_enabled: false },
});

renderWithProviders(<TabsLayout />);

expect(screen.queryByTestId('expo-tab-screen-buddy')).toBeNull();
});

it('shows Buddy tab on web when ai_buddy_enabled is true', () => {
setPlatform('web');
(useAuth as jest.Mock).mockReturnValue({
profile: { ...mockProfile, ai_buddy_enabled: true },
});

renderWithProviders(<TabsLayout />);

expect(screen.getByTestId('expo-tab-screen-buddy')).toBeTruthy();
});
});

describe('rendering', () => {
it('renders without errors', () => {
const { toJSON } = renderWithProviders(<TabsLayout />);
Expand Down
1 change: 1 addition & 0 deletions __tests__/components/SettingsSheet.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ jest.mock('lucide-react-native', () => ({
RotateCcw: () => null,
Zap: () => null,
BookOpen: () => null,
Bot: () => null,
}));

jest.mock('@react-native-community/datetimepicker', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ jest.mock('lucide-react-native', () => ({
ChevronUp: () => null,
Calendar: () => null,
BookOpen: () => null,
Bot: () => null,
}));

jest.mock('@react-native-community/datetimepicker', () => {
Expand Down
1 change: 1 addition & 0 deletions __tests__/components/settings/SettingsContent.dev.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ jest.mock('lucide-react-native', () => ({
Bell: () => null,
Calendar: () => null,
BookOpen: () => null,
Bot: () => null,
}));

jest.mock('@react-native-community/datetimepicker', () => {
Expand Down
21 changes: 21 additions & 0 deletions __tests__/components/settings/SettingsContent.sections.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ jest.mock('lucide-react-native', () => ({
Bell: () => null,
Calendar: () => null,
BookOpen: () => null,
Bot: () => null,
}));

jest.mock('@react-native-community/datetimepicker', () => {
Expand Down Expand Up @@ -337,5 +338,25 @@ describe('SettingsContent - Section Structure', () => {
expect(screen.getByTestId('settings-show-savings-toggle')).toBeTruthy();
expect(screen.getByText('Show savings card')).toBeTruthy();
});

it('renders AI Buddy toggle in Features section', () => {
render(<SettingsContent onDismiss={mockOnDismiss} />);

expect(screen.getByTestId('settings-ai-buddy-toggle')).toBeTruthy();
expect(screen.getByText('Sobers Buddy')).toBeTruthy();
expect(
screen.getByText(/AI-powered accountability partner for your recovery journey/)
).toBeTruthy();
});

it('displays AI Buddy toggle as ON when ai_buddy_enabled is true or undefined', () => {
render(<SettingsContent onDismiss={mockOnDismiss} />);

// Default profile has ai_buddy_enabled undefined, treated as true
const toggle = screen.getByTestId('settings-ai-buddy-toggle');
expect(toggle).toBeTruthy();

expect(within(toggle).getByText('ON')).toBeTruthy();
});
});
});
Loading
Loading