From fcc94f370232f317896023e494c61b0cd777f626 Mon Sep 17 00:00:00 2001 From: MelvinBot Date: Thu, 26 Feb 2026 19:17:17 +0000 Subject: [PATCH 1/7] Fix 'Not Found' page flash after deleting expense The deleteTransactionNavigateBackUrl clearing effect in DeleteTransactionNavigateBackHandler had an inverted focus condition compared to SearchMoneyRequestReportPage. It cleared the guard URL while the screen was still focused, allowing the Not Found page to briefly flash before navigation completed. Changed the condition to only clear after navigating away, matching the established pattern. Co-authored-by: Krishna --- src/pages/inbox/DeleteTransactionNavigateBackHandler.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/inbox/DeleteTransactionNavigateBackHandler.tsx b/src/pages/inbox/DeleteTransactionNavigateBackHandler.tsx index 3391067b22123..d5cfc415ca2a1 100644 --- a/src/pages/inbox/DeleteTransactionNavigateBackHandler.tsx +++ b/src/pages/inbox/DeleteTransactionNavigateBackHandler.tsx @@ -9,20 +9,20 @@ import ONYXKEYS from '@src/ONYXKEYS'; /** * Component that does not render anything but isolates the NVP_DELETE_TRANSACTION_NAVIGATE_BACK_URL * subscription from ReportScreen. Clears the URL after interactions complete - * when the report is focused. + * when the report is no longer focused. */ function DeleteTransactionNavigateBackHandler() { const isFocused = useIsFocused(); const [deleteTransactionNavigateBackUrl] = useOnyx(ONYXKEYS.NVP_DELETE_TRANSACTION_NAVIGATE_BACK_URL); useEffect(() => { - if (!isFocused || !deleteTransactionNavigateBackUrl) { + if (isFocused || !deleteTransactionNavigateBackUrl) { return; } if (doesDeleteNavigateBackUrlIncludeDuplicatesReview(deleteTransactionNavigateBackUrl)) { return; } - // Clear the URL after all interactions are processed to ensure all updates are completed before hiding the skeleton + // Clear the URL only after we navigate away to avoid a brief Not Found flash. // eslint-disable-next-line @typescript-eslint/no-deprecated InteractionManager.runAfterInteractions(() => { requestAnimationFrame(() => { From 2d8e6350a4afb9979ec2cbf3206bd346cd82c6e2 Mon Sep 17 00:00:00 2001 From: MelvinBot Date: Fri, 27 Feb 2026 14:05:13 +0000 Subject: [PATCH 2/7] Defer deleteTransaction until RHP close animation completes Wraps deleteTransaction() in InteractionManager.runAfterInteractions() so that Onyx data isn't cleared while the RHP panel is still visibly animating closed. This prevents the withReportOrNotFound HOC from rendering a brief "Not Found" flash on slower devices where the isFocused state hasn't flipped to false yet during the animation. Co-authored-by: Krishna --- src/pages/ReportDetailsPage.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index 9cbda7d61d0bb..5486193bb0873 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -1,6 +1,6 @@ import {StackActions} from '@react-navigation/native'; import React, {useCallback, useEffect, useMemo} from 'react'; -import {View} from 'react-native'; +import {InteractionManager, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import AvatarWithImagePicker from '@components/AvatarWithImagePicker'; @@ -1012,7 +1012,12 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail } Navigation.setNavigationActionToMicrotaskQueue(() => { navigateToTargetUrl(); - deleteTransaction(); + // Delay deletion until the RHP close animation finishes to prevent a brief + // "Not Found" flash inside the animating-out panel on slower devices. + // eslint-disable-next-line @typescript-eslint/no-deprecated + InteractionManager.runAfterInteractions(() => { + deleteTransaction(); + }); }); }, [showConfirmModal, translate, caseID, navigateToTargetUrl, deleteTransaction]); From 30887f2e1a32a8817faccc66e2103c534f49b766 Mon Sep 17 00:00:00 2001 From: MelvinBot Date: Sun, 1 Mar 2026 20:16:37 +0000 Subject: [PATCH 3/7] Fix time-dependent test failures in SearchUIUtilsTest The date preset intersection tests hardcoded January 2026 as the expected "last month" values. Now that it's March 2026, LAST_MONTH resolves to February. Compute expected dates dynamically using date-fns so the tests remain valid regardless of when they run. Co-authored-by: Krishna --- tests/unit/Search/SearchUIUtilsTest.ts | 37 ++++++++++++++++---------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index 2ff44d46bd884..6f93cc1b72fa8 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/naming-convention */ +import {endOfMonth, format, startOfMonth, subMonths} from 'date-fns'; import {renderHook} from '@testing-library/react-native'; import type {OnyxCollection} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; @@ -3127,13 +3128,17 @@ describe('SearchUIUtils', () => { it('should intersect date preset with additional constraints instead of overwriting', () => { // Test that when combining a date preset (EQUAL_TO) with additional constraints, // we intersect the ranges (take max for start, min for end) rather than overwriting - const yearDateRange = DateUtils.getYearDateRange(2026); + const now = new Date(); + const lastMonth = subMonths(now, 1); + const lastMonthStart = format(startOfMonth(lastMonth), 'yyyy-MM-dd'); + const lastMonthEnd = format(endOfMonth(lastMonth), 'yyyy-MM-dd'); + const yearDateRange = DateUtils.getYearDateRange(now.getFullYear()); const dateFilter = { key: CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, filters: [ { operator: CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO, - value: CONST.SEARCH.DATE_PRESETS.LAST_MONTH, // e.g., January 2026: 2026-01-01 to 2026-01-31 + value: CONST.SEARCH.DATE_PRESETS.LAST_MONTH, }, { operator: CONST.SEARCH.SYNTAX_OPERATORS.GREATER_THAN_OR_EQUAL_TO, @@ -3144,38 +3149,42 @@ describe('SearchUIUtils', () => { const result = SearchUIUtils.adjustTimeRangeToDateFilters(yearDateRange, [dateFilter]); - // Should intersect: max(preset start, constraint start) = max(2026-01-01, 2025-04-01) = 2026-01-01 + // Should intersect: max(preset start, constraint start) — preset start wins since it's later // The preset start should be preserved, not overwritten by the earlier constraint - expect(result.start).toBe('2026-01-01'); - // End should remain the preset end (2026-01-31) - expect(result.end).toBe('2026-01-31'); + expect(result.start).toBe(lastMonthStart); + // End should remain the preset end + expect(result.end).toBe(lastMonthEnd); }); it('should intersect date preset end limit with additional constraints', () => { // Test that when combining a date preset with an end constraint, // we take the minimum (earliest) end date to intersect ranges - const yearDateRange = DateUtils.getYearDateRange(2026); + const now = new Date(); + const lastMonth = subMonths(now, 1); + const lastMonthStart = format(startOfMonth(lastMonth), 'yyyy-MM-dd'); + // Use the 15th of last month as the constraint end — earlier than the preset end + const constraintEnd = format(new Date(lastMonth.getFullYear(), lastMonth.getMonth(), 15), 'yyyy-MM-dd'); + const yearDateRange = DateUtils.getYearDateRange(now.getFullYear()); const dateFilter = { key: CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, filters: [ { operator: CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO, - value: CONST.SEARCH.DATE_PRESETS.LAST_MONTH, // e.g., January 2026: 2026-01-01 to 2026-01-31 + value: CONST.SEARCH.DATE_PRESETS.LAST_MONTH, }, { operator: CONST.SEARCH.SYNTAX_OPERATORS.LOWER_THAN_OR_EQUAL_TO, - value: '2026-01-15', // Earlier than preset end + value: constraintEnd, // Earlier than preset end }, ], }; const result = SearchUIUtils.adjustTimeRangeToDateFilters(yearDateRange, [dateFilter]); - // Start should remain the preset start (2026-01-01) - expect(result.start).toBe('2026-01-01'); - // Should intersect: min(preset end, constraint end) = min(2026-01-31, 2026-01-15) = 2026-01-15 - // The constraint end should be used (earlier date) - expect(result.end).toBe('2026-01-15'); + // Start should remain the preset start + expect(result.start).toBe(lastMonthStart); + // Should intersect: min(preset end, constraint end) — the constraint end should be used (earlier date) + expect(result.end).toBe(constraintEnd); }); it('should correctly intersect multiple date filters (GREATER_THAN and LOWER_THAN) when expanding quarter groups', () => { From 1480dc97cb85a84a53dbb5e0c552a8f2b1aabe2d Mon Sep 17 00:00:00 2001 From: MelvinBot Date: Sun, 1 Mar 2026 20:20:27 +0000 Subject: [PATCH 4/7] Fix: Sort imports in SearchUIUtilsTest.ts to match Prettier configuration Co-authored-by: Krishna --- tests/unit/Search/SearchUIUtilsTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index 6f93cc1b72fa8..1a52207a70cc1 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import {endOfMonth, format, startOfMonth, subMonths} from 'date-fns'; import {renderHook} from '@testing-library/react-native'; +import {endOfMonth, format, startOfMonth, subMonths} from 'date-fns'; import type {OnyxCollection} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {SelectedTransactionInfo} from '@components/Search/types'; From 0cbc4ce7995cab4708897e98da9f37cccfb0e5b4 Mon Sep 17 00:00:00 2001 From: MelvinBot Date: Sun, 1 Mar 2026 20:20:54 +0000 Subject: [PATCH 5/7] Revert unrelated test changes to SearchUIUtilsTest Co-authored-by: Krishna --- tests/unit/Search/SearchUIUtilsTest.ts | 37 ++++++++++---------------- 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index 6f93cc1b72fa8..2ff44d46bd884 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -1,5 +1,4 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import {endOfMonth, format, startOfMonth, subMonths} from 'date-fns'; import {renderHook} from '@testing-library/react-native'; import type {OnyxCollection} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; @@ -3128,17 +3127,13 @@ describe('SearchUIUtils', () => { it('should intersect date preset with additional constraints instead of overwriting', () => { // Test that when combining a date preset (EQUAL_TO) with additional constraints, // we intersect the ranges (take max for start, min for end) rather than overwriting - const now = new Date(); - const lastMonth = subMonths(now, 1); - const lastMonthStart = format(startOfMonth(lastMonth), 'yyyy-MM-dd'); - const lastMonthEnd = format(endOfMonth(lastMonth), 'yyyy-MM-dd'); - const yearDateRange = DateUtils.getYearDateRange(now.getFullYear()); + const yearDateRange = DateUtils.getYearDateRange(2026); const dateFilter = { key: CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, filters: [ { operator: CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO, - value: CONST.SEARCH.DATE_PRESETS.LAST_MONTH, + value: CONST.SEARCH.DATE_PRESETS.LAST_MONTH, // e.g., January 2026: 2026-01-01 to 2026-01-31 }, { operator: CONST.SEARCH.SYNTAX_OPERATORS.GREATER_THAN_OR_EQUAL_TO, @@ -3149,42 +3144,38 @@ describe('SearchUIUtils', () => { const result = SearchUIUtils.adjustTimeRangeToDateFilters(yearDateRange, [dateFilter]); - // Should intersect: max(preset start, constraint start) — preset start wins since it's later + // Should intersect: max(preset start, constraint start) = max(2026-01-01, 2025-04-01) = 2026-01-01 // The preset start should be preserved, not overwritten by the earlier constraint - expect(result.start).toBe(lastMonthStart); - // End should remain the preset end - expect(result.end).toBe(lastMonthEnd); + expect(result.start).toBe('2026-01-01'); + // End should remain the preset end (2026-01-31) + expect(result.end).toBe('2026-01-31'); }); it('should intersect date preset end limit with additional constraints', () => { // Test that when combining a date preset with an end constraint, // we take the minimum (earliest) end date to intersect ranges - const now = new Date(); - const lastMonth = subMonths(now, 1); - const lastMonthStart = format(startOfMonth(lastMonth), 'yyyy-MM-dd'); - // Use the 15th of last month as the constraint end — earlier than the preset end - const constraintEnd = format(new Date(lastMonth.getFullYear(), lastMonth.getMonth(), 15), 'yyyy-MM-dd'); - const yearDateRange = DateUtils.getYearDateRange(now.getFullYear()); + const yearDateRange = DateUtils.getYearDateRange(2026); const dateFilter = { key: CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, filters: [ { operator: CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO, - value: CONST.SEARCH.DATE_PRESETS.LAST_MONTH, + value: CONST.SEARCH.DATE_PRESETS.LAST_MONTH, // e.g., January 2026: 2026-01-01 to 2026-01-31 }, { operator: CONST.SEARCH.SYNTAX_OPERATORS.LOWER_THAN_OR_EQUAL_TO, - value: constraintEnd, // Earlier than preset end + value: '2026-01-15', // Earlier than preset end }, ], }; const result = SearchUIUtils.adjustTimeRangeToDateFilters(yearDateRange, [dateFilter]); - // Start should remain the preset start - expect(result.start).toBe(lastMonthStart); - // Should intersect: min(preset end, constraint end) — the constraint end should be used (earlier date) - expect(result.end).toBe(constraintEnd); + // Start should remain the preset start (2026-01-01) + expect(result.start).toBe('2026-01-01'); + // Should intersect: min(preset end, constraint end) = min(2026-01-31, 2026-01-15) = 2026-01-15 + // The constraint end should be used (earlier date) + expect(result.end).toBe('2026-01-15'); }); it('should correctly intersect multiple date filters (GREATER_THAN and LOWER_THAN) when expanding quarter groups', () => { From 69e4c5ef67198ad224b27c6c9426dd7556a44041 Mon Sep 17 00:00:00 2001 From: MelvinBot Date: Sun, 1 Mar 2026 20:21:38 +0000 Subject: [PATCH 6/7] Remove unused date-fns import after reverting test changes Co-authored-by: Krishna --- tests/unit/Search/SearchUIUtilsTest.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index 0ca622be34f93..2ff44d46bd884 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -1,6 +1,5 @@ /* eslint-disable @typescript-eslint/naming-convention */ import {renderHook} from '@testing-library/react-native'; -import {endOfMonth, format, startOfMonth, subMonths} from 'date-fns'; import type {OnyxCollection} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {SelectedTransactionInfo} from '@components/Search/types'; From 4068c1852a5f3cafd05323c81a347a59621c18cb Mon Sep 17 00:00:00 2001 From: MelvinBot Date: Sun, 1 Mar 2026 20:33:19 +0000 Subject: [PATCH 7/7] Fix time-dependent test failures in SearchUIUtilsTest The two date preset intersection tests use LAST_MONTH which resolves dynamically from new Date(). They hardcoded January 2026 expectations which broke when the month rolled over to March. Wraps these tests in jest.useFakeTimers() frozen to Feb 2026 so LAST_MONTH deterministically resolves to January 2026. Co-authored-by: Krishna --- tests/unit/Search/SearchUIUtilsTest.ts | 107 ++++++++++++++----------- 1 file changed, 60 insertions(+), 47 deletions(-) diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index 2ff44d46bd884..425f0a59c8f24 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -3124,58 +3124,71 @@ describe('SearchUIUtils', () => { expect(result.end).toBe('2025-06-20'); }); - it('should intersect date preset with additional constraints instead of overwriting', () => { - // Test that when combining a date preset (EQUAL_TO) with additional constraints, - // we intersect the ranges (take max for start, min for end) rather than overwriting - const yearDateRange = DateUtils.getYearDateRange(2026); - const dateFilter = { - key: CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, - filters: [ - { - operator: CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO, - value: CONST.SEARCH.DATE_PRESETS.LAST_MONTH, // e.g., January 2026: 2026-01-01 to 2026-01-31 - }, - { - operator: CONST.SEARCH.SYNTAX_OPERATORS.GREATER_THAN_OR_EQUAL_TO, - value: '2025-04-01', // Earlier than preset start - }, - ], - }; + // These tests use LAST_MONTH preset which resolves dynamically from new Date(), + // so we freeze time to February 2026 to make LAST_MONTH = January 2026 deterministically. + describe('date preset intersection with frozen time', () => { + beforeAll(() => { + jest.useFakeTimers(); + jest.setSystemTime(new Date('2026-02-15')); + }); - const result = SearchUIUtils.adjustTimeRangeToDateFilters(yearDateRange, [dateFilter]); + afterAll(() => { + jest.useRealTimers(); + }); - // Should intersect: max(preset start, constraint start) = max(2026-01-01, 2025-04-01) = 2026-01-01 - // The preset start should be preserved, not overwritten by the earlier constraint - expect(result.start).toBe('2026-01-01'); - // End should remain the preset end (2026-01-31) - expect(result.end).toBe('2026-01-31'); - }); + it('should intersect date preset with additional constraints instead of overwriting', () => { + // Test that when combining a date preset (EQUAL_TO) with additional constraints, + // we intersect the ranges (take max for start, min for end) rather than overwriting + const yearDateRange = DateUtils.getYearDateRange(2026); + const dateFilter = { + key: CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, + filters: [ + { + operator: CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO, + value: CONST.SEARCH.DATE_PRESETS.LAST_MONTH, // January 2026: 2026-01-01 to 2026-01-31 + }, + { + operator: CONST.SEARCH.SYNTAX_OPERATORS.GREATER_THAN_OR_EQUAL_TO, + value: '2025-04-01', // Earlier than preset start + }, + ], + }; - it('should intersect date preset end limit with additional constraints', () => { - // Test that when combining a date preset with an end constraint, - // we take the minimum (earliest) end date to intersect ranges - const yearDateRange = DateUtils.getYearDateRange(2026); - const dateFilter = { - key: CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, - filters: [ - { - operator: CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO, - value: CONST.SEARCH.DATE_PRESETS.LAST_MONTH, // e.g., January 2026: 2026-01-01 to 2026-01-31 - }, - { - operator: CONST.SEARCH.SYNTAX_OPERATORS.LOWER_THAN_OR_EQUAL_TO, - value: '2026-01-15', // Earlier than preset end - }, - ], - }; + const result = SearchUIUtils.adjustTimeRangeToDateFilters(yearDateRange, [dateFilter]); - const result = SearchUIUtils.adjustTimeRangeToDateFilters(yearDateRange, [dateFilter]); + // Should intersect: max(preset start, constraint start) = max(2026-01-01, 2025-04-01) = 2026-01-01 + // The preset start should be preserved, not overwritten by the earlier constraint + expect(result.start).toBe('2026-01-01'); + // End should remain the preset end (2026-01-31) + expect(result.end).toBe('2026-01-31'); + }); - // Start should remain the preset start (2026-01-01) - expect(result.start).toBe('2026-01-01'); - // Should intersect: min(preset end, constraint end) = min(2026-01-31, 2026-01-15) = 2026-01-15 - // The constraint end should be used (earlier date) - expect(result.end).toBe('2026-01-15'); + it('should intersect date preset end limit with additional constraints', () => { + // Test that when combining a date preset with an end constraint, + // we take the minimum (earliest) end date to intersect ranges + const yearDateRange = DateUtils.getYearDateRange(2026); + const dateFilter = { + key: CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, + filters: [ + { + operator: CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO, + value: CONST.SEARCH.DATE_PRESETS.LAST_MONTH, // January 2026: 2026-01-01 to 2026-01-31 + }, + { + operator: CONST.SEARCH.SYNTAX_OPERATORS.LOWER_THAN_OR_EQUAL_TO, + value: '2026-01-15', // Earlier than preset end + }, + ], + }; + + const result = SearchUIUtils.adjustTimeRangeToDateFilters(yearDateRange, [dateFilter]); + + // Start should remain the preset start (2026-01-01) + expect(result.start).toBe('2026-01-01'); + // Should intersect: min(preset end, constraint end) = min(2026-01-31, 2026-01-15) = 2026-01-15 + // The constraint end should be used (earlier date) + expect(result.end).toBe('2026-01-15'); + }); }); it('should correctly intersect multiple date filters (GREATER_THAN and LOWER_THAN) when expanding quarter groups', () => {