feat: add store to save and share state

main
Torpenn 2025-04-08 22:30:14 +02:00
parent 14f1c50215
commit 2e44837381
No known key found for this signature in database
GPG Key ID: 56C8A89C974E3ED2
17 changed files with 841 additions and 48 deletions

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import { Text } from '@/components/shared/Themed';
import Card from '@/components/shared/Card';
@ -12,6 +12,8 @@ import { TimerPickerModal } from 'react-native-timer-picker';
import Button from '@/components/shared/Button';
import NumberSelector from '@/components/shared/NumberSelector';
import { i18n } from '@/app/i18n/i18n';
import { useTimerContext } from '@/app/store/TimerContext';
import { ActivityIndicator } from 'react-native';
type NavigationProp = NativeStackNavigationProp<RootStackParamList, 'Timer'>;
@ -19,14 +21,41 @@ const t = i18n.scoped('dashboard');
export default function Dashboard() {
const navigation = useNavigation<NavigationProp>();
const { timerState, isLoading, updateReps, updateWorkTime, updateRestTime } = useTimerContext();
const [reps, setReps] = useState<number>(1);
const [workTime, setWorkTime] = useState<number>(0);
const [restTime, setRestTime] = useState<number>(0);
const [showWorkTimePicker, setShowWorkTimePicker] = useState<boolean>(false);
const [showRestTimePicker, setShowRestTimePicker] = useState<boolean>(false);
const totalWorkTime: string = formatTime((workTime + restTime) * reps);
// Local variables for UI changes
const [localReps, setLocalReps] = useState<number>(timerState.reps);
const [localWorkTime, setLocalWorkTime] = useState<number>(timerState.workTime);
const [localRestTime, setLocalRestTime] = useState<number>(timerState.restTime);
// Update local states when store data is loaded
useEffect(() => {
if (!isLoading) {
setLocalReps(timerState.reps);
setLocalWorkTime(timerState.workTime);
setLocalRestTime(timerState.restTime);
}
}, [isLoading, timerState]);
// Handle repetition changes
const handleRepsChange = (value: number) => {
setLocalReps(value);
updateReps(value);
};
const totalWorkTime: string = formatTime((localWorkTime + localRestTime) * localReps);
if (isLoading) {
return (
<LoadingContainer>
<ActivityIndicator size="large" />
<Text>Chargement...</Text>
</LoadingContainer>
);
}
return (
<Container>
@ -38,14 +67,14 @@ export default function Dashboard() {
<Card backgroundColor="black">
<CardTextContainer>
<CustomText>{t('repetition')}</CustomText>
<NumberSelector reps={reps} setReps={setReps} />
<NumberSelector reps={localReps} setReps={handleRepsChange} />
</CardTextContainer>
</Card>
<Card backgroundColor="red" onPress={() => setShowWorkTimePicker(true)}>
<CardTextContainer>
<CustomText>{t('fight')}</CustomText>
<CustomText>{formatTime(workTime)}</CustomText>
<CustomText>{formatTime(localWorkTime)}</CustomText>
</CardTextContainer>
</Card>
@ -54,7 +83,9 @@ export default function Dashboard() {
visible={showWorkTimePicker}
setIsVisible={setShowWorkTimePicker}
onConfirm={(pickedDuration: any) => {
setWorkTime(pickedDuration.minutes * 60 + pickedDuration.seconds);
const newWorkTime = pickedDuration.minutes * 60 + pickedDuration.seconds;
setLocalWorkTime(newWorkTime);
updateWorkTime(newWorkTime);
setShowWorkTimePicker(false);
}}
modalTitle={t('setWorkTime')}
@ -63,6 +94,7 @@ export default function Dashboard() {
styles={{
theme: "dark",
}}
initialValue={{ minutes: Math.floor(timerState.workTime / 60), seconds: timerState.workTime % 60 }}
modalProps={{
overlayOpacity: 0.2,
}}
@ -71,7 +103,7 @@ export default function Dashboard() {
<Card backgroundColor="green" onPress={() => setShowRestTimePicker(true)}>
<CardTextContainer>
<CustomText>{t('rest')}</CustomText>
<CustomText>{formatTime(restTime)}</CustomText>
<CustomText>{formatTime(localRestTime)}</CustomText>
</CardTextContainer>
</Card>
@ -80,12 +112,15 @@ export default function Dashboard() {
visible={showRestTimePicker}
setIsVisible={setShowRestTimePicker}
onConfirm={(pickedDuration: any) => {
setRestTime(pickedDuration.minutes * 60 + pickedDuration.seconds);
const newRestTime = pickedDuration.minutes * 60 + pickedDuration.seconds;
setLocalRestTime(newRestTime);
updateRestTime(newRestTime);
setShowRestTimePicker(false);
}}
modalTitle={t('setRestTime')}
onCancel={() => setShowRestTimePicker(false)}
closeOnOverlayPress
initialValue={{ minutes: Math.floor(timerState.restTime / 60), seconds: timerState.restTime % 60 }}
styles={{
theme: "dark",
}}
@ -100,7 +135,7 @@ export default function Dashboard() {
<VerticalSpacer heightUnits={5} />
<Button label={t('begin')} onPress={() => navigation.navigate('Timer', { reps, restTime, workTime})} />
<Button label={t('begin')} onPress={() => navigation.navigate('Timer')} />
<VerticalSpacer heightUnits={5} />
@ -109,6 +144,13 @@ export default function Dashboard() {
);
}
const LoadingContainer = styled.View({
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20
});
const Title = styled(Text)(({ theme }) => ({
textAlign: 'center',
fontSize: 40,

View File

@ -1,5 +1,4 @@
export type RootStackParamList = {
Dashboard: undefined;
Timer: { reps: number, restTime: number, workTime: number};
Timer: undefined;
Settings: undefined;
};

View File

@ -81,7 +81,7 @@ const Title = styled(Text)(({ theme }) => ({
textAlign: 'center',
fontSize: 40,
fontWeight: 'bold',
color: theme.colors.black
color: theme.colors.fixed.black
}));
const Row = styled.View({
@ -93,4 +93,3 @@ const Row = styled.View({
const Container = styled.View({
padding: 20
})

View File

@ -11,6 +11,7 @@ import { loadUserSettings } from "@/components/shared/business/AsyncStorage";
import BackgroundTimer from "react-native-background-timer";
import { useAudio } from "@/components/useCases/timer/business/useAudio";
import { useNotification } from "@/components/useCases/timer/business/useNotifications";
import { useTimerContext } from "@/app/store/TimerContext";
interface TimerProps {
reps: number;
@ -21,8 +22,8 @@ interface TimerProps {
export default function Timer() {
const navigation = useNavigation();
const route = useRoute();
const { timerState } = useTimerContext();
const { reps, restTime, workTime } = route.params as TimerProps;
const [currentRep, setCurrentRep] = useState<number>(0);
const [timeLeft, setTimeLeft] = useState<number>(0);
const [isWorkPhase, setIsWorkPhase] = useState<boolean>(true);
@ -51,7 +52,7 @@ export default function Timer() {
handleStart();
await activateKeepAwakeAsync();
} catch (error) {
throw new Error("Erreur lors du chargement des paramètres utilisateur");
throw new Error("Error loading user settings");
}
};
@ -93,7 +94,7 @@ export default function Timer() {
const handleStart = () => {
setCurrentRep(1);
setIsWorkPhase(true);
setTimeLeft(workTime);
setTimeLeft(timerState.workTime);
setIsRunning(true);
setIsFinish(false);
updateNotification(
@ -103,24 +104,24 @@ export default function Timer() {
};
const handleReset = () => {
setCurrentRep(1);
setCurrentRep(0);
setIsWorkPhase(true);
setTimeLeft(workTime);
setTimeLeft(timerState.workTime);
setIsRunning(false);
setIsFinish(false);
cancelNotification();
};
const nextRep = () => {
if (currentRep < reps) {
if (currentRep < timerState.reps) {
if (isWorkPhase) {
playSound();
setIsWorkPhase(false);
setTimeLeft(restTime);
setTimeLeft(timerState.restTime);
} else {
playSound();
setIsWorkPhase(true);
setTimeLeft(workTime);
setTimeLeft(timerState.workTime);
setCurrentRep((prevRep) => prevRep + 1);
}
} else {
@ -135,12 +136,12 @@ export default function Timer() {
if (isWorkPhase) {
if (currentRep > 1) {
setIsWorkPhase(false);
setTimeLeft(restTime);
setTimeLeft(timerState.restTime);
setCurrentRep((prevRep) => prevRep - 1);
}
} else {
setIsWorkPhase(true);
setTimeLeft(workTime);
setTimeLeft(timerState.workTime);
}
};
@ -167,7 +168,7 @@ export default function Timer() {
<TimerContent
isWorkPhase={isWorkPhase}
timeLeft={timeLeft}
reps={reps}
reps={timerState.reps}
bgColor={renderBgColor()}
currentRep={currentRep}
isRunning={isRunning}

View File

@ -8,6 +8,7 @@ import { useEffect } from 'react';
import 'react-native-reanimated';
import { i18n } from './i18n/i18n';
import { LanguageProvider } from '@/app/shared/providers/LanguageProvider';
import { TimerProvider } from '@/app/store/TimerContext';
export {
// Catch any errors thrown by the Layout component.
@ -52,11 +53,13 @@ function RootLayoutNav() {
return (
<LanguageProvider>
<ThemeProvider theme={dynamicTheme}>
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="Timer" options={{ headerShown: false }} />
<Stack.Screen name="Settings" options={{ headerShown: true, title: i18n.t('settings.title') }} />
</Stack>
<TimerProvider>
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="Timer" options={{ headerShown: false }} />
<Stack.Screen name="Settings" options={{ headerShown: true, title: i18n.t('settings.title') }} />
</Stack>
</TimerProvider>
</ThemeProvider>
</LanguageProvider>
);

View File

@ -8,7 +8,6 @@ export const theme = {
};
export const useThemeColors = () => {
const colorScheme = useColorScheme() ?? 'light';
return {
...theme,
};

119
app/store/TimerContext.tsx Normal file
View File

@ -0,0 +1,119 @@
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { timerStore, TimerState } from './TimerStore';
// Context interface
interface TimerContextType {
timerState: TimerState;
isLoading: boolean;
updateReps: (reps: number) => Promise<void>;
updateWorkTime: (workTime: number) => Promise<void>;
updateRestTime: (restTime: number) => Promise<void>;
updateState: (state: Partial<TimerState>) => Promise<void>;
}
// Default context values
const defaultContextValue: TimerContextType = {
timerState: {
reps: 1,
workTime: 0,
restTime: 0
},
isLoading: true,
updateReps: async () => {},
updateWorkTime: async () => {},
updateRestTime: async () => {},
updateState: async () => {}
};
// Context creation
const TimerContext = createContext<TimerContextType>(defaultContextValue);
// Custom hook to use the context
export const useTimerContext = () => useContext(TimerContext);
// Provider props
interface TimerProviderProps {
children: ReactNode;
}
// Provider component
export const TimerProvider: React.FC<TimerProviderProps> = ({ children }) => {
const [timerState, setTimerState] = useState<TimerState>({
reps: 1,
workTime: 0,
restTime: 0
});
const [isLoading, setIsLoading] = useState(true);
// Load data on startup
useEffect(() => {
const loadData = async () => {
try {
const state = await timerStore.getState();
setTimerState(state);
} catch (error) {
console.error('Error loading data:', error);
} finally {
setIsLoading(false);
}
};
loadData();
}, []);
// Update repetitions
const updateReps = async (reps: number) => {
try {
await timerStore.saveReps(reps);
setTimerState(prev => ({ ...prev, reps }));
} catch (error) {
console.error('Error updating repetitions:', error);
}
};
// Update work time
const updateWorkTime = async (workTime: number) => {
try {
await timerStore.saveWorkTime(workTime);
setTimerState(prev => ({ ...prev, workTime }));
} catch (error) {
console.error('Error updating work time:', error);
}
};
// Update rest time
const updateRestTime = async (restTime: number) => {
try {
await timerStore.saveRestTime(restTime);
setTimerState(prev => ({ ...prev, restTime }));
} catch (error) {
console.error('Error updating rest time:', error);
}
};
// Update multiple values at once
const updateState = async (state: Partial<TimerState>) => {
try {
const newState = { ...timerState, ...state };
await timerStore.saveState(newState);
setTimerState(newState);
} catch (error) {
console.error('Error updating state:', error);
}
};
const contextValue: TimerContextType = {
timerState,
isLoading,
updateReps,
updateWorkTime,
updateRestTime,
updateState
};
return (
<TimerContext.Provider value={contextValue}>
{children}
</TimerContext.Provider>
);
};

121
app/store/TimerStore.ts Normal file
View File

@ -0,0 +1,121 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
// Storage keys definition
const TIMER_KEYS = {
REPS: 'timer_reps',
WORK_TIME: 'timer_work_time',
REST_TIME: 'timer_rest_time'
};
// Store interface
export interface TimerState {
reps: number;
workTime: number;
restTime: number;
}
// Default values
const DEFAULT_STATE: TimerState = {
reps: 1,
workTime: 0,
restTime: 0
};
// Store management class
class TimerStore {
// Method to save repetitions
async saveReps(reps: number): Promise<void> {
try {
await AsyncStorage.setItem(TIMER_KEYS.REPS, reps.toString());
} catch (error) {
console.error('Error saving repetitions:', error);
}
}
// Method to save work time
async saveWorkTime(workTime: number): Promise<void> {
try {
await AsyncStorage.setItem(TIMER_KEYS.WORK_TIME, workTime.toString());
} catch (error) {
console.error('Error saving work time:', error);
}
}
// Method to save rest time
async saveRestTime(restTime: number): Promise<void> {
try {
await AsyncStorage.setItem(TIMER_KEYS.REST_TIME, restTime.toString());
} catch (error) {
console.error('Error saving rest time:', error);
}
}
// Method to save complete state
async saveState(state: TimerState): Promise<void> {
try {
await Promise.all([
this.saveReps(state.reps),
this.saveWorkTime(state.workTime),
this.saveRestTime(state.restTime)
]);
} catch (error) {
console.error('Error saving state:', error);
}
}
// Method to retrieve repetitions
async getReps(): Promise<number> {
try {
const value = await AsyncStorage.getItem(TIMER_KEYS.REPS);
return value !== null ? parseInt(value, 10) : DEFAULT_STATE.reps;
} catch (error) {
console.error('Error retrieving repetitions:', error);
return DEFAULT_STATE.reps;
}
}
// Method to retrieve work time
async getWorkTime(): Promise<number> {
try {
const value = await AsyncStorage.getItem(TIMER_KEYS.WORK_TIME);
return value !== null ? parseInt(value, 10) : DEFAULT_STATE.workTime;
} catch (error) {
console.error('Error retrieving work time:', error);
return DEFAULT_STATE.workTime;
}
}
// Method to retrieve rest time
async getRestTime(): Promise<number> {
try {
const value = await AsyncStorage.getItem(TIMER_KEYS.REST_TIME);
return value !== null ? parseInt(value, 10) : DEFAULT_STATE.restTime;
} catch (error) {
console.error('Error retrieving rest time:', error);
return DEFAULT_STATE.restTime;
}
}
// Method to retrieve complete state
async getState(): Promise<TimerState> {
try {
const [reps, workTime, restTime] = await Promise.all([
this.getReps(),
this.getWorkTime(),
this.getRestTime()
]);
return {
reps,
workTime,
restTime
};
} catch (error) {
console.error('Error retrieving state:', error);
return DEFAULT_STATE;
}
}
}
// Export a singleton instance
export const timerStore = new TimerStore();

View File

@ -0,0 +1,205 @@
import { renderHook, act, waitFor } from '@testing-library/react-native';
import { TimerProvider, useTimerContext } from '../TimerContext';
import { timerStore } from '../TimerStore';
import '@testing-library/jest-native/extend-expect';
jest.mock('../TimerStore', () => ({
timerStore: {
getState: jest.fn(),
saveReps: jest.fn(),
saveWorkTime: jest.fn(),
saveRestTime: jest.fn(),
saveState: jest.fn(),
}
}));
const mockTimerState = {
reps: 5,
workTime: 30,
restTime: 10
};
describe('TimerContext', () => {
beforeEach(() => {
jest.clearAllMocks();
// Default configuration for getState
(timerStore.getState as jest.Mock<Promise<any>>).mockResolvedValue(mockTimerState);
});
it('should load initial data', async () => {
const { result } = renderHook(() => useTimerContext(), {
wrapper: TimerProvider
});
// Check initial loading state
expect(result.current.isLoading).toBe(true);
// Wait for loading to complete
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
// Verify getState was called
expect(timerStore.getState).toHaveBeenCalledTimes(1);
// Verify data is correctly loaded
expect(result.current.timerState).toEqual(mockTimerState);
});
it('should handle errors when loading data', async () => {
// Simulate an error
(timerStore.getState as jest.Mock<Promise<any>>).mockRejectedValueOnce(new Error('Loading error'));
// Spy on console.error
jest.spyOn(console, 'error').mockImplementation(() => {});
const { result } = renderHook(() => useTimerContext(), {
wrapper: TimerProvider
});
// Wait for loading to complete despite error
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
// Verify error was logged
expect(console.error).toHaveBeenCalledWith(
'Error loading data:',
expect.any(Error)
);
// Restore console.error
(console.error as jest.MockedFunction<typeof console.error>).mockRestore();
});
it('should update repetitions', async () => {
const { result } = renderHook(() => useTimerContext(), {
wrapper: TimerProvider
});
// Wait for initial loading to complete
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
// Update repetitions
await act(async () => {
await result.current.updateReps(10);
});
// Verify saveReps was called with the correct value
expect(timerStore.saveReps).toHaveBeenCalledWith(10);
// Verify state was updated
expect(result.current.timerState.reps).toBe(10);
});
it('should handle errors when updating repetitions', async () => {
// Simulate an error
(timerStore.saveReps as jest.Mock<Promise<void>>).mockRejectedValueOnce(new Error('Saving error'));
// Spy on console.error
jest.spyOn(console, 'error').mockImplementation(() => {});
const { result } = renderHook(() => useTimerContext(), {
wrapper: TimerProvider
});
// Wait for initial loading to complete
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
// Attempt to update repetitions
await act(async () => {
await result.current.updateReps(10);
});
// Verify error was logged
expect(console.error).toHaveBeenCalledWith(
'Error updating repetitions:',
expect.any(Error)
);
// Restore console.error
(console.error as jest.MockedFunction<typeof console.error>).mockRestore();
});
it('should update work time', async () => {
const { result } = renderHook(() => useTimerContext(), {
wrapper: TimerProvider
});
// Wait for initial loading to complete
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
// Update work time
await act(async () => {
await result.current.updateWorkTime(45);
});
// Verify saveWorkTime was called with the correct value
expect(timerStore.saveWorkTime).toHaveBeenCalledWith(45);
// Verify state was updated
expect(result.current.timerState.workTime).toBe(45);
});
it('should update rest time', async () => {
const { result } = renderHook(() => useTimerContext(), {
wrapper: TimerProvider
});
// Wait for initial loading to complete
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
// Update rest time
await act(async () => {
await result.current.updateRestTime(15);
});
// Verify saveRestTime was called with the correct value
expect(timerStore.saveRestTime).toHaveBeenCalledWith(15);
// Verify state was updated
expect(result.current.timerState.restTime).toBe(15);
});
it('should update multiple values at once', async () => {
const { result } = renderHook(() => useTimerContext(), {
wrapper: TimerProvider
});
// Wait for initial loading to complete
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
const newState = {
reps: 8,
workTime: 60,
restTime: 20
};
// Update complete state
await act(async () => {
await result.current.updateState(newState);
});
// Verify saveState was called with the correct state
expect(timerStore.saveState).toHaveBeenCalledWith({
...mockTimerState,
...newState
});
// Verify state was updated
expect(result.current.timerState).toEqual({
...mockTimerState,
...newState
});
});
});

View File

@ -0,0 +1,263 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import { timerStore, TimerState } from '../TimerStore';
// Mock AsyncStorage with correct typing for Jest
jest.mock('@react-native-async-storage/async-storage', () => ({
setItem: jest.fn(() => Promise.resolve()),
getItem: jest.fn(() => Promise.resolve(null)),
}));
// Add type for mocked functions
const mockedAsyncStorage = AsyncStorage as jest.Mocked<typeof AsyncStorage>;
describe('TimerStore', () => {
// Clear all mocks after each test
afterEach(() => {
jest.clearAllMocks();
});
// Tests for saveReps method
describe('saveReps', () => {
test('should save repetitions in AsyncStorage', async () => {
await timerStore.saveReps(5);
expect(mockedAsyncStorage.setItem).toHaveBeenCalledWith('timer_reps', '5');
});
test('should handle errors when saving repetitions', async () => {
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
mockedAsyncStorage.setItem.mockRejectedValueOnce(new Error('AsyncStorage Error'));
await timerStore.saveReps(3);
expect(consoleSpy).toHaveBeenCalledWith(
'Error saving repetitions:',
expect.any(Error)
);
consoleSpy.mockRestore();
});
});
// Tests for saveWorkTime method
describe('saveWorkTime', () => {
test('should save work time in AsyncStorage', async () => {
await timerStore.saveWorkTime(30);
expect(mockedAsyncStorage.setItem).toHaveBeenCalledWith('timer_work_time', '30');
});
test('should handle errors when saving work time', async () => {
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
mockedAsyncStorage.setItem.mockRejectedValueOnce(new Error('AsyncStorage Error'));
await timerStore.saveWorkTime(30);
expect(consoleSpy).toHaveBeenCalledWith(
'Error saving work time:',
expect.any(Error)
);
consoleSpy.mockRestore();
});
});
// Tests for saveRestTime method
describe('saveRestTime', () => {
test('should save rest time in AsyncStorage', async () => {
await timerStore.saveRestTime(15);
expect(mockedAsyncStorage.setItem).toHaveBeenCalledWith('timer_rest_time', '15');
});
test('should handle errors when saving rest time', async () => {
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
mockedAsyncStorage.setItem.mockRejectedValueOnce(new Error('AsyncStorage Error'));
await timerStore.saveRestTime(15);
expect(consoleSpy).toHaveBeenCalledWith(
'Error saving rest time:',
expect.any(Error)
);
consoleSpy.mockRestore();
});
});
// Tests for saveState method
describe('saveState', () => {
test('should save complete state in AsyncStorage', async () => {
const state: TimerState = {
reps: 3,
workTime: 40,
restTime: 20
};
await timerStore.saveState(state);
expect(mockedAsyncStorage.setItem).toHaveBeenCalledWith('timer_reps', '3');
expect(mockedAsyncStorage.setItem).toHaveBeenCalledWith('timer_work_time', '40');
expect(mockedAsyncStorage.setItem).toHaveBeenCalledWith('timer_rest_time', '20');
});
test('should handle errors when saving state', async () => {
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
mockedAsyncStorage.setItem.mockRejectedValueOnce(new Error('AsyncStorage Error'));
const state: TimerState = {
reps: 3,
workTime: 40,
restTime: 20
};
await timerStore.saveState(state);
expect(consoleSpy).toHaveBeenCalledWith(
'Error saving repetitions:',
expect.any(Error)
);
consoleSpy.mockRestore();
});
});
// Tests for getReps method
describe('getReps', () => {
test('should retrieve repetitions from AsyncStorage', async () => {
mockedAsyncStorage.getItem.mockResolvedValueOnce('5');
const result = await timerStore.getReps();
expect(result).toBe(5);
expect(mockedAsyncStorage.getItem).toHaveBeenCalledWith('timer_reps');
});
test('should return default value if no data is found', async () => {
mockedAsyncStorage.getItem.mockResolvedValueOnce(null);
const result = await timerStore.getReps();
expect(result).toBe(1); // Default value
expect(mockedAsyncStorage.getItem).toHaveBeenCalledWith('timer_reps');
});
test('should handle errors when retrieving repetitions', async () => {
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
mockedAsyncStorage.getItem.mockRejectedValueOnce(new Error('AsyncStorage Error'));
const result = await timerStore.getReps();
expect(result).toBe(1); // Default value
expect(consoleSpy).toHaveBeenCalledWith(
'Error retrieving repetitions:',
expect.any(Error)
);
consoleSpy.mockRestore();
});
});
// Tests for getWorkTime method
describe('getWorkTime', () => {
test('should retrieve work time from AsyncStorage', async () => {
mockedAsyncStorage.getItem.mockResolvedValueOnce('30');
const result = await timerStore.getWorkTime();
expect(result).toBe(30);
expect(mockedAsyncStorage.getItem).toHaveBeenCalledWith('timer_work_time');
});
test('should return default value if no data is found', async () => {
mockedAsyncStorage.getItem.mockResolvedValueOnce(null);
const result = await timerStore.getWorkTime();
expect(result).toBe(0); // Default value
expect(mockedAsyncStorage.getItem).toHaveBeenCalledWith('timer_work_time');
});
test('should handle errors when retrieving work time', async () => {
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
mockedAsyncStorage.getItem.mockRejectedValueOnce(new Error('AsyncStorage Error'));
const result = await timerStore.getWorkTime();
expect(result).toBe(0); // Default value
expect(consoleSpy).toHaveBeenCalledWith(
'Error retrieving work time:',
expect.any(Error)
);
consoleSpy.mockRestore();
});
});
// Tests for getRestTime method
describe('getRestTime', () => {
test('should retrieve rest time from AsyncStorage', async () => {
mockedAsyncStorage.getItem.mockResolvedValueOnce('15');
const result = await timerStore.getRestTime();
expect(result).toBe(15);
expect(mockedAsyncStorage.getItem).toHaveBeenCalledWith('timer_rest_time');
});
test('should return default value if no data is found', async () => {
mockedAsyncStorage.getItem.mockResolvedValueOnce(null);
const result = await timerStore.getRestTime();
expect(result).toBe(0); // Default value
expect(mockedAsyncStorage.getItem).toHaveBeenCalledWith('timer_rest_time');
});
test('should handle errors when retrieving rest time', async () => {
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
mockedAsyncStorage.getItem.mockRejectedValueOnce(new Error('AsyncStorage Error'));
const result = await timerStore.getRestTime();
expect(result).toBe(0); // Default value
expect(consoleSpy).toHaveBeenCalledWith(
'Error retrieving rest time:',
expect.any(Error)
);
consoleSpy.mockRestore();
});
});
// Tests for getState method
describe('getState', () => {
test('should retrieve complete state from AsyncStorage', async () => {
mockedAsyncStorage.getItem
.mockResolvedValueOnce('3') // reps
.mockResolvedValueOnce('40') // workTime
.mockResolvedValueOnce('20'); // restTime
const result = await timerStore.getState();
expect(result).toEqual({
reps: 3,
workTime: 40,
restTime: 20
});
expect(mockedAsyncStorage.getItem).toHaveBeenCalledWith('timer_reps');
expect(mockedAsyncStorage.getItem).toHaveBeenCalledWith('timer_work_time');
expect(mockedAsyncStorage.getItem).toHaveBeenCalledWith('timer_rest_time');
});
test('should handle errors when retrieving state', async () => {
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
mockedAsyncStorage.getItem.mockRejectedValueOnce(new Error('AsyncStorage Error'));
const result = await timerStore.getState();
expect(result).toEqual({
reps: 1,
workTime: 0,
restTime: 0
}); // Default values
expect(consoleSpy).toHaveBeenCalledWith(
'Error retrieving repetitions:',
expect.any(Error)
);
consoleSpy.mockRestore();
});
});
});

View File

@ -34,7 +34,7 @@ describe('[Component] Card', () => {
const { getByTestId } = renderComponent({ backgroundColor: 'green'});
const card = getByTestId('card');
expect(card?.props.style[0].backgroundColor).toEqual(theme.colors.green);
expect(card?.props.style[0].backgroundColor).toEqual(theme.colors.fixed.green);
});
test('does not render as TouchableOpacity when onPress is not provided', () => {
@ -56,5 +56,3 @@ describe('[Component] Card', () => {
expect(onPressMock).toHaveBeenCalled();
});
});

View File

@ -49,7 +49,7 @@ exports[`[Component] Button Disabled state renders correctly 1`] = `
style={
[
{
"color": "#000",
"color": "#000000",
},
[
{
@ -116,7 +116,7 @@ exports[`[Component] Button Loading state renders correctly 1`] = `
style={
[
{
"color": "#000",
"color": "#000000",
},
[
{
@ -183,7 +183,7 @@ exports[`[Component] Button Ready state renders correctly 1`] = `
style={
[
{
"color": "#000",
"color": "#000000",
},
[
{

View File

@ -5,7 +5,7 @@ exports[`[Component] NumberSelect renders correctly 1`] = `
style={
[
{
"backgroundColor": "#fff",
"backgroundColor": "#FFFFFF",
},
[
{
@ -22,7 +22,7 @@ exports[`[Component] NumberSelect renders correctly 1`] = `
style={
[
{
"color": "#000",
"color": "#000000",
},
[
{
@ -54,7 +54,7 @@ exports[`[Component] NumberSelect renders correctly 1`] = `
style={
[
{
"color": "#000",
"color": "#000000",
},
[
{
@ -87,7 +87,7 @@ exports[`[Component] NumberSelect renders correctly 1`] = `
style={
[
{
"color": "#000",
"color": "#000000",
},
[
{

View File

@ -1,7 +1,7 @@
import React from 'react';
import { Text, View } from '@/components/shared/Themed';
import styled from '@emotion/native';
import { useRouter } from 'expo-router';
import { useNavigation } from '@react-navigation/native';
import Button from '@/components/shared/Button';
import { HorizontalSpacer, VerticalSpacer } from '@/components/shared/Spacers';
@ -18,10 +18,10 @@ export default function FinishContent({
handleStart,
handleReset
}: FinishContentProps) {
const router = useRouter();
const navigation = useNavigation();
const handleBackClick = () => {
router.push('/');
navigation.goBack();
handleReset();
};

View File

@ -5,7 +5,7 @@ import { formatTime } from '@/components/shared/business/timeHelpers';
import Button from '@/components/shared/Button';
import { ElasticSpacer, VerticalSpacer } from '@/components/shared/Spacers';
import { TimerBgColor } from '@/components/useCases/timer/business/type';
import { useRouter } from 'expo-router';
import { useNavigation } from '@react-navigation/native';
import { i18n } from '@/app/i18n/i18n';
interface TimerContentProps {
@ -37,10 +37,10 @@ export default function TimerContent({
previousRep,
bgColor,
}: TimerContentProps ) {
const router = useRouter();
const navigation = useNavigation();
const handleBackClick = () => {
router.push('/');
navigation.goBack();
handleReset();
};

43
package-lock.json generated
View File

@ -44,6 +44,7 @@
"@babel/core": "^7.20.0",
"@testing-library/jest-native": "^5.4.3",
"@testing-library/react-native": "^13.2.0",
"@types/jest": "^29.5.14",
"@types/react": "~18.2.45",
"@types/react-native-background-timer": "^2.0.2",
"@types/react-native-push-notification": "^8.1.4",
@ -8059,6 +8060,48 @@
"@types/istanbul-lib-report": "*"
}
},
"node_modules/@types/jest": {
"version": "29.5.14",
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz",
"integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==",
"dev": true,
"dependencies": {
"expect": "^29.0.0",
"pretty-format": "^29.0.0"
}
},
"node_modules/@types/jest/node_modules/ansi-styles": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/@types/jest/node_modules/pretty-format": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
"integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==",
"dev": true,
"dependencies": {
"@jest/schemas": "^29.6.3",
"ansi-styles": "^5.0.0",
"react-is": "^18.0.0"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/@types/jest/node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
"dev": true
},
"node_modules/@types/jsdom": {
"version": "20.0.1",
"resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz",

View File

@ -61,6 +61,7 @@
"@babel/core": "^7.20.0",
"@testing-library/jest-native": "^5.4.3",
"@testing-library/react-native": "^13.2.0",
"@types/jest": "^29.5.14",
"@types/react": "~18.2.45",
"@types/react-native-background-timer": "^2.0.2",
"@types/react-native-push-notification": "^8.1.4",