diff --git a/packages/vue/src/components/IonRouterOutlet.ts b/packages/vue/src/components/IonRouterOutlet.ts
index ebd52b25cb8..6ab254557ad 100644
--- a/packages/vue/src/components/IonRouterOutlet.ts
+++ b/packages/vue/src/components/IonRouterOutlet.ts
@@ -217,9 +217,18 @@ export const IonRouterOutlet = /*@__PURE__*/ defineComponent({
requestAnimationFrame(async () => {
enteringEl.classList.add('ion-page-invisible');
+ const hasRootDirection = direction === undefined || direction === 'root' || direction === 'none';
const result = await ionRouterOutlet.value.commit(enteringEl, leavingEl, {
deepWait: true,
- duration: direction === undefined || direction === 'root' || direction === 'none' ? 0 : undefined,
+ /**
+ * replace operations result in a direction of none.
+ * These typically do not have need animations, so we set
+ * the duration to 0. However, if a developer explicitly
+ * passes an animationBuilder, we should assume that
+ * they want an animation to be played even
+ * though it is a replace operation.
+ */
+ duration: hasRootDirection && animationBuilder === undefined ? 0 : undefined,
direction,
showGoBack,
progressAnimation,
diff --git a/packages/vue/test/base/tests/unit/hooks.spec.ts b/packages/vue/test/base/tests/unit/hooks.spec.ts
index 3b6701ce2e1..0d2a45a65bf 100644
--- a/packages/vue/test/base/tests/unit/hooks.spec.ts
+++ b/packages/vue/test/base/tests/unit/hooks.spec.ts
@@ -203,7 +203,9 @@ describe('useIonRouter', () => {
await waitForRouter();
expect(router.currentRoute.value.path).toEqual('/page2');
- expect(animFn).not.toHaveBeenCalled();
+
+ // Animation should still be called even though this is a replace operation
+ expect(animFn).toHaveBeenCalled();
expect(vm.ionRouter.canGoBack()).toEqual(false);
})
diff --git a/packages/vue/test/base/tests/unit/router-outlet.spec.ts b/packages/vue/test/base/tests/unit/router-outlet.spec.ts
new file mode 100644
index 00000000000..cafd1580554
--- /dev/null
+++ b/packages/vue/test/base/tests/unit/router-outlet.spec.ts
@@ -0,0 +1,157 @@
+import { enableAutoUnmount, mount } from '@vue/test-utils';
+import { createRouter, createWebHistory } from '@ionic/vue-router';
+import {
+ IonicVue,
+ IonApp,
+ IonRouterOutlet,
+ IonPage,
+ useIonRouter,
+ createAnimation
+} from '@ionic/vue';
+import { onBeforeRouteLeave } from 'vue-router';
+import { mockAnimation, waitForRouter } from './utils';
+
+enableAutoUnmount(afterEach);
+
+const App = {
+ components: { IonApp, IonRouterOutlet },
+ template: '',
+}
+
+const BasePage = {
+ template: '',
+ components: { IonPage },
+}
+
+/**
+ * While these tests use useIonRouter,
+ * they are different from the tests in hook.spec.ts
+ * in that they are testing that the correct parameters
+ * are passed to IonRouterOutlet as opposed to hook.spec.ts
+ * which makes sure that the animation function is called when
+ * specifically using useIonRouter.
+ */
+describe('Routing', () => {
+ it('should have an animation duration of 0 if replacing without an explicit animation', async () => {
+ const Page1 = {
+ ...BasePage,
+ setup() {
+ const ionRouter = useIonRouter();
+ const redirect = () => {
+ ionRouter.replace('/page2')
+ }
+
+ return { redirect }
+ }
+ };
+
+ const Page2 = {
+ ...BasePage
+ };
+
+ const router = createRouter({
+ history: createWebHistory(process.env.BASE_URL),
+ routes: [
+ { path: '/', component: Page1 },
+ { path: '/page2', component: Page2 }
+ ]
+ });
+
+ router.push('/');
+ await router.isReady();
+ const wrapper = mount(App, {
+ global: {
+ plugins: [router, IonicVue]
+ }
+ });
+
+ /**
+ * Mock the commit function on IonRouterOutlet
+ */
+ const commitFn = jest.fn();
+ const routerOutlet = wrapper.findComponent(IonRouterOutlet);
+ routerOutlet.vm.$el.commit = commitFn;
+
+ // call redirect method on Page1
+ const cmp = wrapper.findComponent(Page1);
+ cmp.vm.redirect();
+ await waitForRouter();
+
+ expect(commitFn).toBeCalledWith(
+ /**
+ * We are not checking the first 2
+ * params in this test,
+ * so we can use expect.anything().
+ */
+ expect.anything(),
+ expect.anything(),
+ expect.objectContaining({
+ direction: "none",
+ duration: 0,
+ animationBuilder: undefined
+ })
+ )
+ });
+
+ it('should have an animation duration of null if replacing with an explicit animation', async () => {
+ const animation = mockAnimation();
+ const Page1 = {
+ ...BasePage,
+ setup() {
+ const ionRouter = useIonRouter();
+ const redirect = () => {
+ ionRouter.replace('/page2', animation)
+ }
+
+ return { redirect }
+ }
+ };
+
+ const Page2 = {
+ ...BasePage
+ };
+
+ const router = createRouter({
+ history: createWebHistory(process.env.BASE_URL),
+ routes: [
+ { path: '/', component: Page1 },
+ { path: '/page2', component: Page2 }
+ ]
+ });
+
+ router.push('/');
+ await router.isReady();
+ const wrapper = mount(App, {
+ global: {
+ plugins: [router, IonicVue]
+ }
+ });
+
+ /**
+ * Mock the commit function on IonRouterOutlet
+ */
+ const commitFn = jest.fn();
+ const routerOutlet = wrapper.findComponent(IonRouterOutlet);
+ routerOutlet.vm.$el.commit = commitFn;
+
+ // call redirect method on Page1
+ const cmp = wrapper.findComponent(Page1);
+ cmp.vm.redirect();
+ await waitForRouter();
+
+ expect(commitFn).toBeCalledWith(
+ /**
+ * We are not checking the first 2
+ * params in this test,
+ * so we can use expect.anything().
+ */
+ expect.anything(),
+ expect.anything(),
+ expect.objectContaining({
+ direction: "none",
+ duration: undefined,
+ animationBuilder: animation
+ })
+ )
+ });
+});
diff --git a/packages/vue/test/base/tests/unit/utils.ts b/packages/vue/test/base/tests/unit/utils.ts
index c49f54e7cc3..2fb0a6144e2 100644
--- a/packages/vue/test/base/tests/unit/utils.ts
+++ b/packages/vue/test/base/tests/unit/utils.ts
@@ -1,4 +1,5 @@
import { flushPromises } from '@vue/test-utils';
+import { createAnimation } from '@ionic/vue';
export const waitForRouter = async () => {
await flushPromises();
@@ -6,10 +7,5 @@ export const waitForRouter = async () => {
}
export const mockAnimation = () => {
- return jest.fn(() => {
- return {
- onFinish: () => {},
- play: () => {}
- }
- })
+ return jest.fn(() => createAnimation());
}