How can I access functions within a VueJs component using vitest?

I am facing a challenge in testing a VueJS component as I am new to both Vue and Vitest. Despite my efforts, I have not been able to find a solution through online searches.

The component in question receives props and has two named slots.

The onmounted() function checks if the text within one of these slots vertically fits a set threshold in pixels. If it does not fit, a CSS class is added to the parent <div> and a button is displayed.

However, during testing, I noticed that the height of the elements always returns as 0. It seems that the rendering engine in vitest does not expose or compute element.clientHeight.

This issue results in the button, which I need to test its functionality, never being rendered.

I attempted to modify the variable controlling the button's visibility from the test using

wrapper.vm.isButtonVisible = true
(where isButtonVisible is a ref), but unfortunately, it did not work. This could be due to the script being defined under <script setup>.

It appears that functions and refs within the component are not accessible from my test suite. Here is a simplified version of the component and the relevant 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 moving isButtonVisible.value = true; to a separate function and calling it in the test, but there were no errors. Yet, wrapper.html() did not show the button, indicating that functions cannot be accessed either.

Edit (adding a sample test)

In the test, I attempted:

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

If anyone can suggest how to proceed or direct me to relevant documentation/examples, I would greatly appreciate it.

Answer №1

When working with the composition API, testing a component can be limited. Typically, it is best to treat it as a "graybox" and test it as a single unit. However, this approach presents a challenge because there is no actual DOM to test against.

In such scenarios, it becomes necessary to mock the clientHeight property of multiple elements. The issue arises when these elements are only available after the component has been mounted, and Vue test utils do not offer a straightforward way to manipulate the natural lifecycle. While it is possible to introduce additional mounted lifecycle hooks, ensuring their execution before the component's onMounted hook can be problematic. This may require restructuring the tested code to prioritize testability, potentially leading to unintended side effects like layout flickering:

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

The clientHeight property is read-only but can be overridden in JSDOM using the following approach:

  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>

Alternatively, instead of altering the behavior of the onMounted hook, one can mock the fake DOM behavior to align with the testing requirements. Given that clientHeight is accessed across multiple elements, a more sophisticated mocking strategy is needed to meet the testing 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>

Subsequently, a click event can be simulated on the button element to assess its impact on the visibility of a modal.

This approach relies on established conventions within Testing Library, which necessitate assigning unique data-testid attributes to tested elements for unambiguous DOM selection, as illustrated below:

<div data-testid="textWrapper" ref="textWrapper" class="detail-summary__text" ...>

Answer №2

I managed to come up with a solution after realizing I had forgotten to update the rendered component in the testing phase.

After implementing a nextTick() method, everything seems to be functioning as it should now.

it.only('should display the see more button', async () => {
    // action
    const wrapper = await mount(DetailSummary, {
        props: {
            [...]
        },
        renderStubDefaultSlot: true,
        slots: {
            default: 'text here',
        },
        stubs: [...],
    });
    // assertion
    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();
});

It may not be the most elegant solution, but it definitely gets the job done.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

What sets apart +variable+ and ${variable} in JavaScript?

Just starting to code and eager to learn. While I was taking in a lecture, I came across this interesting snippet of code: var coworkers = ['go', 'hello', 'hi', 'doit']; <script type="text/javascript&q ...

Node.js causing issues with retrieving data from REST Calls

I am trying to make a REST API call from my PHP and Node.js application to a specific URL provided by the client, which is expected to return a JSON object. While I am able to successfully retrieve data using PHP, I'm encountering issues with my Node ...

Discover and locate inactive element using Selenium in Python

Struggling with Automation: Unable to Locate Button on Website I'm currently facing a challenge in automating a website as I am unable to locate a specific button using Selenium. The problem lies in the fact that the button is disabled by default, ma ...

"Interested in learning how to remove a particular post by its unique id using Vuejs

I am trying to remove a post based on its id. When I manually specify an id like in the example below, it works. However, I want it to automatically fetch the post's id and delete it. The vue file : deletePost() { var postId = 37; axios.dele ...

When do you think the request is triggered if JSONP is essentially a dynamic script?

