Part 4 (c): Unit Testing: How to Build a To-do App with Vue.Js
Makanju Oluwafemi
Posted on September 12, 2023
Welcome back! In our previous article, we explored the core features of our app and introduced three essential utility components: BaseButton
,BaseModal
, and BaseInput
. In this section, we'll dive into the world of unit testing and learn how to ensure that our utility components work as expected.
We'll continue enhancing our To-Do App by adding comprehensive unit tests for the utility components built in the previous section. Our goal is to build a robust application while ensuring that critical parts of its functionality are thoroughly tested. Let's dive into the test coverage.
If you've followed this series from the beginning, you won't have any problem adding tests to your project. However, if you are new, kindly check here for more information on how to get started.
Writing Tests for BaseButton
Understanding the BaseButton
component is important if we are going to write an effective test that covers all scenarios that are expected. Let's briefly recap the key features of the BaseButton component.
- It's a reusable button component.
- It accepts props for label, variant, and disabled.
- The button's appearance and behavior depend on these props.
- It emits a custom 'clk' event when clicked.
src/components/utility/BaseButton.vue
<template>
<button
:class="['p-3 text-center w-full rounded-4xl', variantClass ]"
:disabled="disabled"
:aria-disabled="disabled"
@click="handleClick"
type="submit"
>
{{ label }}
</button>
</template>
<script>
export default {
name:'BaseButton',
props: {
label: String,
variant: {
type: String,
default: 'primary',
},
disabled: Boolean,
},
computed: {
variantClass(){
if(this.disabled){
return `variant-${this.variant}-disabled`;
}
return `variant-${this.variant}`;
}
},
methods: {
handleClick() {
this.$emit('clk');
},
}
}
</script>
<style>
/* variant style */
.variant-primary {
background-color: #414066;
color: #fff;
}
.variant-primary-disabled {
background-color: #414066c3;
cursor: not-allowed;
}
.variant-secondary {
background-color: #4C6640;
color: #fff;
}
.variant-secondary-disabled {
background-color: #4c6640b4;
cursor: not-allowed;
}
</style>
In this component, the element's appearance and behavior are dynamically controlled by the variant
prop. The variantClass
computed property generates a CSS class based on the prop's value. If the button is disabled
, it appends the -disabled suffix to the variant prop's value, creating a specific class like variant-primary-disabled
or variant-secondary-disabled
. These classes are then applied to the button, altering its visual style and behavior. For instance, if the variant prop is set to 'primary' and disabled is true, the button will have the variant-primary-disabled
class, giving it a disabled appearance with a different background color and a 'not-allowed' cursor. This dynamic class assignment ensures flexible and reusable button styling based on the variant prop, making it easy to create different button styles throughout the application.
src/components/__test__/__utils/BaseButton.spec.ts
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import AppButton from '../../utility/BaseButton.vue';
describe('AppButton', () => {
it('renders primary button properly', () => {
const wrapper = mount(AppButton, {
props: {
label: 'Click me',
variant: 'primary',
disabled: false,
},
});
// Check if the button has the correct label
expect(wrapper.text()).toContain('Click me');
// Check if the button has the correct classes for primary variant
expect(wrapper.classes()).toContain('variant-primary');
expect(wrapper.classes()).not.toContain('variant-primary-disabled');
});
it('renders disabled primary button properly', () => {
const wrapper = mount(AppButton, {
props: {
label: 'Click me',
variant: 'primary',
disabled: true,
},
});
// Check if the button has the correct classes for disabled primary variant
expect(wrapper.classes()).toContain('variant-primary-disabled');
expect(wrapper.classes()).not.toContain('variant-primary');
// Check if the 'disabled' attribute is set correctly
expect(wrapper.attributes('aria-disabled')).toBe('true');
});
});
In the provided unit tests for the BaseButton
component, there are several assertions made to ensure that the component behaves correctly. Assertions are statements that check whether a certain condition or expectation holds true.
Test Case 1 - Renders Primary Button Properly:
- In this test, we mount the
AppButton
component with specific props:label
is set to 'Click me',variant
is set to 'primary', anddisabled
is false. - We then check if the button correctly displays the label 'Click me' using
expect(wrapper.text()).toContain('Click me')
. - Next, we ensure that the button has the class variant-primary, indicating it's a primary button, with expect(wrapper.classes()).toContain('variant-primary'). Additionally, we confirm that it does not have the variant-primary-disabled class, which is expected for disabled primary buttons.
Test Case 2 - Renders Disabled Primary Button Properly:
- In this test, we mount the
AppButton
component with props:label
set to 'Click me',variant
set to 'primary', anddisabled
set totrue
. - We validate that the button now has the
variant-primary-disabled
class, indicating it's a disabled primary button, usingexpect(wrapper.classes()).toContain('variant-primary-disabled')
. - To ensure it's not mistakenly identified as a standard primary button, we confirm that it does not have the variant-primary class with
expect(wrapper.classes()).not.toContain('variant-primary')
. - Finally, we check if the
aria-disabled
attribute is set to 'true' usingexpect(wrapper.attributes('aria-disabled')).toBe('true')
.
Writing Tests for BaseInput
The "BaseInput" component is a versatile input field that supports dynamic styling on focus, error handling, and two-way data binding. It features an input element, a label, and optional error message display. This component is highly customizable and allows for easy integration of input forms into Vue.js applications, providing a user-friendly and accessible experience.
src/components/utility/BaseInput.vue
<template>
<div class="div">
<label
class="size-2xl"
:for="id">{{ label }}</label>
<input
:id="id"
:class="[isFocused && 'focused']"
class="p-2 border-2 border-[#eee] w-full rounded-xl"
:type="text"
:placeholder="placeHolder"
@focus="isFocused = true"
@blur="isFocused = false"
@input="handleText"
:value="value"
:aria-label="label"
:aria-describedby="`${id}-description`"
>
<div
v-if="error"
class="h-[10px] mt-1">
<span class="text-[red]">{{ msg }}</span>
</div>
</div>
</template>
<script>
export default {
name:'BaseInput',
props: {
id: String,
label:String,
text: String,
placeHolder: String,
value: [String , Number],
error: {
type: Boolean,
default: false,
},
msg: {
type: String,
msg: '',
}
},
data(){
return {
isFocused: false,
}
},
methods: {
handleText(e){
this.$emit('update:modelValue', e.target.value )
}
}
}
</script>
<style scoped>
.focused{
outline: 2px solid #414066;
box-shadow: 0px 2px 5px rgba(0, 0, 0, .3);
}
</style>
src/components/__test__/__utils/BaseInput.spec.ts
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import AppInput from '../../utility/BaseInput.vue';
describe('AppInput', () => {
it('renders input and label properly', () => {
const wrapper = mount(AppInput, {
props: {
id: 'input-id',
label: 'Username',
placeHolder: 'Enter your username',
value: '',
},
});
const label = wrapper.find('label');
const input = wrapper.find('input');
// Check if the label and input are rendered correctly
expect(label.exists()).toBe(true);
expect(input.exists()).toBe(true);
// Check if the label text and input placeholder are set correctly
expect(label.text()).toBe('Username');
expect(input.attributes('placeholder')).toBe('Enter your username');
});
it('applies focus styles on input when focused', async () => {
const wrapper = mount(AppInput, {
props: {
id: 'input-id',
label: 'Username',
placeHolder: 'Enter your username',
value: '',
},
});
const input = wrapper.find('input');
// Check if focus styles are not applied initially
expect(input.classes()).not.toContain('focused');
// Simulate focusing on the input
await input.trigger('focus');
// Check if focus styles are applied
expect(input.classes()).toContain('focused');
// Simulate blurring the input
await input.trigger('blur');
// Check if focus styles are removed after blurring
expect(input.classes()).not.toContain('focused');
});
it('emits input event when input value changes', async () => {
const wrapper = mount(AppInput, {
props: {
id: 'input-id',
label: 'Username',
placeHolder: 'Enter your username',
value: '',
},
});
const input = wrapper.find('input');
// Simulate typing into the input
await input.setValue('john_doe');
// Check if the input event is emitted with the new value
expect(wrapper.emitted('update:modelValue')).toBeTruthy();
expect(wrapper.emitted('update:modelValue')[0]).toEqual(['john_doe']);
});
});
Test Case 1 - Renders input and label properly: This test ensures that the "AppInput" component correctly renders both an input field and a label. It checks the following:
- Verifies that a element exists in the component's rendered output. Verifies that an element exists in the component's rendered output.
- Checks if the label text is set to "Username" as expected. Verifies that the input element's placeholder attribute is set to "Enter your username."
Test Case 2 - Applies focus styles on input when focused: This test checks whether the component correctly applies focus styles to the input field when it receives focus and removes them when it loses focus. It does the following:
- Initially checks that the input element does not have the "focused" class.
- Simulates focusing on the input element.
- Checks if the "focused" class is added to the input element.
- Simulates blurring the input element.
- Checks if the "focused" class is removed from the input element.
Test Case 3 - Emits input event when input value changes: - - This test ensures that the component emits an "input" event when the input field's value changes. It verifies the following:
- Simulates typing the text "john_doe" into the input field.
- Checks if the "update:modelValue" event is emitted.
- Verifies that the emitted event contains the expected value, which is "john_doe."
These test cases collectively ensure that the "AppInput" component works as expected, rendering elements correctly, applying styles appropriately.
Writing Tests for BaseModal
src/components/utility/BaseModal.vue
<template>
<div class="flex items-center justify-center h-[100%] bg-[#242222b7] w-full">
<div class="bg-[#fff] w-1/5 h-2/5 flex flex-col drop-shadow-sm rounded-lg">
<span class="flex items-center justify-end p-4 right-0">
<font-awesome-icon
@click="handleClose"
class="mb-5 text-2xl c-tst cursor-pointer" :icon="['fa', 'times']" />
</span>
<div class="flex flex-col items-center justify-center w-full">
<font-awesome-icon class="text-7xl mb-5 text-[green]" :icon="['fa', 'circle-check']" />
<h1 class="text-4xl font-popins font-bold">Success</h1>
</div>
</div>
</div>
</template>
<script>
export default {
name:'AppModal',
methods: {
handleClose(){
this.$emit('close')
}
}
}
</script>
This component, named "AppModal," represents a simple modal dialog for displaying success messages or notifications. It features a centered design with a semi-transparent dark background, a white content container with a drop shadow, and a close button (represented by a "times" icon) at the upper right corner. When the close button is clicked, it emits a 'close' event, allowing parent components to control the modal's closure behavior. The modal's content includes a large green checkmark icon and a bold "Success" text, making it suitable for conveying positive messages or feedback to users in a clean and visually appealing manner.
src/components/__test__/__utils/BaseModal.spec.ts
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import BaseModal from '../../utility/BaseModal.vue';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import { library } from '@fortawesome/fontawesome-svg-core';
import { faTimes, faCircleCheck } from '@fortawesome/free-solid-svg-icons';
library.add(faTimes, faCircleCheck);
describe('BaseModal' , () => {
it('emits close event when close button is clicked', async() => {
const wrapper = mount(BaseModal , {
global: {
components: {
FontAwesomeIcon,
}
}
})
// Lets assume this selector is correct
const closeButton = wrapper.find('.c-tst');
// Simulate clicking the close button
await closeButton.trigger('click');
// Check if the close event is emitted
expect(wrapper.emitted('close')).toBeTruthy();
})
})
This code checks if the component emits a 'close' event when a close button is clicked.it mounts the BaseModal component, simulates a click on the close button, and verifies if the 'close' event is emitted. It also imports Font Awesome icons for rendering.
Test Case 1 - Emits 'close' Event on Close Button Click:
it('emits close event when close button is clicked', async() => { ... });
: This is the description of the first test case.const wrapper = mount(BaseModal , { ... });
: It mounts (renders) theBaseModal
component for testing.const closeButton = wrapper.find('.c-tst');
: It selects the close button within theBaseModal
component. The.c-tst
class selector is assumed to be correct.await closeButton.trigger('click');
: It simulates a click on the close button using thetrigger
method. Theawait
keyword is used because this operation is asynchronous.expect(wrapper.emitted('close')).toBeTruthy();
: It verifies that the 'close' event is emitted by theBaseModal
component. If the event is emitted, the test passes; otherwise, it fails.
Run the test by typing the command npm run test
on your terminal. The result below shows that the test passed for all three components.
Posted on September 12, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.