Vuetify 3 TypeScript Tutorial Series -Part 9
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 namedremoveTask
, 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 theisLoading
reactive variable totrue
, indicating that a task removal operation is starting.await taskService.deleteTask(id)
: Asynchronously calls thedeleteTask
method oftaskService
, passing theid
of the task to be deleted. This is likely an API call..then(() => { ... })
: If the deletion is successful, the code inside thethen
block executes. It callsfetchTasks(taskType)
, which likely refreshes the list of tasks, and then setsisLoading.value
back tofalse
..catch((err: AxiosError | unknown) => { ... })
: If there's an error during the deletion process, thecatch
block executes. It logs the error usinglogRequestError('removeTask', err)
, setsaxiosError.value
to the error if it's anAxiosError
(orundefined
otherwise), and flagsisNetworkError.value
astrue
..finally(() => { ... })
: Thefinally
block executes regardless of whether the deletion was successful or not, and it setsisLoading.value
back tofalse
. 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 ofRef
for variables likeisLoading
andisNetworkError
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 ofasync/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:
Now, you should see a dialog opening:
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
:
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
:
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.ts
file 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
:
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 byformatDate
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 thatformatDate
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