diff --git a/apps/mobile/src/components/PickerSchedule.tsx b/apps/mobile/src/components/PickerSchedule.tsx
index 5f72e01..20fc354 100644
--- a/apps/mobile/src/components/PickerSchedule.tsx
+++ b/apps/mobile/src/components/PickerSchedule.tsx
@@ -3,16 +3,12 @@ import { ScrollView, Text, TouchableOpacity, View } from 'react-native'
import { styles } from '../styles'
-export function PickerSchedule({
- left,
- middle,
- right,
-}: {
+export type PickerScheduleProps = {
left: {
disabled?: boolean,
- onChange: (value: typeof left.options[number]['value']) => void,
+ onChange: (value: string) => void,
options: { label: string, value: string }[],
- value: typeof left.options[number]['value'],
+ value: string,
},
middle: {
disabled?: boolean,
@@ -24,7 +20,13 @@ export function PickerSchedule({
onChange: (value: 'day' | 'week' | 'year') => void,
value: 'day' | 'week' | 'year',
},
-}) {
+}
+
+export function PickerSchedule({
+ left,
+ middle,
+ right,
+}: PickerScheduleProps) {
const justLeft = middle.disabled && right.disabled
return (
diff --git a/apps/mobile/src/components/__tests__/PickerSchedule.test.tsx b/apps/mobile/src/components/__tests__/PickerSchedule.test.tsx
new file mode 100644
index 0000000..153fbcf
--- /dev/null
+++ b/apps/mobile/src/components/__tests__/PickerSchedule.test.tsx
@@ -0,0 +1,71 @@
+import React from 'react'
+import { render, screen } from '@testing-library/react-native'
+
+import { PickerSchedule, type PickerScheduleProps } from '../PickerSchedule'
+
+describe('PickerSchedule', () => {
+ const defaultLeft: PickerScheduleProps['left'] = {
+ options: [{ label: 'Option 1', value: 'option1' }],
+ value: 'option1',
+ onChange: jest.fn(),
+ }
+
+ const defaultRight: PickerScheduleProps['right'] = {
+ value: 'day',
+ onChange: jest.fn(),
+ }
+
+ describe('Right-most labels', () => {
+ it('shows singular labels (day, week, year) when middle selection is 1', () => {
+ render(
+
+ )
+
+ expect(screen.getByText('day')).toBeTruthy()
+ expect(screen.getByText('week')).toBeTruthy()
+ expect(screen.getByText('year')).toBeTruthy()
+
+ // Ensure plural forms are not present
+ expect(screen.queryByText('days')).toBeNull()
+ expect(screen.queryByText('weeks')).toBeNull()
+ expect(screen.queryByText('years')).toBeNull()
+ })
+
+ it('shows plural labels (days, weeks, years) when middle selection is not 1', () => {
+ render(
+
+ )
+
+ expect(screen.getByText('days')).toBeTruthy()
+ expect(screen.getByText('weeks')).toBeTruthy()
+ expect(screen.getByText('years')).toBeTruthy()
+
+ // Ensure singular forms are not present
+ expect(screen.queryByText('day')).toBeNull()
+ expect(screen.queryByText('week')).toBeNull()
+ expect(screen.queryByText('year')).toBeNull()
+ })
+
+ it('shows plural labels when middle selection is a larger number', () => {
+ render(
+
+ )
+
+ expect(screen.getByText('days')).toBeTruthy()
+ expect(screen.getByText('weeks')).toBeTruthy()
+ expect(screen.getByText('years')).toBeTruthy()
+ })
+ })
+})
diff --git a/apps/mobile/src/components/__tests__/SnoozeChoreModal.test.tsx b/apps/mobile/src/components/__tests__/SnoozeChoreModal.test.tsx
new file mode 100644
index 0000000..6357a67
--- /dev/null
+++ b/apps/mobile/src/components/__tests__/SnoozeChoreModal.test.tsx
@@ -0,0 +1,323 @@
+import React from 'react'
+import { render, screen, fireEvent } from '@testing-library/react-native'
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+
+import type { IChore } from '@plannting/api/src/models/Chore'
+
+import { SnoozeChoreModal } from '../SnoozeChoreModal'
+
+type TestChore = Pick
+
+// Mock tRPC
+jest.mock('../../trpc', () => ({
+ trpc: {
+ choreLogs: {
+ create: {
+ useMutation: jest.fn(),
+ },
+ },
+ },
+}))
+
+function getUseMutationMock(): jest.Mock {
+ return (jest.requireMock('../../trpc').trpc.choreLogs.create.useMutation)
+}
+
+describe('SnoozeChoreModal', () => {
+ let queryClient: QueryClient
+ let mockMutate: jest.Mock
+ let mockOnClose: jest.Mock
+ let mockOnSuccess: jest.Mock
+
+ beforeEach(() => {
+ queryClient = new QueryClient({
+ defaultOptions: {
+ queries: { retry: false },
+ mutations: { retry: false },
+ },
+ })
+
+ mockMutate = jest.fn()
+ mockOnClose = jest.fn()
+ mockOnSuccess = jest.fn()
+
+ getUseMutationMock().mockReturnValue({
+ mutate: mockMutate,
+ isPending: false,
+ error: null,
+ })
+ })
+
+ afterEach(() => {
+ jest.clearAllMocks()
+ })
+
+ const renderComponent = (chore: TestChore | null) => {
+ return render(
+
+
+
+ )
+ }
+
+ describe('Skip option', () => {
+ it('snoozes for recurAmount days when recurUnit is "day"', () => {
+ const baseDate = new Date('2024-01-15T10:00:00Z')
+ jest.useFakeTimers()
+ jest.setSystemTime(baseDate)
+
+ const chore: TestChore = {
+ _id: 'chore-1',
+ recurAmount: 3,
+ recurUnit: 'day',
+ }
+
+ renderComponent(chore)
+
+ // Find and press the "Save" button
+ const saveButton = screen.getByText('Save')
+ fireEvent.press(saveButton)
+
+ // Verify the mutation was called
+ expect(mockMutate).toHaveBeenCalledTimes(1)
+
+ const mutationCall = mockMutate.mock.calls[0][0]
+ expect(mutationCall.choreId).toBe('chore-1')
+ expect(mutationCall.action).toBe('snoozed')
+
+ // Calculate expected date: today + (3 * 1) days
+ const expectedDate = new Date(baseDate)
+ expectedDate.setDate(expectedDate.getDate() + 3)
+ expectedDate.setHours(0, 0, 0, 0)
+
+ const actualDate = new Date(mutationCall.snoozeUntil)
+ actualDate.setHours(0, 0, 0, 0)
+
+ expect(actualDate.getTime()).toBe(expectedDate.getTime())
+
+ jest.useRealTimers()
+ })
+
+ it('snoozes for recurAmount * 7 days when recurUnit is "week"', () => {
+ const baseDate = new Date('2024-01-15T10:00:00Z')
+ jest.useFakeTimers()
+ jest.setSystemTime(baseDate)
+
+ const chore: TestChore = {
+ _id: 'chore-2',
+ recurAmount: 2,
+ recurUnit: 'week',
+ }
+
+ renderComponent(chore)
+
+ const saveButton = screen.getByText('Save')
+ fireEvent.press(saveButton)
+
+ expect(mockMutate).toHaveBeenCalledTimes(1)
+
+ const mutationCall = mockMutate.mock.calls[0][0]
+ expect(mutationCall.choreId).toBe('chore-2')
+
+ // Calculate expected date: today + (2 * 7) days = 14 days
+ const expectedDate = new Date(baseDate)
+ expectedDate.setDate(expectedDate.getDate() + 14)
+ expectedDate.setHours(0, 0, 0, 0)
+
+ const actualDate = new Date(mutationCall.snoozeUntil)
+ actualDate.setHours(0, 0, 0, 0)
+
+ expect(actualDate.getTime()).toBe(expectedDate.getTime())
+
+ jest.useRealTimers()
+ })
+
+ it('snoozes for recurAmount * 365 days when recurUnit is "year"', () => {
+ const baseDate = new Date('2024-01-15T10:00:00Z')
+ jest.useFakeTimers()
+ jest.setSystemTime(baseDate)
+
+ const chore: TestChore = {
+ _id: 'chore-3',
+ recurAmount: 1,
+ recurUnit: 'year',
+ }
+
+ renderComponent(chore)
+
+ const saveButton = screen.getByText('Save')
+ fireEvent.press(saveButton)
+
+ expect(mockMutate).toHaveBeenCalledTimes(1)
+
+ const mutationCall = mockMutate.mock.calls[0][0]
+ expect(mutationCall.choreId).toBe('chore-3')
+
+ // Calculate expected date: today + (1 * 365) days = 365 days
+ const expectedDate = new Date(baseDate)
+ expectedDate.setDate(expectedDate.getDate() + 365)
+ expectedDate.setHours(0, 0, 0, 0)
+
+ const actualDate = new Date(mutationCall.snoozeUntil)
+ actualDate.setHours(0, 0, 0, 0)
+
+ expect(actualDate.getTime()).toBe(expectedDate.getTime())
+
+ jest.useRealTimers()
+ })
+ })
+
+ describe('Snooze option', () => {
+ beforeEach(() => {
+ // Set up the component with "snooze" option selected
+ // We'll need to interact with the PickerSchedule to change the option
+ })
+
+ it('snoozes for snoozeAmount days when snoozeUnit is "day"', () => {
+ const baseDate = new Date('2024-01-15T10:00:00Z')
+ jest.useFakeTimers()
+ jest.setSystemTime(baseDate)
+
+ const chore: TestChore = {
+ _id: 'chore-4',
+ recurAmount: 7,
+ recurUnit: 'day',
+ }
+
+ renderComponent(chore)
+
+ // Change option to "snooze" - find the option by text
+ const snoozeOption = screen.getByText('Snooze for')
+ fireEvent.press(snoozeOption)
+
+ // Set snooze amount to 5
+ const amount5 = screen.getByText('5')
+ fireEvent.press(amount5)
+
+ // Unit should default to "day", but let's ensure it's set
+ // When amount is 5, it will show "days" (plural)
+ const dayUnit = screen.getByText('days')
+ fireEvent.press(dayUnit)
+
+ // Submit
+ const saveButton = screen.getByText('Save')
+ fireEvent.press(saveButton)
+
+ expect(mockMutate).toHaveBeenCalledTimes(1)
+
+ const mutationCall = mockMutate.mock.calls[0][0]
+ expect(mutationCall.choreId).toBe('chore-4')
+
+ // Calculate expected date: today + (5 * 1) days = 5 days
+ const expectedDate = new Date(baseDate)
+ expectedDate.setDate(expectedDate.getDate() + 5)
+ expectedDate.setHours(0, 0, 0, 0)
+
+ const actualDate = new Date(mutationCall.snoozeUntil)
+ actualDate.setHours(0, 0, 0, 0)
+
+ expect(actualDate.getTime()).toBe(expectedDate.getTime())
+
+ jest.useRealTimers()
+ })
+
+ it('snoozes for snoozeAmount * 7 days when snoozeUnit is "week"', () => {
+ const baseDate = new Date('2024-01-15T10:00:00Z')
+ jest.useFakeTimers()
+ jest.setSystemTime(baseDate)
+
+ const chore: TestChore = {
+ _id: 'chore-5',
+ recurAmount: 7,
+ recurUnit: 'day',
+ }
+
+ renderComponent(chore)
+
+ // Change option to "snooze"
+ const snoozeOption = screen.getByText('Snooze for')
+ fireEvent.press(snoozeOption)
+
+ // Set snooze amount to 3
+ const amount3 = screen.getByText('3')
+ fireEvent.press(amount3)
+
+ // Set unit to "week" - when amount is 3, it shows "weeks" (plural)
+ const weekUnit = screen.getByText('weeks')
+ fireEvent.press(weekUnit)
+
+ // Submit
+ const saveButton = screen.getByText('Save')
+ fireEvent.press(saveButton)
+
+ expect(mockMutate).toHaveBeenCalledTimes(1)
+
+ const mutationCall = mockMutate.mock.calls[0][0]
+ expect(mutationCall.choreId).toBe('chore-5')
+
+ // Calculate expected date: today + (3 * 7) days = 21 days
+ const expectedDate = new Date(baseDate)
+ expectedDate.setDate(expectedDate.getDate() + 21)
+ expectedDate.setHours(0, 0, 0, 0)
+
+ const actualDate = new Date(mutationCall.snoozeUntil)
+ actualDate.setHours(0, 0, 0, 0)
+
+ expect(actualDate.getTime()).toBe(expectedDate.getTime())
+
+ jest.useRealTimers()
+ })
+
+ it('snoozes for snoozeAmount * 365 days when snoozeUnit is "year"', () => {
+ const baseDate = new Date('2024-01-15T10:00:00Z')
+ jest.useFakeTimers()
+ jest.setSystemTime(baseDate)
+
+ const chore: TestChore = {
+ _id: 'chore-6',
+ recurAmount: 7,
+ recurUnit: 'day',
+ }
+
+ renderComponent(chore)
+
+ // Change option to "snooze"
+ const snoozeOption = screen.getByText('Snooze for')
+ fireEvent.press(snoozeOption)
+
+ // Set snooze amount to 2
+ const amount2 = screen.getByText('2')
+ fireEvent.press(amount2)
+
+ // Set unit to "year" - when amount is 2, it shows "years" (plural)
+ const yearUnit = screen.getByText('years')
+ fireEvent.press(yearUnit)
+
+ // Submit
+ const saveButton = screen.getByText('Save')
+ fireEvent.press(saveButton)
+
+ expect(mockMutate).toHaveBeenCalledTimes(1)
+
+ const mutationCall = mockMutate.mock.calls[0][0]
+ expect(mutationCall.choreId).toBe('chore-6')
+
+ // Calculate expected date: today + (2 * 365) days = 730 days
+ const expectedDate = new Date(baseDate)
+ expectedDate.setDate(expectedDate.getDate() + 730)
+ expectedDate.setHours(0, 0, 0, 0)
+
+ const actualDate = new Date(mutationCall.snoozeUntil)
+ actualDate.setHours(0, 0, 0, 0)
+
+ expect(actualDate.getTime()).toBe(expectedDate.getTime())
+
+ jest.useRealTimers()
+ })
+ })
+})