diff --git a/package-lock.json b/package-lock.json index c9fe84a..6e64c52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,11 +21,72 @@ "devDependencies": { "@types/figlet": "^1.5.8", "@types/node": "^20.12.2", + "@vitest/coverage-v8": "^4.1.4", "ts-node": "^10.9.2", "typescript": "^5.4.3", "vitest": "^4.1.4" } }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -473,6 +534,37 @@ "undici-types": "~6.19.2" } }, + "node_modules/@vitest/coverage-v8": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.4.tgz", + "integrity": "sha512-x7FptB5oDruxNPDNY2+S8tCh0pcq7ymCe1gTHcsp733jYjrJl8V1gMUlVysuCD9Kz46Xz9t1akkv08dPcYDs1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.4", + "ast-v8-to-istanbul": "^1.0.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.1.4", + "vitest": "4.1.4" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, "node_modules/@vitest/expect": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.4.tgz", @@ -613,6 +705,29 @@ "node": ">=12" } }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.0.tgz", + "integrity": "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -816,11 +931,64 @@ "node": ">=8" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", @@ -1137,6 +1305,34 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/magicast": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz", + "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -1333,6 +1529,19 @@ "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.16" } }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", diff --git a/package.json b/package.json index 1b3d8da..ad06491 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ }, "scripts": { "build": "npx tsc", - "test": "npx vitest run" + "test": "npx vitest run", + "test:coverage": "npx vitest run --coverage --coverage.include \"src/**/*.ts\"" }, "keywords": [ "time-management", @@ -23,6 +24,7 @@ "devDependencies": { "@types/figlet": "^1.5.8", "@types/node": "^20.12.2", + "@vitest/coverage-v8": "^4.1.4", "ts-node": "^10.9.2", "typescript": "^5.4.3", "vitest": "^4.1.4" diff --git a/tests/render/index.test.ts b/tests/render/index.test.ts new file mode 100644 index 0000000..5211bdc --- /dev/null +++ b/tests/render/index.test.ts @@ -0,0 +1,72 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import render from "../../src/render/index.js"; +import signale from "signale"; +import chalk from "chalk"; + +vi.mock("signale", () => { + const mockSignale = { + config: vi.fn(), + log: vi.fn(), + note: vi.fn(), + pending: vi.fn(), + success: vi.fn(), + error: vi.fn(), + warn: vi.fn(), + info: vi.fn(), + await: vi.fn(), + }; + return { + default: mockSignale, + }; +}); + +vi.mock("chalk", () => ({ + default: { + blue: vi.fn((s) => s), + green: vi.fn((s) => s), + grey: vi.fn((s) => s), + magenta: vi.fn((s) => s), + red: vi.fn((s) => s), + underline: vi.fn((s) => s), + yellow: vi.fn((s) => s), + }, +})); + +describe("Render", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("should call successCreate with correct parameters", () => { + const id = "123"; + render.successCreate(id); + expect(signale.success).toHaveBeenCalledWith( + expect.objectContaining({ + message: "Created task:", + suffix: expect.any(String), + }) + ); + }); + + it("should call successEdit with correct parameters", () => { + const id = "123"; + render.successEdit(id); + expect(signale.success).toHaveBeenCalledWith( + expect.objectContaining({ + message: "Updated item:", + suffix: expect.any(String), + }) + ); + }); + + it("should call successDelete with correct parameters", () => { + const id = "123"; + render.successDelete(id); + expect(signale.success).toHaveBeenCalledWith( + expect.objectContaining({ + message: "Deleted item:", + suffix: expect.any(String), + }) + ); + }); +}); diff --git a/tests/tasks/taskCommands.class.test.ts b/tests/tasks/taskCommands.class.test.ts new file mode 100644 index 0000000..03cab38 --- /dev/null +++ b/tests/tasks/taskCommands.class.test.ts @@ -0,0 +1,156 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import TaskCommands from '../../src/tasks/taskCommands.class.js'; +import { TaskApi } from '../../src/tasks/taskApi.class.js'; +import render from '../../src/render/index.js'; +import { TaskStatus } from '../../src/types/task.interface.js'; + +vi.mock('../../src/tasks/taskApi.class.js'); +vi.mock('../../src/render/index.js'); +vi.mock('../../src/db.js', () => ({ + default: vi.fn().mockResolvedValue({ + data: { tasks: [] }, + write: vi.fn().mockResolvedValue(undefined), + }), +})); + +describe('TaskCommands', () => { + let taskCommands: TaskCommands; + let mockTaskApi: any; + + beforeEach(async () => { + vi.clearAllMocks(); + taskCommands = new TaskCommands(); + taskCommands.db = { + data: { tasks: [], lastTaskId: 0 }, + write: vi.fn().mockResolvedValue(undefined), + data: { tasks: [], lastTaskId: 0 } + }; + taskCommands.archivedb = { + data: { tasks: [] }, + write: vi.fn().mockResolvedValue(undefined), + }; + + mockTaskApi = vi.mocked(TaskApi.prototype); + taskCommands.taskAPi = new TaskApi(taskCommands.db) as any; + }); + + describe('createTask', () => { + it('should create a task and call render.successCreate', async () => { + const taskData = { title: 'New Task', importance: 1, urgency: 1, estimatedTime: 1 }; + const createdTask = { _id: '1', ...taskData }; + mockTaskApi.create.mockResolvedValue(createdTask); + + await taskCommands.createTask(taskData as any); + + expect(mockTaskApi.create).toHaveBeenCalledWith(taskData); + expect(render.successCreate).toHaveBeenCalledWith('1'); + }); + }); + + describe('getTask', () => { + it('should get a task and call render.successGet', async () => { + const task = { _id: '1', title: 'Task 1' }; + mockTaskApi.get.mockResolvedValue(task); + + await taskCommands.getTask('1'); + + expect(mockTaskApi.get).toHaveBeenCalledWith('1'); + expect(render.successGet).toHaveBeenCalledWith(task); + }); + }); + + describe('updateTask', () => { + it('should update a task and call render.successEdit', async () => { + const updateData = { title: 'Updated Task' }; + const updatedTask = { _id: '1', ...updateData }; + mockTaskApi.update.mockResolvedValue(updatedTask); + + await taskCommands.updateTask('1', updateData as any); + + expect(mockTaskApi.update).toHaveBeenCalledWith('1', updateData); + expect(render.successEdit).toHaveBeenCalledWith('1'); + }); + }); + + describe('setStatusInProgress', () => { + it('should set task status to InProgress and call render.successEdit', async () => { + const updatedTask = { _id: '1', status: TaskStatus.InProgress }; + mockTaskApi.update.mockResolvedValue(updatedTask); + + await taskCommands.setStatusInProgress('1'); + + expect(mockTaskApi.update).toHaveBeenCalledWith('1', { + status: TaskStatus.InProgress, + }); + expect(render.successEdit).toHaveBeenCalledWith('1'); + }); + }); + + describe('checkStatus', () => { + it('should toggle status from Done to Pending', async () => { + const task = { _id: '1', status: TaskStatus.Done }; + mockTaskApi.get.mockResolvedValue(task); + mockTaskApi.update.mockResolvedValue({ _id: '1', status: TaskStatus.Pending }); + + await taskCommands.checkStatus('1'); + + expect(mockTaskApi.get).toHaveBeenCalledWith('1'); + expect(mockTaskApi.update).toHaveBeenCalledWith('1', { + status: TaskStatus.Pending, + }); + expect(render.successEdit).toHaveBeenCalledWith('1'); + }); + + it('should toggle status from Pending to Done', async () => { + const task = { _id: '1', status: TaskStatus.Pending }; + mockTaskApi.get.mockResolvedValue(task); + mockTaskApi.update.mockResolvedValue({ _id: '1', status: TaskStatus.Done }); + + await taskCommands.checkStatus('1'); + + expect(mockTaskApi.get).toHaveBeenCalledWith('1'); + expect(mockTaskApi.update).toHaveBeenCalledWith('1', { + status: TaskStatus.Done, + }); + expect(render.successEdit).toHaveBeenCalledWith('1'); + }); + }); + + describe('showTasksDashboard', () => { + it('should show tasks dashboard with default params and call render.displayTaskDashboard', async () => { + const tasks = { data: [{ _id: '1', title: 'Task 1' }] }; + mockTaskApi.getAll.mockResolvedValue(tasks); + + await taskCommands.showTasksDashboard(); + + expect(mockTaskApi.getAll).toHaveBeenCalledWith({}); + expect(render.displayTaskDashboard).toHaveBeenCalledWith(tasks); + }); + + it('should show tasks dashboard with filter when hideBlockedTasks is true', async () => { + const tasks = { data: [{ _id: '1', title: 'Task 1' }] }; + mockTaskApi.getAll.mockResolvedValue(tasks); + + await taskCommands.showTasksDashboard(true); + + expect(mockTaskApi.getAll).toHaveBeenCalledWith({ + statusExcludeFilter: [TaskStatus.Blocked], + }); + expect(render.displayTaskDashboard).toHaveBeenCalledWith(tasks); + }); + }); + + describe('setStatusBlocked', () => { + it('should set task status to Blocked and call render.successEdit', async () => { + const updatedTask = { _id: '1', status: TaskStatus.Blocked }; + mockTaskApi.update.mockResolvedValue(updatedTask); + + await taskCommands.setStatusBlocked('1'); + + expect(mockTaskApi.update).toHaveBeenCalledWith('1', { + status: TaskStatus.Blocked, + }); + expect(render.successEdit).toHaveBeenCalledWith('1'); + }); + }); +});