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()); }