skip to Main Content

I’m in a situation where I don’t know how to test a VueJS component, I think essentially because both vue and vitest are new to me, but I cannot find any solution googling around.

I have one component that receives some props and two different named slots.

onmounted() checks if the text in one of this slots "fits" vertically a threshold in px that I set. If not, adds a CSS class to the parent <div> and shows a button.

What I see in my test is that the height of the elements is always returned as 0, I suppose because the rendering engine on vitest does not expose/compute element.clientHeight.

This causes the button whose functionality I need to test to never be rendered.

I tried to change the variable that toggles the button’s visibility from the test, using wrapper.vm.isButtonVisible = true (isButtonVisible is a ref), but without success, I suppose because the script is defined as <script setup>.

Looks like functions and refs in the component are not accessible from my test suite. Here is a simplified version of the component and the test:

<template>
    <div ref="textWrapper" class="detail-summary__text" :class="{'truncated': isButtonVisible}">
        <div ref="textContainer" class="detail-summary__inner-text">
            <slot name="default"></slot>
        </div>
    </div>
    <div class="detail-summary__button">
        <button
            v-if="isButtonVisible"
            @click="toggleModal"
        >Show more</button>
    </div>
</template>
<script setup lang="ts">
const textContainer: Ref<DomElement> = ref(null);
const textWrapper: Ref<DomElement> = ref(null);
const textModal: Ref<DomElement> = ref(null);
const isButtonVisible: Ref<boolean> = ref(false);
const isModalOpen: Ref<boolean> = ref(false);

onMounted(() => {
    checkContainerHeight();
});

function checkContainerHeight():void {
    let textInnerHeight = textHeight.value;
    let textWrapperHeight = textHeight.value;
    if (textWrapper.value != null && textWrapper.value.clientHeight != null) {
        textWrapperHeight = textWrapper.value.clientHeight;
    }
    if (textContainer.value != null && textContainer.value.clientHeight != null) {
        textInnerHeight = textContainer.value.clientHeight;
        if (textInnerHeight > textWrapperHeight) {
            makeButtonVisible();
        }
    }
}

function makeButtonVisible(): void {
    isButtonVisible.value = true;
}

function toggleModal(): void {
    isModalOpen.value = !isModalOpen.value;
}
</script>

I also tried to move isButtonVisible.value = true; to a function, and call it in the test, but I get no error, and anyway wrapper.html() does not contain the button, so I guess I cannot access functions either.

Edit (adding a sample test)

In the test, I tried:

it.only('should show the see more button', async () => {
    // when
    const wrapper = await mount(DetailSummary, {
        props: {
            [...]
        },
        slots: {
            default: 'text here',
        },
        stubs: [...],
        shallow: true,
    });
    // then

    wrapper.vm.makeButtonVisible() // I see the console that I added to the function
    console.log(wrapper.html()); // The snapshot still doesn't show the button

    const e = wrapper.findComponent({ name: 'DetailSummary' });
    e.vm.makeButtonVisible(); // If I add a console in the function, I see it to be called, even if the linter says that that method does not exist
    console.log(wrapper.html()); // The snapshot still doesn't show the button
});

Can someone suggest me how to proceed, or point me to some docs/examples?

2

Answers


  1. Chosen as BEST ANSWER

    I might have found a solution. In the test, I was missing some update to the rendered component.

    It now looks like this, and seems to be working. The trick was the nextTick() method.

    it.only('should show the see more button', async () => {
        // when
        const wrapper = await mount(DetailSummary, {
            props: {
                [...]
            },
            renderStubDefaultSlot: true,
            slots: {
                default: 'text here',
            },
            stubs: [...],
        });
        // then
        wrapper.vm.makeButtonVisible();
        await wrapper.vm.$nextTick(); // HERE
        const button = wrapper.find('[data-testid="detail-summary__show-more"]');
        expect(button.exists()).toBe(true);
        expect(wrapper.vm.isModalOpen).toBe(false);
        button.trigger('click');
        await wrapper.vm.$nextTick();
        expect(wrapper.vm.isModalOpen).toBe(true);
        expect(wrapper.element).toMatchSnapshot();
    });
    

    I don't know if this is the right/most elegant way, but it seems to be working.


  2. The use of composition API limits a way a component can be tested. As a rule of thumb, it’s supposed to be treated as a graybox and tested as a single unit.
    This also creates a problem because there’s no real DOM to test against.

    In this case it’s necessary to mock clientHeight of multiple elements. The problem is that the elements are available only after the component has been mounted, and Vue test utils don’t provide a way to interfere the natural lifecycle. It’s possible to provide additional mounted lifecycle hooks but it’s impossible to guarantee their execution before the component’s onMounted. Doing this would require to postpone the tested code to make it more testable, which is allowed but can result in unwanted side effects like layout blinking:

      onMounted(() => {
        nextTick(() => { checkContainerHeight() });
      });
    

    clientHeight is readonly but can be overridden in JSDOM with:

      const wrapper = mount(...);
    
      vi.spyOn(wrapper.find('[data-testid=textWrapper]').wrapperElement, 'clientHeight', 'get').mockReturnValue(100);
    
      vi.spyOn(wrapper.find('[data-testid=textContainer]').wrapperElement, 'clientHeight', 'get').mockReturnValue(200);
    
      await nextTick();
    
      // assert <button>
    

    A less limiting way is to not modify the way onMounted works but mock the way fake DOM works to fit the test. Since clientHeight is read multiple times on multiple elements, this requires to to mock it in a smarter way to meet the expectations:

      const clientHeightGetter = vi.spyOn(Element.prototype, 'clientHeight', 'get').mockImplementation(function () {
        if (this.dataset.testid === 'textWrapper')
          return 100;
        if (this.dataset.testid === 'textContainer')
          return 200;
    
        return 0;
      });
      
      const wrapper = mount(...);
    
      expect(clientHeightGetter).toHaveBeenCalledTimes(4)
    
      // assert <button>
    

    Then click can be triggered on button element to test how it triggers the visibility of a modal.

    This relies on the convention used with Testing Library, etc that expects unique data-testid attributes to be added to tested elements to make the selection of DOM elements unambiguous, e.g.:

    <div data-testid="textWrapper" ref="textWrapper" class="detail-summary__text" ...>
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search