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