Vuetify 3 TypeScript Tutorial Series -Part 9

Habibi Coding | حبيبي كودنق
Nerd For Tech
Published in
6 min readFeb 18, 2024
tutorial banner

In Part 8 of this tutorial series, we covered the following steps:

  • Implemented edit task
  • Added a dialog component for deleting a task

If you missed Part 8, you can find it here: Part 8

Continue to enable the deletion of a task

In the previous part, we added the component TaskDeleteDialog.vue , now add the file removeTask.ts in composables package with these lines:

import {taskService} from "@/services/taskApi";
import {AxiosError} from "axios";
import logRequestError from "@/composables/logRequestError";
import {Ref} from "vue";

export async function removeTask(
id: number,
isLoading: Ref<boolean>,
isNetworkError: Ref<boolean>,
axiosError: Ref<AxiosError | unknown>,
fetchTasks: (taskType: string) => Promise<void>,
taskType: string
): Promise<void> {
isLoading.value = true;
await taskService.deleteTask(id).then(() => {
fetchTasks(taskType);
isLoading.value = false;
}).catch((err: AxiosError | unknown) => {
logRequestError('removeTask', err);
axiosError.value = err instanceof AxiosError ? err : undefined;
isNetworkError.value = true;
}).finally(() => {
isLoading.value = false;
});
}

Function Declaration — removeTask:

  • This is an async function named removeTask, meaning it's asynchronous and will return a Promise.
  • The function takes several parameters:
  • id: The ID of the task to be removed.
  • isLoading: A reactive reference (Ref) to a boolean indicating whether an operation is in progress.
  • isNetworkError: A reactive reference to a boolean indicating if a network error has occurred.
  • axiosError: A reactive reference that can hold an Axios error or an unknown error.
  • fetchTasks: A function to fetch tasks, probably to refresh the task list after a deletion.
  • taskType: A string indicating the type of task.

Function Body:

  • isLoading.value = true;: Sets the isLoading reactive variable to true, indicating that a task removal operation is starting.
  • await taskService.deleteTask(id): Asynchronously calls the deleteTask method of taskService, passing the id of the task to be deleted. This is likely an API call.
  • .then(() => { ... }): If the deletion is successful, the code inside the then block executes. It calls fetchTasks(taskType), which likely refreshes the list of tasks, and then sets isLoading.value back to false.
  • .catch((err: AxiosError | unknown) => { ... }): If there's an error during the deletion process, the catch block executes. It logs the error using logRequestError('removeTask', err), sets axiosError.value to the error if it's an AxiosError (or undefined otherwise), and flags isNetworkError.value as true.
  • .finally(() => { ... }): The finally block executes regardless of whether the deletion was successful or not, and it sets isLoading.value back to false. This ensures that the loading state is correctly reset after the operation completes.

Overall Functionality:

  • The removeTask function is designed to handle the deletion of a task. It manages loading state, error handling, and refreshing the task list post-deletion. The use of Ref for variables like isLoading and isNetworkError indicates that this function is designed to be used within the Vue Composition API, which allows for a more reactive and composable application structure. The use of async/await along with .then(), .catch(), and .finally() provides a clear flow for handling the asynchronous operation and its outcomes.

Use of Error dialog

Open again in pages package the file TasksOverviewPage.vue and go to this method:

const deleteTask = (id: number) => {
console.log("delete clicked");
};

Use this implementation instead:

const deleteTask = (id: number) => {
removeTask(id, isLoading, isNetworkError, axiosError, fetchTasks, taskStore.selectedTaskType);
};

Lastly, add the TaskDeleteDialog.vue to the UI structure:

<template>
<Navbar @task-type-selected="handleTaskTypeSelected" @logo-clicked="logoClicked"/>
<MainBackground>
<ErrorDialog :model-value="isNetworkError" :axios-error="axiosError"/>
<TaskOverviewCard
:tasks="tasks"
@card-clicked="handleCardClicked"
@delete-clicked="openDeleteDialog"
@edit-clicked="navigateToTaskUpdateView"
/>
<TaskDeleteDialog
v-model="isDeleteDialogSelected"
:taskDescription="selectedTaskDescription.valueOf()"
@confirm-delete="deleteTask(selectedTaskId.valueOf())"
/>
<LoadingSpinner :is-loading="isLoading"/>
</MainBackground>
</template>

Start again the project:

pnpm dev

Open http://localhost:3000/ and click on “Delete Task” on an item card:

Delete Task

Now, you should see a dialog opening:

error dialog

By clicking on “Cancel” the dialog closes, by clicking on “Confirm” the task gets deleted.

Congratulation / Mabrook/ مبروك you have now implemented all CRUD operations!

Time to write Unit Test

It is time to write some tests for our Vue 3 application. In your root folder create a new package tests and inside it a sub-package called helper :

tests -> helper

