Issues arise when trying to update the modelValue in unit tests for Vue3 Composition API

Recently delving into Vue, I am currently engaged in writing unit tests for a search component incorporated in my project. Basically, when the user inputs text in the search field, a small X icon emerges on the right side of the input box. Clicking this X icon resets the input field back to an empty state.

The component utilizes the composition API and is performing as expected. I can observe the emitted events and payloads using the Vue dev tools. However, I am encountering difficulty in detecting these events using Vitest. The majority of my tests are failing, and I am uncertain about the error in my approach.

To aid in clarity, I have crafted a replica of the component with scoped styling for easy mounting if required. Here is the component utilizing Vue3 Composition API, TypeScript, Vite, Vitest, and vue-test-utils.

Component code snippet:

<template>
    <div class="searchBar">
        <input
            :value="modelValue"
            class="searchInput"
            @input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
            autocomplete="off"
            data-test="searchInput"
        />

        <button
            v-if="modelValue"
            @click="clear($event)"
            class="clearIcon"
            ariaLabel="Clear Search"
            data-test="clearIcon"
        >
            <i class="fa fa-times"></i>
        </button>
    </div>
</template>

<script lang="ts">
import {
    defineComponent,
    watch,
} from "vue";

export default defineComponent({
    name: "SOComponent",
    props: {
    modelValue: {
        type: [String, Number],
        },
    },
    emits: [
        "update:modelValue",
        "search",
        "clear",
    ],
    setup(props, { emit }) {

        function clear(event: Event) {
            emit("clear", event);
            emit("update:modelValue", "");
        }

        watch(
            () => props.modelValue,
            (newValue, oldValue) => {
                if (newValue !== oldValue) {
                    emit("search", newValue);
                }
            }
        );

        return {
            clear,
        };
        },
    });
</script>

<style scoped>
    .searchBar {
        display: flex;
        justify-content: space-between;
        align-items: center;
        background-color: white;
        border: 2px solid black;
        border-radius: 1rem;
    }

    .searchInput {
        border: none;
        width: 100%;
        outline: none;
        color: black;
        font-size: 1rem;
        padding: 1rem;
        background-color: transparent;
    }

    .clearIcon {
        display: flex;
        align-items: center;
        justify-content: center;
        margin-right: 1rem;
        background-color: red;
        border: none;
        color: white;
        border-radius: 1rem;
        padding: 6.5px 9px;
        font-size: 1rem;
    }

    .clearIcon:hover {
        background-color: darkred;
    }
</style>

Below are the unit tests:

import { describe, it, expect, vi, afterEach } from 'vitest'
import { shallowMount } from '@vue/test-utils'
import SOComponent from '../StackOverflowComponent.vue'

describe('SOComponent Component Tests', () => {

    // Wrapper Factory
    let wrapper: any

    function createComponent() {
        wrapper = shallowMount(SOComponent, {
            attachTo: document.body
        })
    }

    afterEach(() => {
        wrapper.unmount()
    })

    // Helper Finder Functions
    const searchInput = () => wrapper.find('[data-test="searchInput"]')
    const clearIcon = () => wrapper.find('[data-test="clearIcon"]')

    describe('component rendering', () => {

        it('component renders as intended when created', () => {
            createComponent()
            expect(searchInput().exists()).toBe(true)
            expect(clearIcon().exists()).toBe(false)
        })

        it('clear icon is displayed when input field has value', async () => {
            createComponent()
            await searchInput().setValue('render X')
            expect(clearIcon().exists()).toBe(true)
        })

        it('clear icon is not displayed when input field has no value', async () => {
            createComponent()
            await searchInput().setValue('')
            expect(clearIcon().exists()).toBe(false)
        })
    })

    describe('component emits and methods', () => {

        it('update:modelValue emits input value', async () => {
            createComponent()
            await searchInput().setValue('emit me')
            expect(wrapper.emitted('update:modelValue')).toBeTruthy()
            expect(wrapper.emitted('update:modelValue')![0]).toEqual(['emit me'])
        })

        it('clear icon click calls clear method', async () => {
            createComponent()
            await searchInput().setValue('call it')
            const clearSpy = vi.spyOn(wrapper.vm, 'clear')
            await clearIcon().trigger('click')
            expect(clearSpy).toHaveBeenCalled()
        })

        it('clear icon click resets input field value', async () => {
            createComponent()
            await searchInput().setValue('clear me')
            await clearIcon().trigger('click')
            expect((searchInput().element as HTMLInputElement).value).toBe('')
        })

        it('search is emitted when input gains value', async () => {
            createComponent()
            await searchInput().setValue('emit me')
            expect(wrapper.emitted('search')).toBeTruthy()
            expect(wrapper.emitted('search')![0]).toEqual(['emit me'])
        })

        it('clear is emitted when clear icon is clicked', async () => {
            createComponent()
            await searchInput().setValue('emit me')
            await clearIcon().trigger('click')
            expect(wrapper.emitted('clear')).toBeTruthy()
        })

        it('update:modelValue is emitted when clear icon is clicked', async () => {
            createComponent()
            await searchInput().setValue('clear me')
            await clearIcon().trigger('click')
            expect(wrapper.emitted('update:modelValue')).toBeTruthy()
            expect(wrapper.emitted('update:modelValue')![1]).toEqual([''])
        })
    })
})

At present, I suspect that I may be overlooking a core aspect of Vue3 reactivity as I am struggling to test conditional renders associated with v-model. Any form of assistance, resolutions, or guidance would be highly valued!

Thank you :)

Answer №1

Based on what I understand, it appears that the 2-way binding for V-Model is not supported in vue-test-utils. I was able to resolve this issue by implementing a watcher in the props to monitor updates on modelValue, which then updates the modelValue prop accordingly.

