Skip to content

Commit e023609

Browse files
thetaPCgzqby
andcommitted
fix(vue-router): clear navigation info when a guard aborts navigation
Co-authored-by: zhiqiang.guo <zguoby@gmail.com>
1 parent c689000 commit e023609

2 files changed

Lines changed: 130 additions & 2 deletions

File tree

packages/vue-router/src/router.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import type {
55
NavigationFailure,
66
RouteLocationRaw,
77
} from "vue-router";
8-
import { parseQuery } from "vue-router";
8+
import {
9+
isNavigationFailure,
10+
NavigationFailureType,
11+
parseQuery,
12+
} from "vue-router";
913

1014
import { createLocationHistory } from "./locationHistory";
1115
import type {
@@ -46,7 +50,28 @@ export const createIonRouter = (
4650
_: RouteLocationNormalized,
4751
failure?: NavigationFailure
4852
) => {
49-
if (failure) return;
53+
if (failure) {
54+
/*
55+
* vue-router reverts the history entry for aborted and duplicated
56+
* navigations, so the staged navigation info describes a history event
57+
* that no longer happened. Clearing it prevents a stale delta from
58+
* leaking into the next navigation, where it would be mistaken for
59+
* history traversal and stop the incoming route from being added.
60+
*
61+
* A cancelled navigation is superseded by another one and keeps its
62+
* history entry, so its info is still accurate and stays in place for
63+
* the superseding navigation to consume.
64+
*/
65+
if (!isNavigationFailure(failure, NavigationFailureType.cancelled)) {
66+
currentNavigationInfo = {
67+
direction: undefined,
68+
action: undefined,
69+
delta: undefined,
70+
};
71+
}
72+
73+
return;
74+
}
5075

5176
const { direction, action, delta } = currentNavigationInfo;
5277

packages/vue/test/base/tests/unit/routing.spec.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -689,4 +689,107 @@ describe('Routing', () => {
689689
expect(wrapper.findComponent(Page2).exists()).toBe(false);
690690
expect(wrapper.findComponent(Page3).exists()).toBe(false);
691691
});
692+
693+
// Verifies fix for https://github.com/ionic-team/ionic-framework/issues/29721
694+
it('should keep the view stack intact after a navigation guard blocks going back', async () => {
695+
const createPage = (id: string) => ({
696+
components: { IonPage },
697+
name: id,
698+
template: `<ion-page data-page="${id}"></ion-page>`
699+
});
700+
701+
const Home = createPage('home');
702+
const Register = createPage('register');
703+
const Profile = createPage('profile');
704+
705+
let isLoggedIn = false;
706+
707+
const router = createRouter({
708+
history: createWebHistory(process.env.BASE_URL),
709+
routes: [
710+
{ path: '/', redirect: '/home' },
711+
{ path: '/home', component: Home },
712+
{ path: '/register', component: Register },
713+
{ path: '/profile', component: Profile }
714+
]
715+
});
716+
717+
/*
718+
* Leaving the authenticated route while still logged in is blocked, which
719+
* aborts the navigation. An aborted back navigation used to leave stale
720+
* navigation info behind, which then made the next navigation look like
721+
* history traversal.
722+
*/
723+
router.beforeEach((to, from) => {
724+
if (from.path === '/profile' && to.path !== '/profile' && isLoggedIn) {
725+
return false;
726+
}
727+
728+
return true;
729+
});
730+
731+
router.push('/home');
732+
await router.isReady();
733+
const wrapper = mount(IonRouterOutlet, {
734+
global: {
735+
plugins: [router, IonicVue]
736+
}
737+
});
738+
739+
/*
740+
* Ionic keeps previously visited pages mounted so they can be animated back
741+
* to, hiding the inactive ones with `ion-page-hidden`. Asserting on the
742+
* whole stack therefore catches both a wrong visible page and a page that
743+
* was destroyed when it should have been kept.
744+
*/
745+
const viewStack = () =>
746+
wrapper.findAll('.ion-page').map((page) => ({
747+
id: page.attributes('data-page'),
748+
hidden: page.classes('ion-page-hidden')
749+
}));
750+
751+
router.push('/register');
752+
await waitForRouter();
753+
754+
isLoggedIn = true;
755+
router.replace('/profile');
756+
await waitForRouter();
757+
758+
expect(viewStack()).toEqual([
759+
{ id: 'home', hidden: true },
760+
{ id: 'profile', hidden: false }
761+
]);
762+
763+
// The guard blocks this, so the stack should be untouched.
764+
router.back();
765+
await waitForRouter();
766+
767+
expect(viewStack()).toEqual([
768+
{ id: 'home', hidden: true },
769+
{ id: 'profile', hidden: false }
770+
]);
771+
772+
/*
773+
* Logging out is a push, so Profile stays in the stack behind Home. Before
774+
* the fix the stale delta from the blocked back navigation made this look
775+
* like history traversal, which destroyed the Profile view.
776+
*/
777+
isLoggedIn = false;
778+
router.push('/home');
779+
await waitForRouter();
780+
781+
expect(viewStack()).toEqual([
782+
{ id: 'home', hidden: false },
783+
{ id: 'profile', hidden: true }
784+
]);
785+
786+
isLoggedIn = true;
787+
router.push('/profile');
788+
await waitForRouter();
789+
790+
expect(viewStack()).toEqual([
791+
{ id: 'home', hidden: true },
792+
{ id: 'profile', hidden: false }
793+
]);
794+
});
692795
});

0 commit comments

Comments
 (0)