From 8ba38c3a4a16799e75133393ef96da41bace0cd9 Mon Sep 17 00:00:00 2001 From: "Kevin Brian Bader (via MelvinBot)" Date: Tue, 4 Aug 2026 18:07:18 +0000 Subject: [PATCH 1/6] Fix: return focus/keyboard to description input on iOS after canceling discard modal Co-authored-by: Kevin Brian Bader --- src/pages/iou/request/step/IOURequestStepDescription.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/iou/request/step/IOURequestStepDescription.tsx b/src/pages/iou/request/step/IOURequestStepDescription.tsx index 747032e67f5e..a72bbae2bf0d 100644 --- a/src/pages/iou/request/step/IOURequestStepDescription.tsx +++ b/src/pages/iou/request/step/IOURequestStepDescription.tsx @@ -176,7 +176,7 @@ function IOURequestStepDescription({ useDiscardChangesConfirmation({ onCancel: () => { - focusComposerWithDelay(inputRef.current)(true); + focusComposerWithDelay(inputRef.current)(true, undefined, true); }, getHasUnsavedChanges: () => { if (isSaved) { From 9d7aa47c19fe4c3c41802de52e32c7efb5be6800 Mon Sep 17 00:00:00 2001 From: "Kevin Brian Bader (via MelvinBot)" Date: Tue, 4 Aug 2026 23:38:21 +0000 Subject: [PATCH 2/6] Add test pinning focusComposerWithDelay args in Description discard-modal onCancel Co-authored-by: Kevin Brian Bader --- tests/ui/IOURequestStepDescriptionTest.tsx | 101 +++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 tests/ui/IOURequestStepDescriptionTest.tsx diff --git a/tests/ui/IOURequestStepDescriptionTest.tsx b/tests/ui/IOURequestStepDescriptionTest.tsx new file mode 100644 index 000000000000..d95d84196800 --- /dev/null +++ b/tests/ui/IOURequestStepDescriptionTest.tsx @@ -0,0 +1,101 @@ +import {act, render} from '@testing-library/react-native'; + +import React from 'react'; + +import IOURequestStepDescription from '@pages/iou/request/step/IOURequestStepDescription'; + +import CONST from '@src/CONST'; +import SCREENS from '@src/SCREENS'; + +import createMock from '../utils/createMock'; +import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; + +/** + * These tests pin the exact arguments passed to `focusComposerWithDelay` from the discard-modal `onCancel` handler + * in `IOURequestStepDescription`. The fix that requires them (returning focus AND re-requesting the soft keyboard on + * iOS after canceling the "Discard changes?" modal) cannot be verified manually in dev/simulator, so this assertion + * is the regression net: it fails loudly if the third `forceKeyboardIfAlreadyFocused` argument is ever trimmed. + */ + +// Capture the inner focus function so we can assert the exact args the component invokes it with. +const mockFocusFn = jest.fn(); +const mockFocusComposerFactory = jest.fn(() => mockFocusFn); +jest.mock('@libs/focusComposerWithDelay', () => ({ + __esModule: true, + default: (...factoryArgs: unknown[]) => mockFocusComposerFactory(...factoryArgs), +})); + +// Capture the `onCancel` the component wires into the discard-changes hook, without dragging in the real +// navigation/modal machinery (that flow is covered by tests/unit/hooks/useDiscardChangesConfirmationNative.test.ts). +let capturedOnCancel: (() => void) | undefined; +jest.mock('@hooks/useDiscardChangesConfirmation', () => ({ + __esModule: true, + default: (options: {onCancel?: () => void}) => { + capturedOnCancel = options.onCancel; + return {suppressDiscardPrompt: jest.fn()}; + }, +})); + +// The "OrNotFound" HOCs gate rendering on Onyx report/transaction data that is irrelevant to this wiring test. +// Stub them to pass-through so the real component body runs directly. +jest.mock('@pages/iou/request/step/withWritableReportOrNotFound', () => (Component: React.ComponentType) => Component); +jest.mock('@pages/iou/request/step/withFullTransactionOrNotFound', () => (Component: React.ComponentType) => Component); + +// The JSX children are irrelevant here — the `onCancel` closure is created in the component body before render. +// Stubbing them keeps the render trivial and stable. +jest.mock('@pages/iou/request/step/StepScreenWrapper', () => () => null); +jest.mock('@components/Form/FormProvider', () => () => null); +jest.mock('@components/Form/InputWrapper', () => () => null); + +jest.mock('@hooks/useThemeStyles', () => ({ + __esModule: true, + default: () => ({}), +})); +jest.mock('@hooks/useLocalize', () => ({ + __esModule: true, + default: () => ({translate: (key: string) => key}), +})); +jest.mock('@hooks/useAutoFocusInput', () => ({ + __esModule: true, + default: () => ({inputCallbackRef: jest.fn(), inputRef: {current: null}}), +})); + +const ROUTE = createMock['route']>({ + key: 'Money_Request_Step_Description-test', + name: SCREENS.MONEY_REQUEST.STEP_DESCRIPTION, + params: { + action: CONST.IOU.ACTION.CREATE, + iouType: CONST.IOU.TYPE.SUBMIT, + reportID: 'report-1', + transactionID: 'txn-1', + }, +}); +const NAVIGATION = createMock['navigation']>({}); + +describe('IOURequestStepDescription - discard modal onCancel', () => { + beforeEach(() => { + jest.clearAllMocks(); + capturedOnCancel = undefined; + }); + + it('re-requests the soft keyboard on cancel by forcing focus even when the input is still focused', async () => { + render( + , + ); + // Let the component's useOnyx subscriptions settle so their updates don't fire outside act(). + await waitForBatchedUpdatesWithAct(); + + // The component must have wired an onCancel handler into the discard-changes hook. + expect(capturedOnCancel).toBeDefined(); + + act(() => capturedOnCancel?.()); + + // Pins the exact PR change: shouldDelay=true, no forced selection range, forceKeyboardIfAlreadyFocused=true. + expect(mockFocusFn).toHaveBeenCalledWith(true, undefined, true); + }); +}); From 23a4845148f48c201b3041ef75b67325be7f35b3 Mon Sep 17 00:00:00 2001 From: "Kevin Brian Bader (via MelvinBot)" Date: Tue, 4 Aug 2026 23:42:50 +0000 Subject: [PATCH 3/6] Fix Oxfmt: reorder imports in IOURequestStepDescriptionTest Co-authored-by: Kevin Brian Bader --- tests/ui/IOURequestStepDescriptionTest.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/ui/IOURequestStepDescriptionTest.tsx b/tests/ui/IOURequestStepDescriptionTest.tsx index d95d84196800..a38fe7976ea0 100644 --- a/tests/ui/IOURequestStepDescriptionTest.tsx +++ b/tests/ui/IOURequestStepDescriptionTest.tsx @@ -1,12 +1,12 @@ import {act, render} from '@testing-library/react-native'; -import React from 'react'; - import IOURequestStepDescription from '@pages/iou/request/step/IOURequestStepDescription'; import CONST from '@src/CONST'; import SCREENS from '@src/SCREENS'; +import React from 'react'; + import createMock from '../utils/createMock'; import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; From 2640b9f4bba66c3fa66e8089d5fe91ce3a3bcf2b Mon Sep 17 00:00:00 2001 From: "Kevin Brian Bader (via MelvinBot)" Date: Wed, 5 Aug 2026 00:01:29 +0000 Subject: [PATCH 4/6] Fix typecheck: correct mock factory signature and drop HOC-provided props in IOURequestStepDescription test Co-authored-by: Kevin Brian Bader --- tests/ui/IOURequestStepDescriptionTest.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/ui/IOURequestStepDescriptionTest.tsx b/tests/ui/IOURequestStepDescriptionTest.tsx index a38fe7976ea0..13542de64bd7 100644 --- a/tests/ui/IOURequestStepDescriptionTest.tsx +++ b/tests/ui/IOURequestStepDescriptionTest.tsx @@ -19,7 +19,7 @@ import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct' // Capture the inner focus function so we can assert the exact args the component invokes it with. const mockFocusFn = jest.fn(); -const mockFocusComposerFactory = jest.fn(() => mockFocusFn); +const mockFocusComposerFactory = jest.fn((..._factoryArgs: unknown[]) => mockFocusFn); jest.mock('@libs/focusComposerWithDelay', () => ({ __esModule: true, default: (...factoryArgs: unknown[]) => mockFocusComposerFactory(...factoryArgs), @@ -83,8 +83,6 @@ describe('IOURequestStepDescription - discard modal onCancel', () => { , ); // Let the component's useOnyx subscriptions settle so their updates don't fire outside act(). From 095b1f34729530d159de6e8cc303d936f045cc64 Mon Sep 17 00:00:00 2001 From: "Kevin Brian Bader (via MelvinBot)" Date: Wed, 5 Aug 2026 00:21:21 +0000 Subject: [PATCH 5/6] Fix ESLint: remove unused rest param from focusComposer mock factory Co-authored-by: Kevin Brian Bader --- tests/ui/IOURequestStepDescriptionTest.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ui/IOURequestStepDescriptionTest.tsx b/tests/ui/IOURequestStepDescriptionTest.tsx index 13542de64bd7..117b3a6f5112 100644 --- a/tests/ui/IOURequestStepDescriptionTest.tsx +++ b/tests/ui/IOURequestStepDescriptionTest.tsx @@ -19,7 +19,7 @@ import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct' // Capture the inner focus function so we can assert the exact args the component invokes it with. const mockFocusFn = jest.fn(); -const mockFocusComposerFactory = jest.fn((..._factoryArgs: unknown[]) => mockFocusFn); +const mockFocusComposerFactory = jest.fn(() => mockFocusFn); jest.mock('@libs/focusComposerWithDelay', () => ({ __esModule: true, default: (...factoryArgs: unknown[]) => mockFocusComposerFactory(...factoryArgs), From 5fa7db0cf9ba53ddf159b271a0bc2fbe98e01a0d Mon Sep 17 00:00:00 2001 From: "Kevin Brian Bader (via MelvinBot)" Date: Fri, 7 Aug 2026 20:45:46 +0000 Subject: [PATCH 6/6] Fix TS2556 in IOURequestStepDescription test by simplifying focusComposerWithDelay mock Co-authored-by: Kevin Brian Bader --- tests/ui/IOURequestStepDescriptionTest.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/ui/IOURequestStepDescriptionTest.tsx b/tests/ui/IOURequestStepDescriptionTest.tsx index 117b3a6f5112..87ae97309cb4 100644 --- a/tests/ui/IOURequestStepDescriptionTest.tsx +++ b/tests/ui/IOURequestStepDescriptionTest.tsx @@ -19,10 +19,9 @@ import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct' // Capture the inner focus function so we can assert the exact args the component invokes it with. const mockFocusFn = jest.fn(); -const mockFocusComposerFactory = jest.fn(() => mockFocusFn); jest.mock('@libs/focusComposerWithDelay', () => ({ __esModule: true, - default: (...factoryArgs: unknown[]) => mockFocusComposerFactory(...factoryArgs), + default: () => mockFocusFn, })); // Capture the `onCancel` the component wires into the discard-changes hook, without dragging in the real