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
6 changes: 6 additions & 0 deletions SparkyFitnessMobile/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import FoodEntryViewScreen from './src/screens/FoodEntryViewScreen';
import ManualFoodEntryScreen from './src/screens/ManualFoodEntryScreen';
import { configureBackgroundSync } from './src/services/backgroundSyncService';
import { initializeTheme } from './src/services/themeService';
import { initLogService } from './src/services/LogService';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { createNativeBottomTabNavigator } from '@bottom-tabs/react-navigation';
import type { RootStackParamList, TabParamList } from './src/types/navigation';
Expand Down Expand Up @@ -106,6 +107,11 @@ function AppContent() {

initializeApp();

// Initialize log service (warms cache, prunes old logs, registers AppState listener)
initLogService().catch(error => {
console.error('[App] Failed to initialize log service:', error);
});

// Configure background sync without blocking app startup
configureBackgroundSync().catch(error => {
console.error('[App] Failed to configure background sync:', error);
Expand Down
54 changes: 53 additions & 1 deletion SparkyFitnessMobile/__tests__/hooks/useRefetchOnFocus.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,21 @@ jest.mock('@react-navigation/native', () => ({
const mockUseFocusEffect = useFocusEffect as jest.MockedFunction<typeof useFocusEffect>;

describe('useRefetchOnFocus', () => {
let focusCallback: (() => void) | undefined;

beforeEach(() => {
jest.clearAllMocks();
// By default, simulate immediate focus
focusCallback = undefined;
// Capture the callback and invoke it immediately (simulates focus on mount)
mockUseFocusEffect.mockImplementation((callback) => {
focusCallback = callback;
callback();
});
jest.spyOn(Date, 'now').mockReturnValue(0);
});

afterEach(() => {
jest.restoreAllMocks();
});

test('calls refetch when enabled is true (default)', () => {
Expand Down Expand Up @@ -63,4 +72,47 @@ describe('useRefetchOnFocus', () => {

expect(mockUseFocusEffect).toHaveBeenCalledWith(expect.any(Function));
});

test('skips refetch when re-focused within staleTime', () => {
const mockRefetch = jest.fn();

renderHook(() => useRefetchOnFocus(mockRefetch));
expect(mockRefetch).toHaveBeenCalledTimes(1);

// Simulate re-focus 10s later (within the default 30s staleTime)
(Date.now as jest.Mock).mockReturnValue(10_000);
focusCallback!();

expect(mockRefetch).toHaveBeenCalledTimes(1);
});

test('refetches after staleTime has elapsed', () => {
const mockRefetch = jest.fn();

renderHook(() => useRefetchOnFocus(mockRefetch));
expect(mockRefetch).toHaveBeenCalledTimes(1);

// Simulate re-focus 30s later (exactly at staleTime boundary)
(Date.now as jest.Mock).mockReturnValue(30_000);
focusCallback!();

expect(mockRefetch).toHaveBeenCalledTimes(2);
});

test('custom staleTime is respected', () => {
const mockRefetch = jest.fn();

renderHook(() => useRefetchOnFocus(mockRefetch, true, 5_000));
expect(mockRefetch).toHaveBeenCalledTimes(1);

// 4s later — still within 5s staleTime
(Date.now as jest.Mock).mockReturnValue(4_000);
focusCallback!();
expect(mockRefetch).toHaveBeenCalledTimes(1);

// 5s later — staleTime elapsed
(Date.now as jest.Mock).mockReturnValue(5_000);
focusCallback!();
expect(mockRefetch).toHaveBeenCalledTimes(2);
});
});
58 changes: 26 additions & 32 deletions SparkyFitnessMobile/__tests__/hooks/useWaterIntakeMutation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Alert } from 'react-native';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useWaterIntakeMutation } from '../../src/hooks/useWaterIntakeMutation';
import { fetchWaterContainers, changeWaterIntake } from '../../src/services/api/measurementsApi';
import type { DailySummary } from '../../src/types/dailySummary';
import type { DailySummaryRawData } from '../../src/hooks/useDailySummary';
import { dailySummaryQueryKey } from '../../src/hooks/queryKeys';

jest.mock('../../src/services/api/measurementsApi', () => ({
Expand All @@ -30,26 +30,20 @@ const primaryContainer = {
servings_per_container: 1,
};

const makeSummary = (waterConsumed = 500): DailySummary => ({
date: '2024-06-15',
calorieGoal: 2000,
caloriesConsumed: 1200,
caloriesBurned: 300,
activeCalories: 200,
otherExerciseCalories: 100,
exerciseMinutes: 30,
exerciseMinutesGoal: 60,
exerciseCaloriesGoal: 300,
netCalories: 900,
remainingCalories: 1100,
protein: { consumed: 80, goal: 150 },
carbs: { consumed: 200, goal: 250 },
fat: { consumed: 50, goal: 70 },
fiber: { consumed: 20, goal: 30 },
waterConsumed,
waterGoal: 2500,
const makeRawData = (waterMl = 500): DailySummaryRawData => ({
goals: {
calories: 2000,
protein: 150,
carbs: 250,
fat: 70,
dietary_fiber: 30,
water_goal_ml: 2500,
target_exercise_calories_burned: 300,
target_exercise_duration_minutes: 60,
},
foodEntries: [],
exerciseEntries: [],
waterIntake: { water_ml: waterMl },
});

describe('useWaterIntakeMutation', () => {
Expand Down Expand Up @@ -213,7 +207,7 @@ describe('useWaterIntakeMutation', () => {
});

test('optimistic update adjusts waterConsumed in cache', async () => {
const summary = makeSummary(500);
const summary = makeRawData(500);
queryClient.setQueryData(dailySummaryQueryKey(testDate), summary);

// Hold the mutation so we can check the optimistic state
Expand All @@ -236,8 +230,8 @@ describe('useWaterIntakeMutation', () => {

// Check optimistic update applied
await waitFor(() => {
const cached = queryClient.getQueryData<DailySummary>(dailySummaryQueryKey(testDate));
expect(cached?.waterConsumed).toBe(750); // 500 + 250 (container volume)
const cached = queryClient.getQueryData<DailySummaryRawData>(dailySummaryQueryKey(testDate));
expect(cached?.waterIntake.water_ml).toBe(750); // 500 + 250 (container volume)
});

// Resolve with server truth
Expand All @@ -247,13 +241,13 @@ describe('useWaterIntakeMutation', () => {

// Server truth overwrites optimistic value
await waitFor(() => {
const cached = queryClient.getQueryData<DailySummary>(dailySummaryQueryKey(testDate));
expect(cached?.waterConsumed).toBe(760);
const cached = queryClient.getQueryData<DailySummaryRawData>(dailySummaryQueryKey(testDate));
expect(cached?.waterIntake.water_ml).toBe(760);
});
});

test('server truth overwrites optimistic value on success', async () => {
const summary = makeSummary(1000);
const summary = makeRawData(1000);
queryClient.setQueryData(dailySummaryQueryKey(testDate), summary);

mockChangeWaterIntake.mockResolvedValue({ id: '1', water_ml: 1300, entry_date: testDate });
Expand All @@ -271,13 +265,13 @@ describe('useWaterIntakeMutation', () => {
});

await waitFor(() => {
const cached = queryClient.getQueryData<DailySummary>(dailySummaryQueryKey(testDate));
expect(cached?.waterConsumed).toBe(1300);
const cached = queryClient.getQueryData<DailySummaryRawData>(dailySummaryQueryKey(testDate));
expect(cached?.waterIntake.water_ml).toBe(1300);
});
});

test('invalidates query on error', async () => {
const summary = makeSummary(500);
const summary = makeRawData(500);
queryClient.setQueryData(dailySummaryQueryKey(testDate), summary);

mockChangeWaterIntake.mockRejectedValue(new Error('Network error'));
Expand Down Expand Up @@ -311,7 +305,7 @@ describe('useWaterIntakeMutation', () => {
});

test('optimistic decrement clamps to zero', async () => {
const summary = makeSummary(100); // Less than container volume (250)
const summary = makeRawData(100); // Less than container volume (250)
queryClient.setQueryData(dailySummaryQueryKey(testDate), summary);

let resolveMutation: (value: { id: string; water_ml: number; entry_date: string }) => void;
Expand All @@ -333,8 +327,8 @@ describe('useWaterIntakeMutation', () => {

// Optimistic should clamp to 0, not go negative
await waitFor(() => {
const cached = queryClient.getQueryData<DailySummary>(dailySummaryQueryKey(testDate));
expect(cached?.waterConsumed).toBe(0);
const cached = queryClient.getQueryData<DailySummaryRawData>(dailySummaryQueryKey(testDate));
expect(cached?.waterIntake.water_ml).toBe(0);
});

await act(async () => {
Expand All @@ -343,7 +337,7 @@ describe('useWaterIntakeMutation', () => {
});

test('rapid taps: each mutation sends to server', async () => {
const summary = makeSummary(500);
const summary = makeRawData(500);
queryClient.setQueryData(dailySummaryQueryKey(testDate), summary);

let callCount = 0;
Expand Down
Loading
Loading