diff --git a/packages/react-router/src/ReactRouter/IonRouter.tsx b/packages/react-router/src/ReactRouter/IonRouter.tsx index fcadd6561a6..761b19295d7 100644 --- a/packages/react-router/src/ReactRouter/IonRouter.tsx +++ b/packages/react-router/src/ReactRouter/IonRouter.tsx @@ -84,10 +84,15 @@ class IonRouterInner extends React.PureComponent { this.incomingRouteParams.routeOptions = routeOptions; this.props.history.push(routeInfo.pathname + (routeInfo.search || '')); } else { + /** + * The recorded search has to match the URL we push, otherwise + * handleHistoryChange sees a URL change where there is none. + */ + const normalizedSearch = search ? '?' + search : ''; this.incomingRouteParams.pathname = pathname; - this.incomingRouteParams.search = search ? '?' + search : undefined; + this.incomingRouteParams.search = normalizedSearch; this.incomingRouteParams.routeOptions = routeOptions; - this.props.history.push(pathname + (search ? '?' + search : '')); + this.props.history.push(pathname + normalizedSearch); } } else { this.handleNavigate(pathname, 'push', 'none', undefined, routeOptions, tab); @@ -107,7 +112,8 @@ class IonRouterInner extends React.PureComponent { } const leavingUrl = leavingLocationInfo.pathname + leavingLocationInfo.search; - if (leavingUrl !== location.pathname) { + const currentUrl = location.pathname + (location.search || ''); + if (leavingUrl !== currentUrl) { if (!this.incomingRouteParams) { if (action === 'REPLACE') { this.incomingRouteParams = { diff --git a/packages/react-router/test/base/tests/e2e/specs/routing.cy.js b/packages/react-router/test/base/tests/e2e/specs/routing.cy.js index fd28ee573c0..bf71c305c3b 100644 --- a/packages/react-router/test/base/tests/e2e/specs/routing.cy.js +++ b/packages/react-router/test/base/tests/e2e/specs/routing.cy.js @@ -250,7 +250,7 @@ describe('Routing Tests', () => { it('/routing/ > Details 1 > Details 2 > Details 3 > Back > Settings Tab > Home Tab > Should be at details 2 page', () => { // fixes an issue where route history was being lost after starting to go back, switching tabs // and switching back to the same tab again - // for bug https://github.com/ionic-team/ionic-framework/issues/21834 + // For bug https://github.com/ionic-team/ionic-framework/issues/21834 cy.visit(`http://localhost:${port}/routing`); cy.ionPageVisible('home-page'); cy.ionNav('ion-item', 'Details 1'); @@ -344,6 +344,65 @@ describe('Routing Tests', () => { cy.get('div.ion-page[data-pageid=home-details-page-1] [data-testid="details-input"]').should('have.value', '1'); }); + it('Details 1 with Query Params > pop a same-URL history entry, should stay on details 1', () => { + // Regression: popstate over a same-URL entry on a search-bearing route used to trigger a false POP transition. + // For bug https://github.com/ionic-team/ionic-framework/issues/31152 + cy.visit(`http://localhost:${port}/routing`); + cy.ionNav('ion-item', 'Details 1 with Query Params'); + cy.ionPageVisible('home-details-page-1'); + cy.location('search').should('eq', '?hello=there'); + + cy.window().then((win) => { + win.history.pushState({ marker: true }, '', win.location.href); + }); + + cy.go('back'); + + // Wait for a late teleport, otherwise the assertions pass against the pre-transition state. + cy.wait(1000); + + cy.ionPageVisible('home-details-page-1'); + cy.ionPageHidden('home-page'); + cy.location('pathname').should('eq', '/routing/tabs/home/details/1'); + cy.location('search').should('eq', '?hello=there'); + }); + + it('Details 1 > Settings Details 1 > Back > Settings Tab > pop a same-URL history entry, should stay on settings', () => { + // Regression: switching to a tab recorded a route with no search, so a popstate over a + // same-URL entry used to teleport to the home tab. + // For bug https://github.com/ionic-team/ionic-framework/issues/31152 + cy.visit(`http://localhost:${port}/routing`); + cy.ionNav('ion-item', 'Details 1'); + cy.ionPageVisible('home-details-page-1'); + + cy.ionNav('ion-button', 'Go to Settings Details 1'); + cy.ionPageVisible('settings-details-page-1'); + + cy.go('back'); + cy.ionPageVisible('home-details-page-1'); + + cy.ionTabClick('Settings'); + cy.ionPageVisible('settings-page'); + cy.location('pathname').should('eq', '/routing/tabs/settings'); + + // Let the tab switch finish so the pop doesn't race the transition. + cy.wait(1000); + + cy.window().then((win) => { + win.history.pushState({ marker: true }, '', win.location.href); + }); + + cy.go('back'); + + // Wait for a late teleport, otherwise the assertions pass against the pre-transition state. + cy.wait(1000); + + cy.ionPageVisible('settings-page'); + cy.ionPageHidden('home-details-page-1'); + cy.location('pathname').should('eq', '/routing/tabs/settings'); + cy.location('search').should('eq', ''); + }); + /* Tests to add: Test that lifecycle events fire