Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(vue): custom animation plays when replacing #25863

Merged
merged 12 commits into from
Sep 6, 2022
11 changes: 10 additions & 1 deletion packages/vue/src/components/IonRouterOutlet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 3 additions & 1 deletion packages/vue/test/base/tests/unit/hooks.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
})
Expand Down
157 changes: 157 additions & 0 deletions packages/vue/test/base/tests/unit/router-outlet.spec.ts
Original file line number Diff line number Diff line change
@@ -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: '<ion-app><ion-router-outlet /></ion-app>',
}

const BasePage = {
template: '<ion-page :data-pageid="name"></ion-page>',
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
})
)
});
});
8 changes: 2 additions & 6 deletions packages/vue/test/base/tests/unit/utils.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
import { flushPromises } from '@vue/test-utils';
import { createAnimation } from '@ionic/vue';

export const waitForRouter = async () => {
await flushPromises();
await new Promise((r) => setTimeout(r, 500));
}

export const mockAnimation = () => {
return jest.fn(() => {
return {
onFinish: () => {},
play: () => {}
}
})
return jest.fn(() => createAnimation());
}