Skip to content

Commit 49dde14

Browse files
committed
test(react-router): add coverage for index route reuse across nested outlet tabs
1 parent a6ee4c4 commit 49dde14

4 files changed

Lines changed: 298 additions & 0 deletions

File tree

packages/react-router/test/base/src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import IonRoutePropsTest from './pages/ion-route-props/IonRouteProps';
5151
import PrefixMatchWildcard from './pages/prefix-match-wildcard/PrefixMatchWildcard';
5252
import StaleViewCleanup from './pages/stale-view-cleanup/StaleViewCleanup';
5353
import IndexParamPriority from './pages/index-param-priority/IndexParamPriority';
54+
import IndexRouteReuse from './pages/index-route-reuse/IndexRouteReuse';
5455

5556
setupIonicReact();
5657

@@ -91,6 +92,7 @@ const App: React.FC = () => {
9192
<Route path="/prefix-match-wildcard/*" element={<PrefixMatchWildcard />} />
9293
<Route path="/stale-view-cleanup/*" element={<StaleViewCleanup />} />
9394
<Route path="/index-param-priority/*" element={<IndexParamPriority />} />
95+
<Route path="/index-route-reuse/*" element={<IndexRouteReuse />} />
9496
</IonRouterOutlet>
9597
</IonReactRouter>
9698
</IonApp>

packages/react-router/test/base/src/pages/Main.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,9 @@ const Main: React.FC = () => {
9898
<IonItem routerLink="/index-param-priority">
9999
<IonLabel>Index Param Priority</IonLabel>
100100
</IonItem>
101+
<IonItem routerLink="/index-route-reuse">
102+
<IonLabel>Index Route Reuse</IonLabel>
103+
</IonItem>
101104
</IonList>
102105
</IonContent>
103106
</IonPage>
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import {
2+
IonContent,
3+
IonHeader,
4+
IonPage,
5+
IonTitle,
6+
IonToolbar,
7+
IonRouterOutlet,
8+
IonTabs,
9+
IonTabBar,
10+
IonTabButton,
11+
IonIcon,
12+
IonLabel,
13+
IonButton,
14+
IonBackButton,
15+
IonButtons,
16+
} from '@ionic/react';
17+
import { triangle, ellipse, square } from 'ionicons/icons';
18+
import React from 'react';
19+
import { Route, Navigate } from 'react-router-dom';
20+
21+
/**
22+
* Test page for index route reuse across tabs with nested outlets.
23+
*
24+
* Each tab has its own nested IonRouterOutlet with an index route that
25+
* renders distinct content. This tests that switching between tabs shows
26+
* the correct index route content and doesn't incorrectly reuse a view
27+
* item from another tab's index route.
28+
*/
29+
30+
// Tab 1 nested outlet with its own index route
31+
const Tab1Content: React.FC = () => {
32+
return (
33+
<IonPage data-pageid="irr-tab1-home">
34+
<IonHeader>
35+
<IonToolbar>
36+
<IonTitle>Tab 1 Home</IonTitle>
37+
</IonToolbar>
38+
</IonHeader>
39+
<IonContent>
40+
<div data-testid="irr-tab1-home-content">Tab 1 Index Route Content</div>
41+
<IonButton routerLink="/index-route-reuse/tab1/detail" id="irr-tab1-detail-btn">
42+
Go to Tab 1 Detail
43+
</IonButton>
44+
</IonContent>
45+
</IonPage>
46+
);
47+
};
48+
49+
const Tab1Detail: React.FC = () => {
50+
return (
51+
<IonPage data-pageid="irr-tab1-detail">
52+
<IonHeader>
53+
<IonToolbar>
54+
<IonButtons slot="start">
55+
<IonBackButton defaultHref="/index-route-reuse/tab1" />
56+
</IonButtons>
57+
<IonTitle>Tab 1 Detail</IonTitle>
58+
</IonToolbar>
59+
</IonHeader>
60+
<IonContent>
61+
<div data-testid="irr-tab1-detail-content">Tab 1 Detail Content</div>
62+
</IonContent>
63+
</IonPage>
64+
);
65+
};
66+
67+
const Tab1Outlet: React.FC = () => {
68+
return (
69+
<IonPage>
70+
<IonRouterOutlet>
71+
<Route index element={<Tab1Content />} />
72+
<Route path="detail" element={<Tab1Detail />} />
73+
</IonRouterOutlet>
74+
</IonPage>
75+
);
76+
};
77+
78+
// Tab 2 nested outlet with its own index route
79+
const Tab2Content: React.FC = () => {
80+
return (
81+
<IonPage data-pageid="irr-tab2-home">
82+
<IonHeader>
83+
<IonToolbar>
84+
<IonTitle>Tab 2 Home</IonTitle>
85+
</IonToolbar>
86+
</IonHeader>
87+
<IonContent>
88+
<div data-testid="irr-tab2-home-content">Tab 2 Index Route Content</div>
89+
<IonButton routerLink="/index-route-reuse/tab2/detail" id="irr-tab2-detail-btn">
90+
Go to Tab 2 Detail
91+
</IonButton>
92+
</IonContent>
93+
</IonPage>
94+
);
95+
};
96+
97+
const Tab2Detail: React.FC = () => {
98+
return (
99+
<IonPage data-pageid="irr-tab2-detail">
100+
<IonHeader>
101+
<IonToolbar>
102+
<IonButtons slot="start">
103+
<IonBackButton defaultHref="/index-route-reuse/tab2" />
104+
</IonButtons>
105+
<IonTitle>Tab 2 Detail</IonTitle>
106+
</IonToolbar>
107+
</IonHeader>
108+
<IonContent>
109+
<div data-testid="irr-tab2-detail-content">Tab 2 Detail Content</div>
110+
</IonContent>
111+
</IonPage>
112+
);
113+
};
114+
115+
const Tab2Outlet: React.FC = () => {
116+
return (
117+
<IonPage>
118+
<IonRouterOutlet>
119+
<Route index element={<Tab2Content />} />
120+
<Route path="detail" element={<Tab2Detail />} />
121+
</IonRouterOutlet>
122+
</IonPage>
123+
);
124+
};
125+
126+
// Tab 3 nested outlet with its own index route
127+
const Tab3Content: React.FC = () => {
128+
return (
129+
<IonPage data-pageid="irr-tab3-home">
130+
<IonHeader>
131+
<IonToolbar>
132+
<IonTitle>Tab 3 Home</IonTitle>
133+
</IonToolbar>
134+
</IonHeader>
135+
<IonContent>
136+
<div data-testid="irr-tab3-home-content">Tab 3 Index Route Content</div>
137+
</IonContent>
138+
</IonPage>
139+
);
140+
};
141+
142+
const Tab3Outlet: React.FC = () => {
143+
return (
144+
<IonPage>
145+
<IonRouterOutlet>
146+
<Route index element={<Tab3Content />} />
147+
</IonRouterOutlet>
148+
</IonPage>
149+
);
150+
};
151+
152+
// Main tabs component
153+
const IndexRouteReuse: React.FC = () => {
154+
return (
155+
<IonTabs>
156+
<IonRouterOutlet>
157+
<Route path="tab1/*" element={<Tab1Outlet />} />
158+
<Route path="tab2/*" element={<Tab2Outlet />} />
159+
<Route path="tab3/*" element={<Tab3Outlet />} />
160+
<Route index element={<Navigate to="tab1" replace />} />
161+
</IonRouterOutlet>
162+
<IonTabBar slot="bottom">
163+
<IonTabButton tab="tab1" href="/index-route-reuse/tab1">
164+
<IonIcon icon={triangle} />
165+
<IonLabel>Tab 1</IonLabel>
166+
</IonTabButton>
167+
<IonTabButton tab="tab2" href="/index-route-reuse/tab2">
168+
<IonIcon icon={ellipse} />
169+
<IonLabel>Tab 2</IonLabel>
170+
</IonTabButton>
171+
<IonTabButton tab="tab3" href="/index-route-reuse/tab3">
172+
<IonIcon icon={square} />
173+
<IonLabel>Tab 3</IonLabel>
174+
</IonTabButton>
175+
</IonTabBar>
176+
</IonTabs>
177+
);
178+
};
179+
180+
export default IndexRouteReuse;
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
const port = 3000;
2+
3+
/**
4+
* Tests for index route reuse across tabs with nested outlets.
5+
*
6+
* Validates that switching between tabs where each has its own nested
7+
* IonRouterOutlet with an index route correctly shows the right content.
8+
* This tests whether createViewItem's index route reuse check
9+
* (existingIsIndexRoute && newIsIndexRoute) causes issues when multiple
10+
* outlets each have their own index routes.
11+
*/
12+
describe('Index Route Reuse - Nested Outlet Index Routes', () => {
13+
it('should show tab1 index content by default', () => {
14+
cy.visit(`http://localhost:${port}/index-route-reuse`);
15+
cy.ionPageVisible('irr-tab1-home');
16+
cy.get('[data-testid="irr-tab1-home-content"]').should('be.visible');
17+
cy.get('[data-testid="irr-tab1-home-content"]').should('contain', 'Tab 1 Index Route Content');
18+
});
19+
20+
it('should show tab2 index content when switching to tab2', () => {
21+
cy.visit(`http://localhost:${port}/index-route-reuse/tab1`);
22+
cy.ionPageVisible('irr-tab1-home');
23+
24+
// Switch to Tab 2
25+
cy.ionTabClick('Tab 2');
26+
cy.ionPageVisible('irr-tab2-home');
27+
cy.get('[data-testid="irr-tab2-home-content"]').should('be.visible');
28+
cy.get('[data-testid="irr-tab2-home-content"]').should('contain', 'Tab 2 Index Route Content');
29+
30+
// Verify URL changed to tab2
31+
cy.url().should('include', '/index-route-reuse/tab2');
32+
});
33+
34+
it('should show tab3 index content when switching to tab3', () => {
35+
cy.visit(`http://localhost:${port}/index-route-reuse/tab1`);
36+
cy.ionPageVisible('irr-tab1-home');
37+
38+
// Switch to Tab 3
39+
cy.ionTabClick('Tab 3');
40+
cy.ionPageVisible('irr-tab3-home');
41+
cy.get('[data-testid="irr-tab3-home-content"]').should('be.visible');
42+
cy.get('[data-testid="irr-tab3-home-content"]').should('contain', 'Tab 3 Index Route Content');
43+
44+
// Verify URL changed to tab3
45+
cy.url().should('include', '/index-route-reuse/tab3');
46+
});
47+
48+
it('should correctly show each tab index when cycling through all tabs', () => {
49+
cy.visit(`http://localhost:${port}/index-route-reuse/tab1`);
50+
cy.ionPageVisible('irr-tab1-home');
51+
cy.get('[data-testid="irr-tab1-home-content"]').should('be.visible');
52+
53+
// Tab 1 -> Tab 2
54+
cy.ionTabClick('Tab 2');
55+
cy.ionPageVisible('irr-tab2-home');
56+
cy.get('[data-testid="irr-tab2-home-content"]').should('be.visible');
57+
cy.get('[data-testid="irr-tab2-home-content"]').should('contain', 'Tab 2 Index Route Content');
58+
59+
// Tab 2 -> Tab 3
60+
cy.ionTabClick('Tab 3');
61+
cy.ionPageVisible('irr-tab3-home');
62+
cy.get('[data-testid="irr-tab3-home-content"]').should('be.visible');
63+
cy.get('[data-testid="irr-tab3-home-content"]').should('contain', 'Tab 3 Index Route Content');
64+
65+
// Tab 3 -> Tab 1 (back to start)
66+
cy.ionTabClick('Tab 1');
67+
cy.ionPageVisible('irr-tab1-home');
68+
cy.get('[data-testid="irr-tab1-home-content"]').should('be.visible');
69+
cy.get('[data-testid="irr-tab1-home-content"]').should('contain', 'Tab 1 Index Route Content');
70+
});
71+
72+
it('should preserve tab1 detail navigation and return correctly', () => {
73+
cy.visit(`http://localhost:${port}/index-route-reuse/tab1`);
74+
cy.ionPageVisible('irr-tab1-home');
75+
76+
// Navigate to detail page
77+
cy.get('#irr-tab1-detail-btn').click();
78+
cy.ionPageVisible('irr-tab1-detail');
79+
cy.get('[data-testid="irr-tab1-detail-content"]').should('be.visible');
80+
81+
// Switch to Tab 2
82+
cy.ionTabClick('Tab 2');
83+
cy.ionPageVisible('irr-tab2-home');
84+
cy.get('[data-testid="irr-tab2-home-content"]').should('be.visible');
85+
86+
// Switch back to Tab 1 - should show detail (preserved history)
87+
cy.ionTabClick('Tab 1');
88+
cy.ionPageVisible('irr-tab1-detail');
89+
cy.get('[data-testid="irr-tab1-detail-content"]').should('be.visible');
90+
});
91+
92+
it('should show correct content after rapid tab switching', () => {
93+
cy.visit(`http://localhost:${port}/index-route-reuse/tab1`);
94+
cy.ionPageVisible('irr-tab1-home');
95+
96+
// Rapid switching: Tab1 -> Tab2 -> Tab3 -> Tab2 -> Tab1
97+
cy.ionTabClick('Tab 2');
98+
cy.ionPageVisible('irr-tab2-home');
99+
100+
cy.ionTabClick('Tab 3');
101+
cy.ionPageVisible('irr-tab3-home');
102+
103+
cy.ionTabClick('Tab 2');
104+
cy.ionPageVisible('irr-tab2-home');
105+
cy.get('[data-testid="irr-tab2-home-content"]').should('be.visible');
106+
cy.get('[data-testid="irr-tab2-home-content"]').should('contain', 'Tab 2 Index Route Content');
107+
108+
cy.ionTabClick('Tab 1');
109+
cy.ionPageVisible('irr-tab1-home');
110+
cy.get('[data-testid="irr-tab1-home-content"]').should('be.visible');
111+
cy.get('[data-testid="irr-tab1-home-content"]').should('contain', 'Tab 1 Index Route Content');
112+
});
113+
});

0 commit comments

Comments
 (0)