test(react-router): add coverage for index route reuse across nested outlet tabs

This commit is contained in:
ShaneK
2026-03-12 08:00:11 -07:00
parent a6ee4c44fb
commit 49dde1483c
4 changed files with 298 additions and 0 deletions

View File

@@ -51,6 +51,7 @@ import IonRoutePropsTest from './pages/ion-route-props/IonRouteProps';
import PrefixMatchWildcard from './pages/prefix-match-wildcard/PrefixMatchWildcard';
import StaleViewCleanup from './pages/stale-view-cleanup/StaleViewCleanup';
import IndexParamPriority from './pages/index-param-priority/IndexParamPriority';
import IndexRouteReuse from './pages/index-route-reuse/IndexRouteReuse';
setupIonicReact();
@@ -91,6 +92,7 @@ const App: React.FC = () => {
<Route path="/prefix-match-wildcard/*" element={<PrefixMatchWildcard />} />
<Route path="/stale-view-cleanup/*" element={<StaleViewCleanup />} />
<Route path="/index-param-priority/*" element={<IndexParamPriority />} />
<Route path="/index-route-reuse/*" element={<IndexRouteReuse />} />
</IonRouterOutlet>
</IonReactRouter>
</IonApp>

View File

@@ -98,6 +98,9 @@ const Main: React.FC = () => {
<IonItem routerLink="/index-param-priority">
<IonLabel>Index Param Priority</IonLabel>
</IonItem>
<IonItem routerLink="/index-route-reuse">
<IonLabel>Index Route Reuse</IonLabel>
</IonItem>
</IonList>
</IonContent>
</IonPage>

View File

@@ -0,0 +1,180 @@
import {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonRouterOutlet,
IonTabs,
IonTabBar,
IonTabButton,
IonIcon,
IonLabel,
IonButton,
IonBackButton,
IonButtons,
} from '@ionic/react';
import { triangle, ellipse, square } from 'ionicons/icons';
import React from 'react';
import { Route, Navigate } from 'react-router-dom';
/**
* Test page for index route reuse across tabs with nested outlets.
*
* Each tab has its own nested IonRouterOutlet with an index route that
* renders distinct content. This tests that switching between tabs shows
* the correct index route content and doesn't incorrectly reuse a view
* item from another tab's index route.
*/
// Tab 1 nested outlet with its own index route
const Tab1Content: React.FC = () => {
return (
<IonPage data-pageid="irr-tab1-home">
<IonHeader>
<IonToolbar>
<IonTitle>Tab 1 Home</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<div data-testid="irr-tab1-home-content">Tab 1 Index Route Content</div>
<IonButton routerLink="/index-route-reuse/tab1/detail" id="irr-tab1-detail-btn">
Go to Tab 1 Detail
</IonButton>
</IonContent>
</IonPage>
);
};
const Tab1Detail: React.FC = () => {
return (
<IonPage data-pageid="irr-tab1-detail">
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonBackButton defaultHref="/index-route-reuse/tab1" />
</IonButtons>
<IonTitle>Tab 1 Detail</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<div data-testid="irr-tab1-detail-content">Tab 1 Detail Content</div>
</IonContent>
</IonPage>
);
};
const Tab1Outlet: React.FC = () => {
return (
<IonPage>
<IonRouterOutlet>
<Route index element={<Tab1Content />} />
<Route path="detail" element={<Tab1Detail />} />
</IonRouterOutlet>
</IonPage>
);
};
// Tab 2 nested outlet with its own index route
const Tab2Content: React.FC = () => {
return (
<IonPage data-pageid="irr-tab2-home">
<IonHeader>
<IonToolbar>
<IonTitle>Tab 2 Home</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<div data-testid="irr-tab2-home-content">Tab 2 Index Route Content</div>
<IonButton routerLink="/index-route-reuse/tab2/detail" id="irr-tab2-detail-btn">
Go to Tab 2 Detail
</IonButton>
</IonContent>
</IonPage>
);
};
const Tab2Detail: React.FC = () => {
return (
<IonPage data-pageid="irr-tab2-detail">
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonBackButton defaultHref="/index-route-reuse/tab2" />
</IonButtons>
<IonTitle>Tab 2 Detail</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<div data-testid="irr-tab2-detail-content">Tab 2 Detail Content</div>
</IonContent>
</IonPage>
);
};
const Tab2Outlet: React.FC = () => {
return (
<IonPage>
<IonRouterOutlet>
<Route index element={<Tab2Content />} />
<Route path="detail" element={<Tab2Detail />} />
</IonRouterOutlet>
</IonPage>
);
};
// Tab 3 nested outlet with its own index route
const Tab3Content: React.FC = () => {
return (
<IonPage data-pageid="irr-tab3-home">
<IonHeader>
<IonToolbar>
<IonTitle>Tab 3 Home</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<div data-testid="irr-tab3-home-content">Tab 3 Index Route Content</div>
</IonContent>
</IonPage>
);
};
const Tab3Outlet: React.FC = () => {
return (
<IonPage>
<IonRouterOutlet>
<Route index element={<Tab3Content />} />
</IonRouterOutlet>
</IonPage>
);
};
// Main tabs component
const IndexRouteReuse: React.FC = () => {
return (
<IonTabs>
<IonRouterOutlet>
<Route path="tab1/*" element={<Tab1Outlet />} />
<Route path="tab2/*" element={<Tab2Outlet />} />
<Route path="tab3/*" element={<Tab3Outlet />} />
<Route index element={<Navigate to="tab1" replace />} />
</IonRouterOutlet>
<IonTabBar slot="bottom">
<IonTabButton tab="tab1" href="/index-route-reuse/tab1">
<IonIcon icon={triangle} />
<IonLabel>Tab 1</IonLabel>
</IonTabButton>
<IonTabButton tab="tab2" href="/index-route-reuse/tab2">
<IonIcon icon={ellipse} />
<IonLabel>Tab 2</IonLabel>
</IonTabButton>
<IonTabButton tab="tab3" href="/index-route-reuse/tab3">
<IonIcon icon={square} />
<IonLabel>Tab 3</IonLabel>
</IonTabButton>
</IonTabBar>
</IonTabs>
);
};
export default IndexRouteReuse;

View File

@@ -0,0 +1,113 @@
const port = 3000;
/**
* Tests for index route reuse across tabs with nested outlets.
*
* Validates that switching between tabs where each has its own nested
* IonRouterOutlet with an index route correctly shows the right content.
* This tests whether createViewItem's index route reuse check
* (existingIsIndexRoute && newIsIndexRoute) causes issues when multiple
* outlets each have their own index routes.
*/
describe('Index Route Reuse - Nested Outlet Index Routes', () => {
it('should show tab1 index content by default', () => {
cy.visit(`http://localhost:${port}/index-route-reuse`);
cy.ionPageVisible('irr-tab1-home');
cy.get('[data-testid="irr-tab1-home-content"]').should('be.visible');
cy.get('[data-testid="irr-tab1-home-content"]').should('contain', 'Tab 1 Index Route Content');
});
it('should show tab2 index content when switching to tab2', () => {
cy.visit(`http://localhost:${port}/index-route-reuse/tab1`);
cy.ionPageVisible('irr-tab1-home');
// Switch to Tab 2
cy.ionTabClick('Tab 2');
cy.ionPageVisible('irr-tab2-home');
cy.get('[data-testid="irr-tab2-home-content"]').should('be.visible');
cy.get('[data-testid="irr-tab2-home-content"]').should('contain', 'Tab 2 Index Route Content');
// Verify URL changed to tab2
cy.url().should('include', '/index-route-reuse/tab2');
});
it('should show tab3 index content when switching to tab3', () => {
cy.visit(`http://localhost:${port}/index-route-reuse/tab1`);
cy.ionPageVisible('irr-tab1-home');
// Switch to Tab 3
cy.ionTabClick('Tab 3');
cy.ionPageVisible('irr-tab3-home');
cy.get('[data-testid="irr-tab3-home-content"]').should('be.visible');
cy.get('[data-testid="irr-tab3-home-content"]').should('contain', 'Tab 3 Index Route Content');
// Verify URL changed to tab3
cy.url().should('include', '/index-route-reuse/tab3');
});
it('should correctly show each tab index when cycling through all tabs', () => {
cy.visit(`http://localhost:${port}/index-route-reuse/tab1`);
cy.ionPageVisible('irr-tab1-home');
cy.get('[data-testid="irr-tab1-home-content"]').should('be.visible');
// Tab 1 -> Tab 2
cy.ionTabClick('Tab 2');
cy.ionPageVisible('irr-tab2-home');
cy.get('[data-testid="irr-tab2-home-content"]').should('be.visible');
cy.get('[data-testid="irr-tab2-home-content"]').should('contain', 'Tab 2 Index Route Content');
// Tab 2 -> Tab 3
cy.ionTabClick('Tab 3');
cy.ionPageVisible('irr-tab3-home');
cy.get('[data-testid="irr-tab3-home-content"]').should('be.visible');
cy.get('[data-testid="irr-tab3-home-content"]').should('contain', 'Tab 3 Index Route Content');
// Tab 3 -> Tab 1 (back to start)
cy.ionTabClick('Tab 1');
cy.ionPageVisible('irr-tab1-home');
cy.get('[data-testid="irr-tab1-home-content"]').should('be.visible');
cy.get('[data-testid="irr-tab1-home-content"]').should('contain', 'Tab 1 Index Route Content');
});
it('should preserve tab1 detail navigation and return correctly', () => {
cy.visit(`http://localhost:${port}/index-route-reuse/tab1`);
cy.ionPageVisible('irr-tab1-home');
// Navigate to detail page
cy.get('#irr-tab1-detail-btn').click();
cy.ionPageVisible('irr-tab1-detail');
cy.get('[data-testid="irr-tab1-detail-content"]').should('be.visible');
// Switch to Tab 2
cy.ionTabClick('Tab 2');
cy.ionPageVisible('irr-tab2-home');
cy.get('[data-testid="irr-tab2-home-content"]').should('be.visible');
// Switch back to Tab 1 - should show detail (preserved history)
cy.ionTabClick('Tab 1');
cy.ionPageVisible('irr-tab1-detail');
cy.get('[data-testid="irr-tab1-detail-content"]').should('be.visible');
});
it('should show correct content after rapid tab switching', () => {
cy.visit(`http://localhost:${port}/index-route-reuse/tab1`);
cy.ionPageVisible('irr-tab1-home');
// Rapid switching: Tab1 -> Tab2 -> Tab3 -> Tab2 -> Tab1
cy.ionTabClick('Tab 2');
cy.ionPageVisible('irr-tab2-home');
cy.ionTabClick('Tab 3');
cy.ionPageVisible('irr-tab3-home');
cy.ionTabClick('Tab 2');
cy.ionPageVisible('irr-tab2-home');
cy.get('[data-testid="irr-tab2-home-content"]').should('be.visible');
cy.get('[data-testid="irr-tab2-home-content"]').should('contain', 'Tab 2 Index Route Content');
cy.ionTabClick('Tab 1');
cy.ionPageVisible('irr-tab1-home');
cy.get('[data-testid="irr-tab1-home-content"]').should('be.visible');
cy.get('[data-testid="irr-tab1-home-content"]').should('contain', 'Tab 1 Index Route Content');
});
});