import { mount, shallowMount } from '@vue/test-utils'; import { nextTick } from 'vue'; import { hasHorizontalOverflow } from '~/lib/utils/dom_utils'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; const MOCK_TITLE = 'lorem-ipsum-dolar-sit-amit-consectur-adipiscing-elit-sed-do'; const SHORT_TITLE = 'my-text'; const createChildElement = () => `${MOCK_TITLE}`; jest.mock('~/lib/utils/dom_utils', () => ({ ...jest.requireActual('~/lib/utils/dom_utils'), hasHorizontalOverflow: jest.fn().mockImplementation(() => { throw new Error('this needs to be mocked'); }), })); describe('TooltipOnTruncate component', () => { let wrapper; let parent; const createComponent = ({ propsData, ...options } = {}) => { wrapper = shallowMount(TooltipOnTruncate, { propsData: { title: MOCK_TITLE, ...propsData, }, slots: { default: [MOCK_TITLE], }, directives: { GlTooltip: createMockDirective(), GlResizeObserver: createMockDirective(), }, ...options, }); }; const createWrappedComponent = ({ propsData, ...options }) => { const WrappedTooltipOnTruncate = { ...TooltipOnTruncate, directives: { ...TooltipOnTruncate.directives, GlTooltip: createMockDirective(), GlResizeObserver: createMockDirective(), }, }; // set a parent around the tested component parent = mount( { props: { title: { default: '' }, }, template: `
{{title}}
`, components: { TooltipOnTruncate: WrappedTooltipOnTruncate, }, }, { propsData: { ...propsData }, ...options, }, ); wrapper = parent.findComponent(WrappedTooltipOnTruncate); }; const getTooltipValue = () => getBinding(wrapper.element, 'gl-tooltip')?.value; const resize = async ({ truncate }) => { hasHorizontalOverflow.mockReturnValueOnce(truncate); getBinding(wrapper.element, 'gl-resize-observer').value(); await nextTick(); }; afterEach(() => { wrapper.destroy(); }); describe('when truncated', () => { beforeEach(async () => { hasHorizontalOverflow.mockReturnValueOnce(true); createComponent(); }); it('renders tooltip', async () => { expect(hasHorizontalOverflow).toHaveBeenLastCalledWith(wrapper.element); expect(getTooltipValue()).toMatchObject({ title: MOCK_TITLE, placement: 'top', disabled: false, }); expect(wrapper.classes('js-show-tooltip')).toBe(true); }); }); describe('with default target', () => { beforeEach(async () => { hasHorizontalOverflow.mockReturnValueOnce(false); createComponent(); }); it('does not render tooltip if not truncated', () => { expect(hasHorizontalOverflow).toHaveBeenLastCalledWith(wrapper.element); expect(getTooltipValue()).toMatchObject({ disabled: true, }); expect(wrapper.classes('js-show-tooltip')).toBe(false); }); it('renders tooltip on resize', async () => { await resize({ truncate: true }); expect(getTooltipValue()).toMatchObject({ disabled: false, }); await resize({ truncate: false }); expect(getTooltipValue()).toMatchObject({ disabled: true, }); }); }); describe('with child target', () => { it('renders tooltip if truncated', async () => { hasHorizontalOverflow.mockReturnValueOnce(true); createComponent({ propsData: { truncateTarget: 'child', }, slots: { default: createChildElement(), }, }); expect(hasHorizontalOverflow).toHaveBeenLastCalledWith(wrapper.element.childNodes[0]); await nextTick(); expect(getTooltipValue()).toMatchObject({ title: MOCK_TITLE, placement: 'top', disabled: false, }); }); it('does not render tooltip if normal', async () => { hasHorizontalOverflow.mockReturnValueOnce(false); createComponent({ propsData: { truncateTarget: 'child', }, slots: { default: createChildElement(), }, }); expect(hasHorizontalOverflow).toHaveBeenLastCalledWith(wrapper.element.childNodes[0]); await nextTick(); expect(getTooltipValue()).toMatchObject({ disabled: true, }); }); }); describe('with fn target', () => { it('renders tooltip if truncated', async () => { hasHorizontalOverflow.mockReturnValueOnce(true); createComponent({ propsData: { truncateTarget: (el) => el.childNodes[1], }, slots: { default: [createChildElement(), createChildElement()], }, }); expect(hasHorizontalOverflow).toHaveBeenLastCalledWith(wrapper.element.childNodes[1]); await nextTick(); expect(getTooltipValue()).toMatchObject({ disabled: false, }); }); }); describe('placement', () => { it('sets placement when tooltip is rendered', () => { const mockPlacement = 'bottom'; hasHorizontalOverflow.mockReturnValueOnce(true); createComponent({ propsData: { placement: mockPlacement, }, }); expect(hasHorizontalOverflow).toHaveBeenLastCalledWith(wrapper.element); expect(getTooltipValue()).toMatchObject({ placement: mockPlacement, }); }); }); describe('updates when title and slot content changes', () => { describe('is initialized with a long text', () => { beforeEach(async () => { hasHorizontalOverflow.mockReturnValueOnce(true); createWrappedComponent({ propsData: { title: MOCK_TITLE }, }); await nextTick(); }); it('renders tooltip', () => { expect(getTooltipValue()).toMatchObject({ title: MOCK_TITLE, placement: 'top', disabled: false, }); }); it('does not render tooltip after updated to a short text', async () => { hasHorizontalOverflow.mockReturnValueOnce(false); parent.setProps({ title: SHORT_TITLE, }); await nextTick(); await nextTick(); // wait 2 times to get an updated slot expect(getTooltipValue()).toMatchObject({ title: SHORT_TITLE, disabled: true, }); }); }); describe('is initialized with a short text that does not overflow', () => { beforeEach(async () => { hasHorizontalOverflow.mockReturnValueOnce(false); createWrappedComponent({ propsData: { title: MOCK_TITLE }, }); await nextTick(); }); it('does not render tooltip', () => { expect(getTooltipValue()).toMatchObject({ title: MOCK_TITLE, disabled: true, }); }); it('renders tooltip after text is updated', async () => { hasHorizontalOverflow.mockReturnValueOnce(true); parent.setProps({ title: SHORT_TITLE, }); await nextTick(); await nextTick(); // wait 2 times to get an updated slot expect(getTooltipValue()).toMatchObject({ title: SHORT_TITLE, disabled: false, }); }); }); }); });