Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 30 additions & 3 deletions src/libs/Navigation/helpers/linkTo/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,33 @@ function isNavigatingToReportWithSameReportID(currentRoute: NavigationPartialRou
return currentParams?.reportID === newParams?.reportID;
}

function areFullScreenRoutesEqual(matchingFullScreenRoute: NavigationPartialRoute, lastFullScreenRoute: NavigationPartialRoute) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to add unit tests for those functions?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the areFullScreenRoutesEqual function is just a small helper used for value comparison, and it's a private function, I don't think it's necessary to create a unit test for it. What do you think?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have CodeCov guidelines here. Because we have "decrease coverage" in src/libs/Navigation/helpers/linkTo/index.ts, thus we need to keep or increase it @dmkt9. We either test public method linkTo or export and test areFullScreenRoutesEqual method.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I will update the test soon.

@hoangzinh hoangzinh Oct 15, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TY @dmkt9. Leave a comment on PR whenever PR is ready for review again.

const lastRouteInMatchingFullScreen = matchingFullScreenRoute.state?.routes?.at(-1);
const lastRouteInLastFullScreenRoute = lastFullScreenRoute.state?.routes?.at(-1);

const isEqualFullScreenRoute = matchingFullScreenRoute.name === lastFullScreenRoute.name;
const isEqualLastRouteInFullScreenRoute =
!lastRouteInMatchingFullScreen?.name || !lastRouteInLastFullScreenRoute?.name || lastRouteInMatchingFullScreen.name === lastRouteInLastFullScreenRoute.name;

return isEqualFullScreenRoute && isEqualLastRouteInFullScreenRoute;
Comment thread
dmkt9 marked this conversation as resolved.
}

function isRoutePreloaded(currentState: PlatformStackNavigationState<RootNavigatorParamList>, matchingFullScreenRoute: NavigationPartialRoute) {
const lastRouteInMatchingFullScreen = matchingFullScreenRoute.state?.routes?.at(-1);

const preloadedRoutes = currentState.preloadedRoutes;

return preloadedRoutes.some((preloadedRoute) => {
const isMatchingFullScreenRoute = preloadedRoute.name === matchingFullScreenRoute.name;

// Compare the last route of the preloadedRoute and the last route of the matchingFullScreenRoute to ensure the preloaded route is accepted when matching subroutes as well
const isMatchingLastRoute =
!lastRouteInMatchingFullScreen?.name || (preloadedRoute.params && 'screen' in preloadedRoute.params && preloadedRoute.params.screen === lastRouteInMatchingFullScreen?.name);

return isMatchingFullScreenRoute && isMatchingLastRoute;
});
}