It appears that JSONP requests made using jQuery.ajax are not truly asynchronous, but rather utilize the Script DOM Element approach by adding a script tag to the page. This information was found on a discussion thread linked here: https://groups.google.co ...

Nuxt3: sending a user's submitted value from a registration form

I'm facing an issue in my Nuxt3 application where I need to pass the input value from an email field in a child component (SignForm.vue) to the parent component (signup.vue) for user registration. signup.vue <script setup> const props = { ...

Guide to installing angular-ui-bootstrap through npm in angularjs application

My goal is to incorporate angular-ui-bootstrap into my project using the following steps: 1. npm install angular-ui-bootstrap 2. import uiBootstrap from 'angular-ui-bootstrap'; 3. angular.module('app', [      uiBootstrap    ]) I ...

JavaScript - Loading image from local file on Linux machine

I have a server running and serving an HTML page, but I am trying to display an image from a local drive on a Linux machine. I've tried using file://.., but it doesn't seem to be working for me on Ubuntu 18.04. There are no errors, the img tag ju ...

Revamping the hyperlinks in my collapsible menu extension

Is there a way to modify the links in this accordion drop menu so that they lead to external HTML pages instead of specific areas on the same page? I've tried various methods but can't seem to achieve it without affecting the styles. Currently, i ...

Populating an item in a for each loop - JavaScript

In the following example, I am struggling to populate the object with the actual data that I have. var dataset = {}; $.each(data.body, function( index, value ) { dataset[index] = { label: 'hello', ...

Selenium fails to detect elements that are easily located by the browser using the aba elements, as well as through the use of JavaScript in the console

Looking to automate actions on the bet365 casino platform, but facing challenges with bot blocking mechanisms. Visit: Struggling to interact with elements within the "app-container" div using Selenium, however able to access them via JavaScript in the br ...

Showcasing the Contrast in Appearance between Chrome and Firefox on Wordpress

Is there anybody who can help me out? I am currently using a "pullout" widget on my Wordpress website to show an inline link that opens specific text in a prettyPhoto box with scrollbars. The setup works flawlessly in Google Chrome, but when I view the pa ...

Angular 4: Solving the 'Access-Control-Allow-Origin' issue specifically for post requests

I have searched for solutions to my problem on the topic, but haven't found one that works for me. As a newcomer to Angular 4, I am attempting to create an update/delete service using a custom external API. Initially, I encountered CORS issues which ...

Can I execute the mocha test suite from a node endpoint?

Our team has created a custom API to streamline internal web services within our organization. I've developed a comprehensive test suite using Mocha to ensure the integrity of the codebase, currently executing it through the command line interface. W ...

Toggle the visibility of a div and reset the input field when there is a change

In my form, I have two input fields named 'Apartment' and 'Bachelor'. Depending on the selected value, certain divs will be shown. If 'Apartment' is selected, related divs and select fields for apartments should show. The issu ...

Encountering a Jquery 404 error while attempting to locate a PHP file through Ajax requests

I've been struggling with this issue for the past couple of hours, but I just can't seem to get it fixed. Here's my current file setup: classes -html --index.html --front_gallery.php -javascript --gallery.js I've been following a tuto ...

Mastering the map() and reduce() functions in JavaScript: A step-by-step guide

I have been working on a simple function to calculate prime numbers using a NoSQL database. Despite trying different approaches, I kept encountering an error stating that the value I was searching for is not defined. Any feedback or suggestions would be gr ...

Days and Their Corresponding Names Listed in Table

Currently, I am working on creating a "timesheet" feature that requires the user to input 2 dates. The goal is to calculate each month and year based on the input dates and then display them in a dropdown menu combobox. I would like to dynamically update ...

Is using Javascript objects a better alternative to Redux?

I'm in the process of creating my initial application with React and have opted to integrate Redux into it. Since incorporating Redux, it appears that setting a component state to a basic global JavaScript object would be a simpler approach. I unders ...

Javascript does not execute properly in Google Apps Script

Encountering issues while attempting to execute the code provided in Google app script. // 1.foldername = Folder name | 3.templateId = ID of the template to use // 2.SheetId = ID of spreadsheet to use. | 4.rootFolder = ID of the root fold ...