diff --git a/src/components/ChronosTimerHeaderButton.tsx b/src/components/ChronosTimerHeaderButton.tsx index c9b030639df2..d09d35268a50 100644 --- a/src/components/ChronosTimerHeaderButton.tsx +++ b/src/components/ChronosTimerHeaderButton.tsx @@ -68,6 +68,7 @@ function ChronosTimerHeaderButton({report}: ChronosTimerHeaderButtonProps) { { value: 'timer' as const, text: translate(isTimerRunning ? 'chronos.stopTimer' : 'chronos.startTimer'), + onSelected: () => callFunctionIfActionIsAllowed(sendCommentToChronos)(), }, { value: 'scheduleOOO' as const, diff --git a/tests/ui/ChronosTimerHeaderButton.test.tsx b/tests/ui/ChronosTimerHeaderButton.test.tsx new file mode 100644 index 000000000000..ab294efe962b --- /dev/null +++ b/tests/ui/ChronosTimerHeaderButton.test.tsx @@ -0,0 +1,194 @@ +/* eslint-disable @typescript-eslint/naming-convention -- Mock module paths use non-standard naming conventions required by jest.mock */ +import {PortalProvider} from '@gorhom/portal'; +import {fireEvent, render, screen, waitFor} from '@testing-library/react-native'; +import React from 'react'; +import Onyx from 'react-native-onyx'; +import ChronosTimerHeaderButton from '@components/ChronosTimerHeaderButton'; +import ComposeProviders from '@components/ComposeProviders'; +import {LocaleContextProvider} from '@components/LocaleContextProvider'; +import OnyxListItemProvider from '@components/OnyxListItemProvider'; +import type {PopoverMenuItem, PopoverMenuProps} from '@components/PopoverMenu'; +import CONST from '@src/CONST'; +import type {Report} from '@src/types/onyx'; +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; + +const mockAddComment = jest.fn(); +jest.mock('@userActions/Report', () => ({ + addComment: (...args: unknown[]): void => { + mockAddComment(...args); + }, +})); + +const mockNavigate = jest.fn(); +jest.mock('@libs/Navigation/Navigation', () => ({ + navigate: (...args: unknown[]): void => { + mockNavigate(...args); + }, + isTopmostRouteModalScreen: () => false, +})); + +jest.mock('@userActions/Session', () => ({ + callFunctionIfActionIsAllowed: (fn: () => void) => fn, +})); + +jest.mock('@libs/ReportUtils', () => ({ + canUserPerformWriteAction: () => true, + canWriteInReport: () => true, + getAncestors: () => [], +})); + +jest.mock('@libs/ChronosUtils', () => ({ + isChronosTimerRunningFromVisibleActions: () => false, +})); + +jest.mock('@libs/ReportActionsUtils', () => ({ + getSortedReportActionsForDisplay: () => [], +})); + +jest.mock('@hooks/useIsInSidePanel', () => ({ + __esModule: true, + default: () => false, +})); + +jest.mock('@hooks/useReportIsArchived', () => ({ + __esModule: true, + default: () => false, +})); + +jest.mock('@hooks/useCurrentUserPersonalDetails', () => ({ + __esModule: true, + default: () => ({accountID: 12345, timezone: {automatic: true, selected: 'America/Los_Angeles'}}), +})); + +/** + * A simplified PopoverMenu that renders menu items as pressable elements + * and directly invokes onSelected, bypassing the modal animation lifecycle. + */ +jest.mock('@components/PopoverMenu', () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- jest.requireActual returns an untyped module object + const {View, Text, TouchableOpacity} = jest.requireActual('react-native'); + function MockPopoverMenu({isVisible, menuItems, onClose, onItemSelected}: PopoverMenuProps) { + if (!isVisible) { + return null; + } + return ( + + {menuItems.map((item: PopoverMenuItem, index: number) => ( + { + onItemSelected?.(item, index); + item.onSelected?.(); + onClose(); + }} + > + {item.text} + + ))} + + ); + } + MockPopoverMenu.displayName = 'PopoverMenu'; + return MockPopoverMenu; +}); + +const TEST_REPORT_ID = '123456789'; + +const mockReport: Report = { + reportID: TEST_REPORT_ID, + reportName: 'Chronos Test', + type: CONST.REPORT.TYPE.CHAT, +} as Report; + +/** + * Finds the dropdown arrow button (the split button with expanded accessibility state). + */ +function getDropdownArrowButton() { + const buttons = screen.getAllByRole('button'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const dropdownButton = buttons.find((btn) => btn.props.accessibilityState?.expanded !== undefined); + if (!dropdownButton) { + throw new Error('Could not find dropdown arrow button'); + } + return dropdownButton; +} + +function renderComponent() { + return render( + + + , + ); +} + +describe('ChronosTimerHeaderButton', () => { + beforeEach(async () => { + await Onyx.clear(); + await waitForBatchedUpdates(); + jest.clearAllMocks(); + }); + + it('should send a start timer command when the main button is pressed', async () => { + // Given a rendered ChronosTimerHeaderButton + renderComponent(); + await waitForBatchedUpdates(); + + // When the main "Start Timer" button is pressed + fireEvent.press(screen.getByText('Start Timer')); + await waitForBatchedUpdates(); + + // Then addComment is called with the start timer command + expect(mockAddComment).toHaveBeenCalledTimes(1); + expect(mockAddComment).toHaveBeenCalledWith( + expect.objectContaining({ + report: mockReport, + text: CONST.CHRONOS.TIMER_COMMAND.START, + }), + ); + }); + + it('should send a start timer command when Start Timer is selected from the dropdown menu', async () => { + // Given a rendered ChronosTimerHeaderButton + renderComponent(); + await waitForBatchedUpdates(); + + // When the dropdown is opened and "Start Timer" is selected + fireEvent.press(getDropdownArrowButton()); + await waitForBatchedUpdates(); + await waitFor(() => { + expect(screen.getByTestId('PopoverMenuItem-Start Timer')).toBeOnTheScreen(); + }); + fireEvent.press(screen.getByTestId('PopoverMenuItem-Start Timer')); + await waitForBatchedUpdates(); + + // Then addComment is called with the start timer command + expect(mockAddComment).toHaveBeenCalledTimes(1); + expect(mockAddComment).toHaveBeenCalledWith( + expect.objectContaining({ + report: mockReport, + text: CONST.CHRONOS.TIMER_COMMAND.START, + }), + ); + }); + + it('should navigate to Schedule OOO when the option is selected from the dropdown menu', async () => { + // Given a rendered ChronosTimerHeaderButton + renderComponent(); + await waitForBatchedUpdates(); + + // When the dropdown is opened and "Schedule OOO" is selected + fireEvent.press(getDropdownArrowButton()); + await waitForBatchedUpdates(); + await waitFor(() => { + expect(screen.getByTestId('PopoverMenuItem-Schedule OOO')).toBeOnTheScreen(); + }); + fireEvent.press(screen.getByTestId('PopoverMenuItem-Schedule OOO')); + await waitForBatchedUpdates(); + + // Then Navigation.navigate is called with the Schedule OOO route + expect(mockNavigate).toHaveBeenCalledTimes(1); + expect(mockNavigate).toHaveBeenCalledWith(`r/${TEST_REPORT_ID}/chronos/schedule-ooo`); + }); +});