To tackle this, I created a function called createComponent() that uses shallowMount() to mount the Component, setting 'onUpdate:modelValue' in props to asynchronously update the modelValue prop when changes occur.

For more details on this solution, visit: https://github.com/vuejs/test-utils/discussions/279

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

Show JSON information in an angular-data-table

I am trying to showcase the following JSON dataset within an angular-data-table {"_links":{"self":[{"href":"http://uni/api/v1/cycle1"},{"href":"http://uni/api/v1/cycle2"},{"href":"http://uni/api/v1/cycle3"}]}} This is what I have written so far in my cod ...

The type 'HTMLDivElement | null' cannot be assigned to the type 'HTMLDivElement' in this context

Struggling with a scroll function to maintain position while scrolling up or down - encountering an error: Error: Type 'HTMLDivElement | null' is not assignable to type 'HTMLDivElement'. Type 'null' is not assignable to type & ...

Can classes from an external package be imported into an Angular library within an Angular app using Openlayers?

I am currently developing an Angular library that configures an OpenLayers map as an Angular component. The component is functioning properly, but I need to access classes and constants from a package imported by the library, specifically OpenLayers. To w ...

Tips for adjusting the size of icons in Ionic Framework v4 and Angular 7

The library ngx-skycons offers a variety of icons for use in projects. If you're interested, check out the demo here. I'm currently incorporating this icon library into an Ionic project that utilizes Angular. While the icons work perfectly, I&ap ...

TypeScript's type casting will fail if one mandatory interface property is missing while an additional property is present

While running tsc locally on an example file named example.ts, I encountered some unexpected behavior. In particular, when I created the object onePropMissing and omitted the property c which is not optional according to the interface definition, I did not ...

Ionic Framework: Implementing a search bar in the navigation bar

I am looking to include a search icon in the navbar of my Ionic project <ion-navbar> <ion-buttons left> <button ion-button menuToggle> <ion-icon name="menu"></icon-icon> </button> </ion-bu ...

React: The useContext hook does not accurately reflect the current state

I'm currently facing a dilemma as I attempt to unify data in my app. Whenever I click the button, the isDisplay value is supposed to be set to true; even though the state changes in my context file, it does not reflect in the app. Thank you for your ...

Issue with Next.js: Callback function not being executed upon form submission

Within my Next.js module, I have a form that is coded in the following manner: <form onSubmit = {() => { async() => await requestCertificate(id) .then(async resp => await resp.json()) .then(data => console.log(data)) .catch(err => console ...

You were supposed to provide 2 arguments, but you only gave 1.ts(2554)

Hey everyone, I hope you're having a good morning. Apologies for the inconvenience, I've been practicing to improve my skills and encountered an issue while working on a login feature. I'm trying to connect it to an API but facing a strange ...

Is it possible to use square brackets in conjunction with the "this" keyword to access a class property using an expression?

export class AppComponent implements OnInit { userSubmitted = false; accountSubmitted = false; userForm!: FormGroup; ngOnInit(): void {} onSubmit(type: string): void { this[type + 'Submitted'] = true; if(this[type + 'For ...

The JSON creation response is not meeting the expected criteria

Hello, I'm currently working on generating JSON data and need assistance with the following code snippet: generateArray(array) { var map = {}; for(var i = 0; i < array.length; i++){ var obj = array[i]; var items = obj.items; ...

Error displaying messages from the console.log function within a TypeScript script

My node.js application runs smoothly with "npm run dev" and includes some typescript scripts/files. Nodemon is used to execute my code: This is an excerpt from my package.json file: { "scripts": { "start": "ts-node ./src/ind ...

Eliminating an index from a JSON array using Typescript

I'm working with a JSON array/model that is structured as follows: var jsonArray = [0] [1] ... [x] [anotherArray][0] [1] ... [e] My goal is to extract only the arrays from [0] to [x] and save them into their ...

Creating an HTML tag from Angular using TypeScript

Looking at the Angular TypeScript code below, I am trying to reference the divisions mentioned in the HTML code posted below using document.getElementById. However, the log statement results in null. Could you please advise on the correct way to reference ...

Tips for including a sequelize getter in a model instance?

I'm currently struggling to add a getter to the name field of the Company model object in my project. Despite trying various approaches, I haven't had any success so far. Unfortunately, I also couldn't find a suitable example to guide me thr ...

Managing business logic in an observable callback in Angular with TypeScript - what's the best approach?

Attempting to fetch data and perform a task upon success using an Angular HttpClient call has led me to the following scenario: return this.http.post('api/my-route', model).subscribe( data => ( this.data = data; ...

Show a specific form field based on the chosen option in a dropdown menu using Angular and TypeScript

I am looking to dynamically display a form field based on the selected value from a dropdown list. For instance, if 'first' is chosen in the dropdown list, I want the form to remain unchanged. However, if 'two' is selected in the drop ...

Getting a vnode from a DOM element in Vue 3.0: A Step-by-Step Guide

My question pertains to obtaining a vnode through accessing the DOM using document.getElementById(id). How can I accomplish this? ...

Struggling to Enforce Restricted Imports in TypeScript Project Even After Setting baseUrl and resolve Configuration

I am facing challenges enforcing restricted imports in my TypeScript project using ESLint. The configuration seems to be causing issues for me. I have configured the baseUrl in my tsconfig.json file as "src" and attempted to use modules in my ESLint setup ...

Angular 6: Sending Back HTTP Headers

I have been working on a small Angular Application for educational purposes, where I am utilizing a .net core WebApi to interact with data. One question that has come up involves the consistent use of headers in all Post and Put requests: const headers = ...