From c21ac37a583769b6baf0624a6cf454abc1e0860d Mon Sep 17 00:00:00 2001 From: ShaneK Date: Mon, 11 May 2026 09:48:27 -0700 Subject: [PATCH 1/2] fix(vue-router): preserve tab back navigation after history rewrite --- packages/vue-router/src/router.ts | 27 ++++++- packages/vue/test/base/src/router/index.ts | 5 ++ .../vue/test/base/src/views/tabs/Tab2.vue | 4 ++ .../base/src/views/tabs/Tab2Parameter.vue | 57 +++++++++++++++ .../e2e/playwright/tabs-back-button.spec.ts | 70 +++++++++++++++++++ 5 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 packages/vue/test/base/src/views/tabs/Tab2Parameter.vue create mode 100644 packages/vue/test/base/tests/e2e/playwright/tabs-back-button.spec.ts diff --git a/packages/vue-router/src/router.ts b/packages/vue-router/src/router.ts index 35ce3d55a94..dff0e34cee5 100644 --- a/packages/vue-router/src/router.ts +++ b/packages/vue-router/src/router.ts @@ -183,7 +183,32 @@ export const createIonRouter = ( * is not good because we would have two /tabs/tab1/child1 entries * separated by a /tabs/tab1/child2 entry. */ - router.go(prevInfo.position - routeInfo.position); + const positionDelta = prevInfo.position! - routeInfo.position!; + if (positionDelta < 0) { + router.go(positionDelta); + } else if (prevInfo.pathname) { + /** + * prevInfo's history position was wiped when the user went + * back then pushed a new route (FW-6472), so router.go can't + * reach it. Replace falls through to afterEach with the + * pop/back `incomingRouteParams` set above, which preserves + * the back animation and consumes the params so they don't + * leak into the next navigation. We replace even when + * `positionDelta === 0` for the same consumption reason. + */ + router.replace({ + path: prevInfo.pathname, + query: parseQuery(prevInfo.search), + }); + } else { + /** + * prevInfo has no pathname (synthesized root entry). Route + * to `defaultHref` so the pop/back `incomingRouteParams` + * set above gets consumed instead of leaking into the next + * navigation. + */ + handleNavigate(defaultHref, "pop", "back", routerAnimation); + } } } else { handleNavigate(defaultHref, "pop", "back", routerAnimation); diff --git a/packages/vue/test/base/src/router/index.ts b/packages/vue/test/base/src/router/index.ts index 37511871b81..c8fc905d428 100644 --- a/packages/vue/test/base/src/router/index.ts +++ b/packages/vue/test/base/src/router/index.ts @@ -140,6 +140,11 @@ const routes: Array = [ path: 'tab2', component: () => import('@/views/tabs/Tab2.vue') }, + { + path: 'tab2/:id', + component: () => import('@/views/tabs/Tab2Parameter.vue'), + props: true + }, { path: 'tab3', beforeEnter: (to, from, next) => { diff --git a/packages/vue/test/base/src/views/tabs/Tab2.vue b/packages/vue/test/base/src/views/tabs/Tab2.vue index 35976fa5e7a..b29be1d3d74 100644 --- a/packages/vue/test/base/src/views/tabs/Tab2.vue +++ b/packages/vue/test/base/src/views/tabs/Tab2.vue @@ -18,6 +18,10 @@ Go to /routing + + + Go to Tab 2 Child 1 + diff --git a/packages/vue/test/base/src/views/tabs/Tab2Parameter.vue b/packages/vue/test/base/src/views/tabs/Tab2Parameter.vue new file mode 100644 index 00000000000..8b9daae7a86 --- /dev/null +++ b/packages/vue/test/base/src/views/tabs/Tab2Parameter.vue @@ -0,0 +1,57 @@ + + + diff --git a/packages/vue/test/base/tests/e2e/playwright/tabs-back-button.spec.ts b/packages/vue/test/base/tests/e2e/playwright/tabs-back-button.spec.ts new file mode 100644 index 00000000000..cecfcdeaeaa --- /dev/null +++ b/packages/vue/test/base/tests/e2e/playwright/tabs-back-button.spec.ts @@ -0,0 +1,70 @@ +import { test, expect } from './utils/test-base'; +import { ionBackClick, ionPageVisible, ionPageHidden, ionPageDoesNotExist, tabClick } from './utils/test-utils'; + +/** + * Regression coverage for FW-6472. When the user has child pages open in + * two tabs and switches between them, the back button on the active tab's + * child must pop within that tab's stack, not jump to the other tab. + */ +test.describe('Tabs: back button after switching between tabs with child pages', () => { + test('back button on tab2 child still returns to tab2 root after multiple tab switches (FW-6472 scenario A)', async ({ page }) => { + await page.goto('/tabs'); + await ionPageVisible(page, 'tab1'); + + await page.locator('.ion-page[data-pageid="tab1"] #child-one').click(); + await ionPageVisible(page, 'tab1childone'); + + await tabClick(page, 'tab2'); + await ionPageVisible(page, 'tab2'); + await ionPageHidden(page, 'tab1childone'); + + await page.locator('.ion-page[data-pageid="tab2"] #child-one').click(); + await ionPageVisible(page, 'tab2childone'); + + await tabClick(page, 'tab1'); + await ionPageVisible(page, 'tab1childone'); + await ionPageHidden(page, 'tab2childone'); + + await tabClick(page, 'tab2'); + await ionPageVisible(page, 'tab2childone'); + await ionPageHidden(page, 'tab1childone'); + + await ionBackClick(page, 'tab2childone'); + + await expect.poll(() => new URL(page.url()).pathname).toBe('/tabs/tab2'); + await ionPageVisible(page, 'tab2'); + await ionPageDoesNotExist(page, 'tab2childone'); + }); + + // Scenario B: go back on tab1's child first, then re-enter tab2. Back on + // tab2's child must still land on /tabs/tab2 after that sequence. + test('back button on tab2 child works after going back in tab1 then re-entering tab2 (FW-6472 scenario B)', async ({ page }) => { + await page.goto('/tabs'); + await ionPageVisible(page, 'tab1'); + + await page.locator('.ion-page[data-pageid="tab1"] #child-one').click(); + await ionPageVisible(page, 'tab1childone'); + + await tabClick(page, 'tab2'); + await ionPageVisible(page, 'tab2'); + + await page.locator('.ion-page[data-pageid="tab2"] #child-one').click(); + await ionPageVisible(page, 'tab2childone'); + + await tabClick(page, 'tab1'); + await ionPageVisible(page, 'tab1childone'); + + await ionBackClick(page, 'tab1childone'); + await expect.poll(() => new URL(page.url()).pathname).toBe('/tabs/tab1'); + await ionPageVisible(page, 'tab1'); + + await tabClick(page, 'tab2'); + await ionPageVisible(page, 'tab2childone'); + + await ionBackClick(page, 'tab2childone'); + + await expect.poll(() => new URL(page.url()).pathname).toBe('/tabs/tab2'); + await ionPageVisible(page, 'tab2'); + await ionPageDoesNotExist(page, 'tab2childone'); + }); +}); From cc9fa878c524b9fa66fe839c4f219a83f402f5a5 Mon Sep 17 00:00:00 2001 From: Shane Date: Tue, 12 May 2026 09:53:26 -0700 Subject: [PATCH 2/2] chore(comments): clean up Co-authored-by: Maria Hutt --- packages/vue-router/src/router.ts | 2 +- .../test/base/tests/e2e/playwright/tabs-back-button.spec.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/vue-router/src/router.ts b/packages/vue-router/src/router.ts index dff0e34cee5..3c347b47a89 100644 --- a/packages/vue-router/src/router.ts +++ b/packages/vue-router/src/router.ts @@ -189,7 +189,7 @@ export const createIonRouter = ( } else if (prevInfo.pathname) { /** * prevInfo's history position was wiped when the user went - * back then pushed a new route (FW-6472), so router.go can't + * back then pushed a new route, so router.go can't * reach it. Replace falls through to afterEach with the * pop/back `incomingRouteParams` set above, which preserves * the back animation and consumes the params so they don't diff --git a/packages/vue/test/base/tests/e2e/playwright/tabs-back-button.spec.ts b/packages/vue/test/base/tests/e2e/playwright/tabs-back-button.spec.ts index cecfcdeaeaa..49bfc7bcdfd 100644 --- a/packages/vue/test/base/tests/e2e/playwright/tabs-back-button.spec.ts +++ b/packages/vue/test/base/tests/e2e/playwright/tabs-back-button.spec.ts @@ -2,12 +2,12 @@ import { test, expect } from './utils/test-base'; import { ionBackClick, ionPageVisible, ionPageHidden, ionPageDoesNotExist, tabClick } from './utils/test-utils'; /** - * Regression coverage for FW-6472. When the user has child pages open in + * When the user has child pages open in * two tabs and switches between them, the back button on the active tab's * child must pop within that tab's stack, not jump to the other tab. */ test.describe('Tabs: back button after switching between tabs with child pages', () => { - test('back button on tab2 child still returns to tab2 root after multiple tab switches (FW-6472 scenario A)', async ({ page }) => { + test('back button on tab2 child still returns to tab2 root after multiple tab switches', async ({ page }) => { await page.goto('/tabs'); await ionPageVisible(page, 'tab1'); @@ -36,7 +36,7 @@ test.describe('Tabs: back button after switching between tabs with child pages', await ionPageDoesNotExist(page, 'tab2childone'); }); - // Scenario B: go back on tab1's child first, then re-enter tab2. Back on + // Go back on tab1's child first, then re-enter tab2. Back on // tab2's child must still land on /tabs/tab2 after that sequence. test('back button on tab2 child works after going back in tab1 then re-entering tab2 (FW-6472 scenario B)', async ({ page }) => { await page.goto('/tabs');