0

I'm new to Vue and currently writing unit tests for a search component that I am using in my project. Simply, when the user types in the input field and small X icon appears to the right of the input field. Clicking the X will reset the value of the field back to an empty string.

The component is using the composition API and working as intended, and I can watch the emits and payloads using Vue dev tools, however I have been unable to see these events using Vitest. The majority of the tests are failing, and I am wondering where the mistake is in my logic.

For this question I recreated the component with some scoped style to make it easy to mount if necessary. Here it is using the Vue3 Comp Api, TypeScript, Vite, Vitest and vue-test-utils.

Here is the component:

<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>

Here 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 this point I feel like I must be missing something fundamental about Vue3 reactivity since I am unable to test conditional renders attached to v-model. Honestly any help, solutions or advice would be very appreciated!

Thank you :)

1 Answer 1

3

From my understanding, it sounds like the 2 way binding for V-Model is not included in vue-test-utils. The fix I found was to set a watcher in props to track update:modelValue, and this will update the modelValue prop.

function createComponent() {
    wrapper = shallowMount(Component, {
        attachTo: document.body,
        props: {
            'onUpdate:modelValue': async (modelValue: any) => await wrapper.setProps({ modelValue })
        }
    })
}

Solution: https://github.com/vuejs/test-utils/discussions/279

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.