@maxpike/vue
Version:
Vue VariantJS: Fully configurable Vue 3 components styled with TailwindCSS
689 lines (518 loc) • 18.6 kB
text/typescript
/* eslint-disable @typescript-eslint/no-explicit-any */
import { shallowMount, mount } from '@vue/test-utils';
import * as bodyScrollLockModule from 'body-scroll-lock';
import { TModalConfig } from '@variantjs/core';
import TModal from '@/components/TModal.vue';
import plugin from '../../plugin';
const waitUntilModalIsVisible = (wrapper: any) : Promise<void> => new Promise((resolve) => {
// 1. Component is added to the DOM
wrapper.vm.$nextTick().then(() => {
// 2. Overlay is about to show
wrapper.vm.$nextTick().then(() => {
// 3. Overlay is shown, modal is about to show
wrapper.vm.$nextTick().then(() => {
// 4 Modal is shown
wrapper.vm.$nextTick().then(() => {
resolve();
});
});
});
});
});
describe('TModal.vue', () => {
it('doesnt show the component by default', () => {
const wrapper = shallowMount(TModal);
expect(wrapper.vm.$el.tagName).toBeUndefined();
});
describe('opening modal', () => {
const props = {
teleport: false,
};
describe('with the `show` method', () => {
it('show the component when calling the show method', async () => {
const wrapper = mount(TModal, {
props,
});
expect(wrapper.vm.showComponent).toBe(false);
wrapper.vm.show();
await wrapper.vm.$nextTick();
expect(wrapper.vm.showComponent).toBe(true);
});
it('pass parameters from the show method to the before-show event', async () => {
const wrapper = mount(TModal, {
props,
});
const params = {
foo: 'bar',
};
expect(wrapper.vm.showComponent).toBe(false);
wrapper.vm.show(params);
await wrapper.vm.$nextTick();
expect(wrapper.vm.showComponent).toBe(true);
expect((wrapper.emitted('before-show')![0] as any)[0].params).toEqual(params);
});
it('cancel the show if the cancel method is called', async () => {
const wrapper = mount(TModal, {
props: {
...props,
},
});
const emitSpy = jest.spyOn(wrapper.vm.$, 'emit').mockImplementation((name, ...params) => {
if (name === 'before-show') {
(params as any).cancel();
}
});
wrapper.vm.show();
await wrapper.vm.$nextTick();
expect(wrapper.vm.showComponent).toBe(false);
emitSpy.mockRestore();
});
});
describe('with the $modal global property', () => {
it('show the component when calling the show method', async () => {
const wrapper = mount(TModal, {
props: {
...props,
name: 'modal-name',
},
global: {
plugins: [plugin],
},
});
expect(wrapper.vm.showComponent).toBe(false);
wrapper.vm.$modal.show('modal-name');
await wrapper.vm.$nextTick();
expect(wrapper.vm.showComponent).toBe(true);
});
it('doesnt show the component when calling the show method if different name', async () => {
const wrapper = mount(TModal, {
props: {
...props,
name: 'other-name',
},
global: {
plugins: [plugin],
},
});
expect(wrapper.vm.showComponent).toBe(false);
wrapper.vm.$modal.show('modal-name');
await wrapper.vm.$nextTick();
expect(wrapper.vm.showComponent).toBe(false);
});
it('pass parameters from the show method to the before-show event', async () => {
const wrapper = mount(TModal, {
props: {
...props,
name: 'modal-name',
},
global: {
plugins: [plugin],
},
});
const params = {
foo: 'bar',
};
expect(wrapper.vm.showComponent).toBe(false);
wrapper.vm.$modal.show('modal-name', params);
await wrapper.vm.$nextTick();
expect(wrapper.vm.showComponent).toBe(true);
expect((wrapper.emitted('before-show')![0] as any)[0].params).toEqual(params);
});
it('cancel the show if the cancel method is called', async () => {
const wrapper = mount(TModal, {
props: {
...props,
name: 'modal-name',
},
global: {
plugins: [plugin],
},
});
const emitSpy = jest.spyOn(wrapper.vm.$, 'emit').mockImplementation((name, ...params) => {
if (name === 'before-show') {
(params as any).cancel();
}
});
wrapper.vm.$modal.show('modal-name');
await wrapper.vm.$nextTick();
expect(wrapper.vm.showComponent).toBe(false);
emitSpy.mockRestore();
});
});
describe('with the vModel', () => {
it('show the component when updated the v-model to true', async () => {
const wrapper = mount(TModal, {
props,
});
expect(wrapper.vm.showComponent).toBe(false);
wrapper.setProps({
modelValue: true,
});
await wrapper.vm.$nextTick();
expect(wrapper.vm.showComponent).toBe(true);
});
});
});
describe('hiding modal', () => {
const props = {
teleport: false,
modelValue: true,
};
describe('with the `hide` method', () => {
it('hides the component when calling the hide method', async () => {
const wrapper = mount(TModal, {
props,
});
expect(wrapper.vm.showModal).toBe(true);
wrapper.vm.hide();
await wrapper.vm.$nextTick();
expect(wrapper.vm.showModal).toBe(false);
});
it('cancel the hide if the cancel method is called', async () => {
const wrapper = mount(TModal, {
props,
});
const emitSpy = jest.spyOn(wrapper.vm.$, 'emit').mockImplementation((name, ...params) => {
if (name === 'before-hide') {
(params as any).cancel();
}
});
wrapper.vm.hide();
await wrapper.vm.$nextTick();
expect(wrapper.vm.showModal).toBe(true);
emitSpy.mockRestore();
});
});
describe('with the $modal global property', () => {
it('show the component when calling the show method', async () => {
const wrapper = mount(TModal, {
props: {
...props,
name: 'modal-name',
},
global: {
plugins: [plugin],
},
});
expect(wrapper.vm.showModal).toBe(true);
wrapper.vm.$modal.hide('modal-name');
await wrapper.vm.$nextTick();
expect(wrapper.vm.showModal).toBe(false);
});
it('doesnt show the component when calling the show method if different name', async () => {
const wrapper = mount(TModal, {
props: {
...props,
name: 'other-name',
},
global: {
plugins: [plugin],
},
});
expect(wrapper.vm.showModal).toBe(true);
wrapper.vm.$modal.hide('modal-name');
await wrapper.vm.$nextTick();
expect(wrapper.vm.showModal).toBe(true);
});
it('cancel the show if the cancel method is called', async () => {
const wrapper = mount(TModal, {
props: {
...props,
name: 'modal-name',
},
global: {
plugins: [plugin],
},
});
const emitSpy = jest.spyOn(wrapper.vm.$, 'emit').mockImplementation((name, ...params) => {
if (name === 'before-hide') {
(params as any).cancel();
}
});
wrapper.vm.$modal.hide('modal-name');
await wrapper.vm.$nextTick();
expect(wrapper.vm.showModal).toBe(true);
emitSpy.mockRestore();
});
});
describe('with the vModel', () => {
it('hides the component when updated the v-model to false', async () => {
const wrapper = mount(TModal, {
props,
});
expect(wrapper.vm.showModal).toBe(true);
wrapper.setProps({
modelValue: false,
});
await wrapper.vm.$nextTick();
expect(wrapper.vm.showModal).toBe(false);
});
});
});
describe('modal is shown initially', () => {
const props = {
modelValue: true,
teleport: false,
};
it('show the component if modelValue is set to `true`', () => {
const wrapper = mount(TModal, {
props,
slots: {
default: 'Hello World',
},
});
expect(wrapper.html()).toContain('Hello World');
});
describe('disable body scroll', () => {
it('disables the body scroll', () => {
const disableBodyScrollSpy = jest.spyOn(bodyScrollLockModule, 'disableBodyScroll');
mount(TModal, {
props,
});
expect(disableBodyScrollSpy).toHaveBeenCalled();
disableBodyScrollSpy.mockRestore();
});
it('doesnt disabled the body scroll if `disableBodyScroll` is set to `false`', () => {
const disableBodyScrollSpy = jest.spyOn(bodyScrollLockModule, 'disableBodyScroll');
mount(TModal, {
props: {
...props,
disableBodyScroll: false,
},
});
expect(disableBodyScrollSpy).not.toHaveBeenCalled();
disableBodyScrollSpy.mockRestore();
});
});
describe('enable body scroll', () => {
it('enables body scroll when unmounted', () => {
const enableBodyScrollSpy = jest.spyOn(bodyScrollLockModule, 'enableBodyScroll');
const wrapper = mount(TModal, {
props,
});
wrapper.unmount();
expect(enableBodyScrollSpy).toHaveBeenCalled();
enableBodyScrollSpy.mockRestore();
});
it('doesnt enables body scroll when unmounted if `disableBodyScroll` is set to `false`', () => {
const enableBodyScrollSpy = jest.spyOn(bodyScrollLockModule, 'enableBodyScroll');
const wrapper = mount(TModal, {
props: {
...props,
disableBodyScroll: false,
},
});
wrapper.unmount();
expect(enableBodyScrollSpy).not.toHaveBeenCalled();
enableBodyScrollSpy.mockRestore();
});
it('enables body scroll when modal is closed', async () => {
const enableBodyScrollSpy = jest.spyOn(bodyScrollLockModule, 'enableBodyScroll');
const wrapper = mount(TModal, {
props,
});
wrapper.vm.hide();
// Whole close lifecycle
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
expect(enableBodyScrollSpy).toHaveBeenCalled();
enableBodyScrollSpy.mockRestore();
});
it('doesnt enables body scroll when modal is closed if `disableBodyScroll` is set to `false`', async () => {
const enableBodyScrollSpy = jest.spyOn(bodyScrollLockModule, 'enableBodyScroll');
const wrapper = mount(TModal, {
props: {
...props,
disableBodyScroll: false,
},
});
wrapper.vm.hide();
// Whole close lifecycle
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
expect(enableBodyScrollSpy).not.toHaveBeenCalled();
enableBodyScrollSpy.mockRestore();
});
});
describe('focus overlay', () => {
it('focus the overlay', () => {
const focusSpy = jest.spyOn(HTMLElement.prototype, 'focus');
mount(TModal, {
props,
});
expect(focusSpy).toHaveBeenCalled();
focusSpy.mockRestore();
});
it('doesnt focus the overlay if `focusOnOpen` is set to `false`', () => {
const focusSpy = jest.spyOn(HTMLElement.prototype, 'focus');
mount(TModal, {
props: {
...props,
focusOnOpen: false,
},
});
expect(focusSpy).not.toHaveBeenCalled();
focusSpy.mockRestore();
});
});
it('emits the hide-related events in order', async () => {
const wrapper = mount(TModal, {
props,
});
expect(wrapper.emitted('before-hide')).toBeFalsy();
expect(wrapper.emitted('hidden')).toBeFalsy();
wrapper.vm.hide();
// After press hidden it just change the modelValue, no events yet
expect(wrapper.emitted('before-hide')).toBeFalsy();
expect(wrapper.emitted('hidden')).toBeFalsy();
// Model is about to hide
await wrapper.vm.$nextTick();
expect(wrapper.emitted('before-hide')).toBeTruthy();
expect(Object.keys((wrapper.emitted('before-hide')![0] as any)[0])).toEqual([
'cancel',
'reason',
]);
expect(wrapper.emitted('hidden')).toBeFalsy();
// Modal is hidden, overlay is about to hide (no new events emitted)
await wrapper.vm.$nextTick();
expect(wrapper.emitted('before-hide')).toBeTruthy();
expect(wrapper.emitted('hidden')).toBeFalsy();
// Overlay is hidden, component is about to be removed from the DOM (no new events emitted)
await wrapper.vm.$nextTick();
expect(wrapper.emitted('before-hide')).toBeTruthy();
expect(wrapper.emitted('hidden')).toBeFalsy();
// component is removed from the DOM
await wrapper.vm.$nextTick();
expect(wrapper.emitted('before-hide')).toBeTruthy();
expect(wrapper.emitted('hidden')).toBeTruthy();
});
describe('press esc key', () => {
it('hides the modal when press esc', async () => {
const wrapper = mount(TModal, {
props,
});
wrapper.vm.$refs.overlay.dispatchEvent(new KeyboardEvent('keydown', {
key: 'Escape',
}));
await wrapper.vm.$nextTick();
// Meaning the modal was hidden
expect(wrapper.vm.$.setupState.showModal).toBe(false);
});
it('doesnt hide the modal when press esc if `escToClose` is set to `false`', async () => {
const wrapper = mount(TModal, {
props: {
...props,
escToClose: false,
},
});
wrapper.vm.$refs.overlay.dispatchEvent(new KeyboardEvent('keydown', {
key: 'Escape',
}));
await wrapper.vm.$nextTick();
// Meaning the modal was hidden
expect(wrapper.vm.$.setupState.showModal).toBe(true);
});
});
describe('user clicks the overlay (outside)', () => {
it('hides the modal when clicks outside', async () => {
const wrapper = mount(TModal, {
props,
});
wrapper.vm.$refs.overlay.dispatchEvent(new MouseEvent('click'));
await wrapper.vm.$nextTick();
// Meaning the modal was hidden
expect(wrapper.vm.$.setupState.showModal).toBe(false);
});
it('doesnt hides the modal when clicks the modal', async () => {
const wrapper = mount(TModal, {
props,
});
wrapper.vm.$refs.modal.dispatchEvent(new MouseEvent('click'));
await wrapper.vm.$nextTick();
// Meaning the modal was hidden
expect(wrapper.vm.$.setupState.showModal).toBe(true);
});
it('doesnt hide the modal when click outside if `clickToClose` is set to `false`', async () => {
const wrapper = mount(TModal, {
props: {
...props,
clickToClose: false,
},
});
wrapper.vm.$refs.overlay.dispatchEvent(new MouseEvent('click'));
await wrapper.vm.$nextTick();
// Meaning the modal was hidden
expect(wrapper.vm.$.setupState.showModal).toBe(true);
});
});
});
describe('modal is hidden initially', () => {
const props = {
teleport: false,
};
it('focus the overlay when shown', async () => {
const focusSpy = jest.spyOn(HTMLElement.prototype, 'focus');
const wrapper = mount(TModal, {
props,
});
expect(focusSpy).not.toHaveBeenCalled();
wrapper.vm.show();
await waitUntilModalIsVisible(wrapper);
expect(focusSpy).toHaveBeenCalled();
focusSpy.mockRestore();
});
it('emits the open-related events in order', async () => {
const wrapper = mount(TModal, {
props,
});
expect(wrapper.emitted('before-show')).toBeFalsy();
expect(wrapper.emitted('shown')).toBeFalsy();
wrapper.vm.show();
// After press shown it just adds the component into the DOM, no events emitted yet
expect(wrapper.emitted('before-show')).toBeFalsy();
expect(wrapper.emitted('shown')).toBeFalsy();
// Component is added to the DOM, but not shown yet
await wrapper.vm.$nextTick();
expect(wrapper.emitted('before-show')).toBeTruthy();
expect(Object.keys((wrapper.emitted('before-show')![0] as any)[0])).toEqual([
'cancel',
'params',
]);
expect(wrapper.emitted('shown')).toBeFalsy();
// Overlay is about to show (no new events emitted)
await wrapper.vm.$nextTick();
expect(wrapper.emitted('before-show')).toBeTruthy();
expect(wrapper.emitted('shown')).toBeFalsy();
// Overlay is shown, modal is about to show (no new events emitted)
await wrapper.vm.$nextTick();
expect(wrapper.emitted('before-show')).toBeTruthy();
expect(wrapper.emitted('shown')).toBeFalsy();
// Modal is shown
await wrapper.vm.$nextTick();
expect(wrapper.emitted('before-show')).toBeTruthy();
expect(wrapper.emitted('shown')).toBeTruthy();
});
});
describe('classes', () => {
const props = {
teleport: false,
};
it('creates the overlayTransitionClassesList computed property from default configuration', () => {
const wrapper = mount(TModal, {
props,
});
expect(wrapper.vm.overlayTransitionClassesList).toEqual({
enterActiveClass: TModalConfig.classes.overlayEnterActiveClass,
enterFromClass: TModalConfig.classes.overlayEnterFromClass,
enterToClass: TModalConfig.classes.overlayEnterToClass,
leaveActiveClass: TModalConfig.classes.overlayLeaveActiveClass,
leaveFromClass: TModalConfig.classes.overlayLeaveFromClass,
leaveToClass: TModalConfig.classes.overlayLeaveToClass,
});
});
});
});