Inside of helper create a file called mockResponse.ts and add these lines:

import {TaskCreateRequest, TaskFetchResponse, TaskUpdateRequest} from "../../src/dtos/taskDto";

export const mockTaskFetchResponse: TaskFetchResponse[] = [{
"id": 23,
"description": "test new nav",
"isReminderSet": false,
"isTaskOpen": false,
"createdOn": "2023-12-26T17:53:53.045334",
"priority": "LOW"
}, {
"id": 25,
"description": "Write new Unit Tests",
"isReminderSet": true,
"isTaskOpen": true,
"createdOn": "2023-12-26T17:55:41.73961",
"priority": "HIGH"
}]

export const mockTaskUpdateRequest: TaskUpdateRequest = {
"description": "buy groceries",
"isReminderSet": false,
"isTaskOpen": true,
"priority": "HIGH"
}

export const mockTaskCreateRequest: TaskCreateRequest= {
"description": "workout",
"isReminderSet": true,
"isTaskOpen": true,
"priority": "MEDIUM"
}

These are just mock object instances we need for testing.

Next, create in src the file setupTests.ts :

setupTests.ts

Then add these lines to setupTests.ts :

import {afterAll, afterEach, beforeAll} from "vitest";
import {rest} from "msw";
import "whatwg-fetch";
import {setupServer} from "msw/native";
import {mockTaskCreateRequest, mockTaskFetchResponse, mockTaskUpdateRequest} from "../tests/helper/mockResponse";


export const restHandlers = [
rest.get("https://backend4frontend.onrender.com/api/v1/tasks", (req, res, ctx) => {
return res(ctx.status(200), ctx.json(mockTaskFetchResponse));
}),
// Add mocks for POST, DELETE, and PATCH
rest.post("https://backend4frontend.onrender.com/api/v1/tasks", (req, res, ctx) => {
return res(ctx.status(201), ctx.json(mockTaskCreateRequest)); // Assume successful creation
}),
rest.delete("https://backend4frontend.onrender.com/api/v1/tasks/:id", (req, res, ctx) => {
return res(ctx.status(204)); // Assume successful deletion
}),
rest.patch("https://backend4frontend.onrender.com/api/v1/tasks/:id", (req, res, ctx) => {
return res(ctx.status(200), ctx.json(mockTaskUpdateRequest)); // Assume successful update
})
];

export const server = setupServer(...restHandlers);

// Start server before all tests
beforeAll(() => server.listen({onUnhandledRequest: "error"}));

// Close server after all tests
afterAll(() => server.close());

// Reset handlers after each test `important for test isolation`
afterEach(() => server.resetHandlers());

The setupTests.tsfile is used for setting up a mock server for API testing in a Vue.js application. It utilizes msw (Mock Service Worker) to intercept and handle HTTP requests during tests. The file defines mock handlers for various API endpoints (GET, POST, DELETE, PATCH) and returns predefined responses. This setup ensures that when the application’s code makes API requests during tests, these requests are intercepted by the mock server, which then returns the mocked responses. The server is started before all tests begin and is closed after all tests have finished, ensuring a clean environment for each test run.

In tests create a new package impl and write the for formatDate.ts so create a files called formatDate.test.ts :

formatDate.test.ts

Then add the test implementation:

import {expect, describe, it} from 'vitest'
import {formatDate} from "../../src/composables/formatDate";


describe('formatDate Unit Tests', () => {
it('formatDate formats date correctly', () => {
const date1 = '2022-12-31';
const expected1 = '12.31.2022';
const result1 = formatDate(date1)

const date2 = '2024-01-01'
const expected2 = '01.01.2024';
const result2 = formatDate(date2);

expect(result1).toBe(expected1);
expect(result2).toBe(expected2);
});

it('formatDate returns empty string for invalid date', () => {
const invalidDate = 'invalid-date';
const result = formatDate(invalidDate);
expect(result).toBe('');
})
});

Test Case 1

  • Correct Formatting: This test checks if formatDate correctly formats valid date strings. It tests two different dates ('2022-12-31' and '2024-01-01'). The expected format is 'MM.DD.YYYY' (e.g., '12.31.2022'). The test asserts that the formatted dates returned by formatDate match the expected formatted strings.

Test Case 2

  • Handling Invalid Dates: This test checks how formatDate handles an invalid date string ('invalid-date'). The expected behavior is that formatDate should return an empty string when given an invalid date. The test asserts that the function indeed returns an empty string for the invalid date input.

Overall, these tests aim to ensure that formatDate correctly formats valid date strings and handles invalid dates gracefully by returning an empty string.

Now run in the Terminal:

npx vitest run
npx vitest run

With that, we conclude the first part of this tutorial series. If you found it useful and informative, give it a clap. Here is Part 10

Don’t forget to check out the video playlist on YouTube.

Here is the source code on GitHub, check out the branch: part-nine

--

--