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() + }) + }) +})