diff --git a/packages/vue-router/src/router.ts b/packages/vue-router/src/router.ts index 35ce3d55a94..3c347b47a89 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, 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..49bfc7bcdfd --- /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'; + +/** + * 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', 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'); + }); + + // 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'); + }); +});