From 433768ab5999ddca7931898a9a140c2e4e26c056 Mon Sep 17 00:00:00 2001 From: Eric Hechavarria Date: Tue, 19 May 2026 03:51:59 -0400 Subject: [PATCH] fix(react-router): symmetric pathname+search url comparison MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The leavingUrl/currentUrl comparison in handleHistoryChange used `leavingLocationInfo.pathname + leavingLocationInfo.search` on the left side but `location.pathname` (no search) on the right. For any route with a non-empty search string, the comparison was always unequal, so the transition block ran on every history event — including no-op popstates over same-URL entries (e.g. pushed via window.history.pushState). That triggered a false POP transition to the previous route while the browser URL stayed put. Compares pathname+search on both sides so the block runs only when the URL actually changed. Verified with a new Cypress regression that pushes a same-URL state on a search-bearing route, calls history.back(), and asserts the page does not teleport. --- .../src/ReactRouter/IonRouter.tsx | 3 ++- .../test/base/tests/e2e/specs/routing.cy.js | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/react-router/src/ReactRouter/IonRouter.tsx b/packages/react-router/src/ReactRouter/IonRouter.tsx index fcadd6561a6..6faee1e3a50 100644 --- a/packages/react-router/src/ReactRouter/IonRouter.tsx +++ b/packages/react-router/src/ReactRouter/IonRouter.tsx @@ -107,7 +107,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..258e528a364 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 @@ -344,6 +344,25 @@ 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. + 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'); + + // No teleport: still on details 1, URL unchanged. + cy.ionPageVisible('home-details-page-1'); + cy.location('pathname').should('eq', '/routing/tabs/home/details/1'); + cy.location('search').should('eq', '?hello=there'); + }); + /* Tests to add: Test that lifecycle events fire