export default function linkTo(navigation: NavigationContainerRef<RootNavigatorParamList> | null, path: Route, options?: LinkToOptions) {
if (!navigation) {
throw new Error("Couldn't find a navigation object. Is your component inside a screen in a navigator?");
Expand Down Expand Up @@ -126,9 +153,9 @@ export default function linkTo(navigation: NavigationContainerRef<RootNavigatorP
const matchingFullScreenRoute = getMatchingFullScreenRoute(newFocusedRoute);

const lastFullScreenRoute = currentState.routes.findLast((route) => isFullScreenName(route.name));
if (matchingFullScreenRoute && lastFullScreenRoute && matchingFullScreenRoute.name !== lastFullScreenRoute.name) {
const isMatchingRoutePreloaded = currentState.preloadedRoutes.some((preloadedRoute) => preloadedRoute.name === matchingFullScreenRoute.name);
if (isMatchingRoutePreloaded) {

if (matchingFullScreenRoute && lastFullScreenRoute && !areFullScreenRoutesEqual(matchingFullScreenRoute, lastFullScreenRoute as NavigationPartialRoute)) {
if (isRoutePreloaded(currentState, matchingFullScreenRoute)) {
navigation.dispatch(StackActions.push(matchingFullScreenRoute.name));
} else {
const lastRouteInMatchingFullScreen = matchingFullScreenRoute.state?.routes?.at(-1);
Expand Down
101 changes: 101 additions & 0 deletions tests/navigation/NavigateTests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,5 +145,106 @@ describe('Navigate', () => {
expect(rootStateAfterNavigate?.index).toBe(1);
expect(lastSplitAfterNavigate?.name).toBe(NAVIGATORS.REPORTS_SPLIT_NAVIGATOR);
});

it('to the sub-route from a different split navigator', () => {
// Given the initialized navigation on the narrow layout with the reports split navigator
render(
<TestNavigationContainer
initialState={{
index: 0,
routes: [
{
name: NAVIGATORS.REPORTS_SPLIT_NAVIGATOR,
state: {
index: 0,
routes: [
{
name: SCREENS.HOME,
},
{
name: SCREENS.REPORT,
params: {reportID: '1'},
},
],
},
},
],
}}
/>,
);

const rootStateBeforeNavigate = navigationRef.current?.getRootState();
const lastSplitBeforeNavigate = rootStateBeforeNavigate?.routes.at(-1);
expect(rootStateBeforeNavigate?.index).toBe(0);
expect(lastSplitBeforeNavigate?.name).toBe(NAVIGATORS.REPORTS_SPLIT_NAVIGATOR);
expect(lastSplitBeforeNavigate?.state?.routes.at(-1)?.name).toBe(SCREENS.REPORT);

// When navigate to the page from the different split navigator
act(() => {
Navigation.navigate(ROUTES.SETTINGS_SUBSCRIPTION_ADD_PAYMENT_CARD);
});

// Then push a new split navigator to the navigation state
const rootStateAfterNavigate = navigationRef.current?.getRootState();
expect(rootStateAfterNavigate?.index).toBe(2);

const middleSplitAfterNavigate = rootStateAfterNavigate?.routes.at(-2);
expect(middleSplitAfterNavigate?.name).toBe(NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR);
expect(middleSplitAfterNavigate?.state?.routes.at(-1)?.name).toBe(SCREENS.SETTINGS.SUBSCRIPTION.ROOT);

const lastSplitAfterNavigate = rootStateAfterNavigate?.routes.at(-1);
expect(lastSplitAfterNavigate?.name).toBe(NAVIGATORS.RIGHT_MODAL_NAVIGATOR);
expect(lastSplitAfterNavigate?.state?.routes.at(-1)?.name).toBe(SCREENS.RIGHT_MODAL.SETTINGS);
});

it('to the sub-route from a same split navigator', () => {
// Given the initialized navigation on the narrow layout with the settings split navigator
render(
<TestNavigationContainer
initialState={{
index: 0,
routes: [
{
name: NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR,
state: {
index: 0,
routes: [
{
name: SCREENS.SETTINGS.ROOT,
},
{
name: SCREENS.SETTINGS.PROFILE.ROOT,
},
],
},
},
],
}}
/>,
);

const rootStateBeforeNavigate = navigationRef.current?.getRootState();
const lastSplitBeforeNavigate = rootStateBeforeNavigate?.routes.at(-1);
expect(rootStateBeforeNavigate?.index).toBe(0);
expect(lastSplitBeforeNavigate?.name).toBe(NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR);
expect(lastSplitBeforeNavigate?.state?.routes.at(-1)?.name).toBe(SCREENS.SETTINGS.PROFILE.ROOT);

// When navigate to the page from the same split navigator
act(() => {
Navigation.navigate(ROUTES.SETTINGS_SUBSCRIPTION_ADD_PAYMENT_CARD);
});

// Then push a new split navigator to the navigation state
const rootStateAfterNavigate = navigationRef.current?.getRootState();
expect(rootStateAfterNavigate?.index).toBe(2);

const middleSplitAfterNavigate = rootStateAfterNavigate?.routes.at(-2);
expect(middleSplitAfterNavigate?.name).toBe(NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR);
expect(middleSplitAfterNavigate?.state?.routes.at(-1)?.name).toBe(SCREENS.SETTINGS.SUBSCRIPTION.ROOT);

const lastSplitAfterNavigate = rootStateAfterNavigate?.routes.at(-1);
expect(lastSplitAfterNavigate?.name).toBe(NAVIGATORS.RIGHT_MODAL_NAVIGATOR);
expect(lastSplitAfterNavigate?.state?.routes.at(-1)?.name).toBe(SCREENS.RIGHT_MODAL.SETTINGS);
});
});
});
24 changes: 24 additions & 0 deletions tests/utils/TestNavigationContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import navigationRef from '@libs/Navigation/navigationRef';
import type {
AuthScreensParamList,
ReportsSplitNavigatorParamList,
RightModalNavigatorParamList,
SearchFullscreenNavigatorParamList,
SettingsSplitNavigatorParamList,
WorkspaceSplitNavigatorParamList,
Expand All @@ -21,6 +22,7 @@ const ReportsSplit = createSplitNavigator<ReportsSplitNavigatorParamList>();
const SettingsSplit = createSplitNavigator<SettingsSplitNavigatorParamList>();
const SearchStack = createPlatformStackNavigator<SearchFullscreenNavigatorParamList>();
const WorkspaceSplit = createSplitNavigator<WorkspaceSplitNavigatorParamList>();
const RightModalNavigatorStack = createSplitNavigator<RightModalNavigatorParamList>();

const getEmptyComponent = () => jest.fn();

Expand Down Expand Up @@ -103,6 +105,10 @@ function TestSettingsSplitNavigator() {
name={SCREENS.SETTINGS.ABOUT}
getComponent={getEmptyComponent}
/>
<SettingsSplit.Screen
name={SCREENS.SETTINGS.SUBSCRIPTION.ROOT}
getComponent={getEmptyComponent}
/>
</SettingsSplit.Navigator>
);
}
Expand All @@ -122,6 +128,20 @@ function TestSearchFullscreenNavigator() {
);
}

function TestRightModalNavigator() {
return (
<RightModalNavigatorStack.Navigator
defaultCentralScreen={SCREENS.RIGHT_MODAL.SETTINGS}
parentRoute={CONST.NAVIGATION_TESTS.DEFAULT_PARENT_ROUTE}
>
<RightModalNavigatorStack.Screen
name={SCREENS.RIGHT_MODAL.SETTINGS}
getComponent={getEmptyComponent()}
/>
</RightModalNavigatorStack.Navigator>
);
}

function TestNavigationContainer({initialState}: TestNavigationContainerProps) {
return (
<NavigationContainer
Expand Down Expand Up @@ -149,6 +169,10 @@ function TestNavigationContainer({initialState}: TestNavigationContainerProps) {
name={SCREENS.VALIDATE_LOGIN}
component={getEmptyComponent()}
/>
<RootStack.Screen
name={NAVIGATORS.RIGHT_MODAL_NAVIGATOR}
component={TestRightModalNavigator}
/>
</RootStack.Navigator>
</NavigationContainer>
);
Expand Down
Loading