Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 54 additions & 42 deletions src/modules/tasks/stores/tasks.store.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ import model from '../../../lib/middlewares/model';
*/
const whitelists = ['title', 'description'];

/**
* @desc Build the base API URL from config.
* @returns {string} Base API URL
*/
const apiBase = () => `${config.api.protocol}://${config.api.host}:${config.api.port}/${config.api.base}`;

/**
* Store definition.
*/
Expand All @@ -25,61 +31,67 @@ export const useTasksStore = defineStore('tasks', {
}),

actions: {
/**
* @desc Fetch all tasks.
* @returns {Promise<Array>} Resolved list of tasks
*/
async getTasks() {
const api = `${config.api.protocol}://${config.api.host}:${config.api.port}/${config.api.base}`;

try {
const res = await axios.get(`${api}/${config.api.endPoints.tasks}/`);
this.tasks = res.data.data;
} catch (err) {
console.error(err);
}
const api = apiBase();
const res = await axios.get(`${api}/${config.api.endPoints.tasks}/`);
this.tasks = res.data.data;
return this.tasks;
Comment thread
PierreBrisorgueil marked this conversation as resolved.
Resolved
},

/**
* @desc Fetch a single task by ID.
* @param {Object} params - Request params
* @param {string} params.id - Task ID
* @returns {Promise<Object>} Resolved task object
*/
async getTask(params) {
const api = `${config.api.protocol}://${config.api.host}:${config.api.port}/${config.api.base}`;

try {
const res = await axios.get(`${api}/${config.api.endPoints.tasks}/${params.id}`);
this.task = res.data.data;
} catch (err) {
console.error(err);
}
const api = apiBase();
const res = await axios.get(`${api}/${config.api.endPoints.tasks}/${params.id}`);
this.task = res.data.data;
return this.task;
},

/**
* @desc Create a new task.
* @param {Object} params - Task data
* @returns {Promise<Object>} Resolved created task
*/
async createTask(params) {
const api = `${config.api.protocol}://${config.api.host}:${config.api.port}/${config.api.base}`;

try {
const obj = model.clean(params, whitelists);
const res = await axios.post(`${api}/${config.api.endPoints.tasks}/`, obj);
this.task = res.data.data;
} catch (err) {
console.error(err);
}
const api = apiBase();
const obj = model.clean(params, whitelists);
const res = await axios.post(`${api}/${config.api.endPoints.tasks}/`, obj);
this.task = res.data.data;
return this.task;
},

/**
* @desc Update an existing task.
* @param {Object} params - Request params
* @param {string} params.id - Task ID
* @returns {Promise<Object>} Resolved updated task
*/
async updateTask(params) {
const api = `${config.api.protocol}://${config.api.host}:${config.api.port}/${config.api.base}`;

try {
const obj = model.clean(this.task, whitelists);
const res = await axios.put(`${api}/${config.api.endPoints.tasks}/${params.id}`, obj);
assign(this.task, res.data.data);
} catch (err) {
console.error(err);
}
const api = apiBase();
const obj = model.clean(this.task, whitelists);
const res = await axios.put(`${api}/${config.api.endPoints.tasks}/${params.id}`, obj);
assign(this.task, res.data.data);
return this.task;
},

/**
* @desc Delete a task by ID.
* @param {Object} params - Request params
* @param {string} params.id - Task ID
* @returns {Promise<void>}
*/
async deleteTask(params) {
const api = `${config.api.protocol}://${config.api.host}:${config.api.port}/${config.api.base}`;

try {
await axios.delete(`${api}/${config.api.endPoints.tasks}/${params.id}`);
this.resetTask();
} catch (err) {
console.error(err);
}
const api = apiBase();
await axios.delete(`${api}/${config.api.endPoints.tasks}/${params.id}`);
this.resetTask();
},

resetTask() {
Expand Down
197 changes: 197 additions & 0 deletions src/modules/tasks/tests/task.view.unit.tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { mount, flushPromises } from '@vue/test-utils';
import { createPinia, setActivePinia } from 'pinia';
import { createVuetify } from 'vuetify';

/**
* Hoisted mocks — must be defined before any imports that transitively use them.
*/
const createTaskMock = vi.hoisted(() => vi.fn());
const updateTaskMock = vi.hoisted(() => vi.fn());
const deleteTaskMock = vi.hoisted(() => vi.fn());
const getTaskMock = vi.hoisted(() => vi.fn());

const mockTask = vi.hoisted(() => ({
title: 'My Task',
description: 'My Description',
id: null,
}));

vi.mock('../stores/tasks.store', () => ({
useTasksStore: () => ({
get task() {
return mockTask;
},
createTask: createTaskMock,
updateTask: updateTaskMock,
deleteTask: deleteTaskMock,
getTask: getTaskMock,
resetTask: vi.fn(),
}),
}));

vi.mock('../../core/components/core.pageHeader.component.vue', () => ({
default: { template: '<div><slot name="actions" /></div>' },
}));

vi.mock('../components/task.component.vue', () => ({
default: { template: '<div />' },
}));

import TaskView from '../views/task.view.vue';

const mockPush = vi.fn();

/**
* Mount the task view with optional route params.
* @param {Object} [options]
* @param {string|null} [options.id=null] - Route param id
* @returns {import('@vue/test-utils').VueWrapper}
*/
const mockConfig = {
vuetify: { theme: { flat: true, rounded: 'rounded-lg' } },
};

const mountView = ({ id = null } = {}) =>
mount(TaskView, {
global: {
plugins: [createVuetify(), createPinia()],
mocks: {
config: mockConfig,
$route: { params: { id } },
$router: { push: mockPush },
},
stubs: { RouterLink: true },
},
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

describe('task.view — create()', () => {
beforeEach(() => {
setActivePinia(createPinia());
vi.clearAllMocks();
mockTask.id = null;
mockTask.title = 'My Task';
mockTask.description = 'My Description';
});

it('navigates to /tasks after a successful create', async () => {
createTaskMock.mockResolvedValueOnce({ id: 'new-id', title: 'My Task', description: 'My Description' });

const wrapper = mountView();
await flushPromises();

await wrapper.vm.create();

expect(createTaskMock).toHaveBeenCalledTimes(1);
expect(mockPush).toHaveBeenCalledWith('/tasks');
expect(wrapper.vm.save).toBe(false);
});

it('does NOT navigate when createTask rejects', async () => {
createTaskMock.mockRejectedValueOnce(new Error('Server error'));

const wrapper = mountView();
await flushPromises();

await wrapper.vm.create();

expect(createTaskMock).toHaveBeenCalledTimes(1);
expect(mockPush).not.toHaveBeenCalled();
});

it('does NOT clear save flag when createTask rejects', async () => {
createTaskMock.mockRejectedValueOnce(new Error('Server error'));

const wrapper = mountView();
await flushPromises();

wrapper.vm.save = true;
await wrapper.vm.create();

// save should remain true (not cleared to false) on failure
expect(wrapper.vm.save).toBe(true);
});
});

describe('task.view — update()', () => {
beforeEach(() => {
setActivePinia(createPinia());
vi.clearAllMocks();
mockTask.id = 'task-123';
mockTask.title = 'My Task';
mockTask.description = 'My Description';
getTaskMock.mockResolvedValue({ id: 'task-123', title: 'My Task', description: 'My Description' });
});

it('navigates to /tasks after a successful update', async () => {
updateTaskMock.mockResolvedValueOnce({ id: 'task-123', title: 'My Task', description: 'My Description' });

const wrapper = mountView({ id: 'task-123' });
await flushPromises();

await wrapper.vm.update();

expect(updateTaskMock).toHaveBeenCalledTimes(1);
expect(mockPush).toHaveBeenCalledWith('/tasks');
expect(wrapper.vm.save).toBe(false);
});

it('does NOT navigate when updateTask rejects', async () => {
updateTaskMock.mockRejectedValueOnce(new Error('Update failed'));

const wrapper = mountView({ id: 'task-123' });
await flushPromises();

await wrapper.vm.update();

expect(updateTaskMock).toHaveBeenCalledTimes(1);
expect(mockPush).not.toHaveBeenCalled();
});

it('does NOT clear save flag when updateTask rejects', async () => {
updateTaskMock.mockRejectedValueOnce(new Error('Update failed'));

const wrapper = mountView({ id: 'task-123' });
await flushPromises();

wrapper.vm.save = true;
await wrapper.vm.update();

expect(wrapper.vm.save).toBe(true);
});
});

describe('task.view — remove()', () => {
beforeEach(() => {
setActivePinia(createPinia());
vi.clearAllMocks();
mockTask.id = 'task-123';
mockTask.title = 'My Task';
mockTask.description = 'My Description';
getTaskMock.mockResolvedValue({ id: 'task-123', title: 'My Task', description: 'My Description' });
});

it('navigates to /tasks after a successful delete', async () => {
deleteTaskMock.mockResolvedValueOnce(undefined);

const wrapper = mountView({ id: 'task-123' });
await flushPromises();

await wrapper.vm.remove();

expect(deleteTaskMock).toHaveBeenCalledTimes(1);
expect(mockPush).toHaveBeenCalledWith('/tasks');
});

it('does NOT navigate when deleteTask rejects', async () => {
deleteTaskMock.mockRejectedValueOnce(new Error('Delete failed'));

const wrapper = mountView({ id: 'task-123' });
await flushPromises();

await wrapper.vm.remove();

expect(deleteTaskMock).toHaveBeenCalledTimes(1);
expect(mockPush).not.toHaveBeenCalled();
});
});
Comment thread
PierreBrisorgueil marked this conversation as resolved.
Resolved
Loading
Loading