diff --git a/Sprint-3/todo-list/index.html b/Sprint-3/todo-list/index.html
index 4d12c4654..081ef1507 100644
--- a/Sprint-3/todo-list/index.html
+++ b/Sprint-3/todo-list/index.html
@@ -18,6 +18,8 @@
My ToDo List
+
+
diff --git a/Sprint-3/todo-list/package.json b/Sprint-3/todo-list/package.json
index ce181158a..3676a1836 100644
--- a/Sprint-3/todo-list/package.json
+++ b/Sprint-3/todo-list/package.json
@@ -6,7 +6,7 @@
"type": "module",
"scripts": {
"serve": "http-server",
- "test": "NODE_OPTIONS=--experimental-vm-modules jest"
+ "test": "set NODE_OPTIONS=--experimental-vm-modules && jest"
},
"repository": {
"type": "git",
@@ -20,4 +20,4 @@
"http-server": "^14.1.1",
"jest": "^30.0.4"
}
-}
+}
\ No newline at end of file
diff --git a/Sprint-3/todo-list/script.mjs b/Sprint-3/todo-list/script.mjs
index ba0b2ceae..b2d0f88ce 100644
--- a/Sprint-3/todo-list/script.mjs
+++ b/Sprint-3/todo-list/script.mjs
@@ -8,6 +8,13 @@ const todos = [];
window.addEventListener("load", () => {
document.getElementById("add-task-btn").addEventListener("click", addNewTodo);
+ document
+ .getElementById("delete-completed-btn")
+ .addEventListener("click", () => {
+ Todos.deleteCompleted(todos);
+ render();
+ });
+
// Populate sample data
Todos.addTask(todos, "Wash the dishes", false);
Todos.addTask(todos, "Do the shopping", true);
diff --git a/Sprint-3/todo-list/todos.js b/Sprint-3/todo-list/todos.js
new file mode 100644
index 000000000..e617c472e
--- /dev/null
+++ b/Sprint-3/todo-list/todos.js
@@ -0,0 +1,23 @@
+// Add a new task
+export function addTask(todos, task, completed) {
+ todos.push({ task, completed });
+}
+
+// Delete a task by index
+export function deleteTask(todos, index) {
+ if (index < 0 || index >= todos.length) return;
+ todos.splice(index, 1);
+}
+
+// Toggle completed status
+export function toggleCompletedOnTask(todos, index) {
+ if (index < 0 || index >= todos.length) return;
+ todos[index].completed = !todos[index].completed;
+}
+
+// Delete all completed tasks
+export function deleteCompleted(todos) {
+ const remaining = todos.filter((todo) => !todo.completed);
+ todos.length = 0;
+ todos.push(...remaining);
+}
\ No newline at end of file
diff --git a/Sprint-3/todo-list/todos.test.js b/Sprint-3/todo-list/todos.test.js
new file mode 100644
index 000000000..36da5134f
--- /dev/null
+++ b/Sprint-3/todo-list/todos.test.js
@@ -0,0 +1,169 @@
+// The tests is prepared to demonstrate we can test the functions
+// in a module independently.
+
+// Command to execute this script:
+// npm test todos.test.mjs
+
+// Import all the exported members through an object
+import * as Todos from "./todos.js";
+
+// Return a mock ToDo List data with exactly 4 elements.
+function createMockTodos() {
+ return [
+ { task: "Task 1 description", completed: true },
+ { task: "Task 2 description", completed: false },
+ { task: "Task 3 description", completed: true },
+ { task: "Task 4 description", completed: false },
+ ];
+}
+
+// A mock task to simulate user input
+const theTask = { task: "The Task", completed: false };
+
+describe("addTask()", () => {
+ test("Add a task to an empty ToDo list", () => {
+ let todos = [];
+ Todos.addTask(todos, theTask.task, theTask.completed);
+ expect(todos).toHaveLength(1);
+ expect(todos[0]).toEqual(theTask);
+ });
+
+ test("Should append a new task to the end of a ToDo list", () => {
+
+ const todos = createMockTodos();
+ const lengthBeforeAddition = todos.length;
+ Todos.addTask(todos, theTask.task, theTask.completed);
+ // todos should now have one more task
+ expect(todos).toHaveLength(lengthBeforeAddition + 1);
+
+ // New task should be appended to the todos
+ expect(todos[todos.length - 1]).toEqual(theTask);
+ });
+});
+
+describe("deleteTask()", () => {
+
+ test("Delete the first task", () => {
+ const todos = createMockTodos();
+ const todosBeforeDeletion = createMockTodos();
+ const lengthBeforeDeletion = todos.length;
+ Todos.deleteTask(todos, 0);
+
+ expect(todos).toHaveLength(lengthBeforeDeletion - 1);
+
+ expect(todos[0]).toEqual(todosBeforeDeletion[1]);
+ expect(todos[1]).toEqual(todosBeforeDeletion[2]);
+ expect(todos[2]).toEqual(todosBeforeDeletion[3]);
+ });
+
+ test("Delete the second task (a middle task)", () => {
+ const todos = createMockTodos();
+ const todosBeforeDeletion = createMockTodos();
+ const lengthBeforeDeletion = todos.length;
+ Todos.deleteTask(todos, 1);
+
+ expect(todos).toHaveLength(lengthBeforeDeletion - 1);
+
+ expect(todos[0]).toEqual(todosBeforeDeletion[0]);
+ expect(todos[1]).toEqual(todosBeforeDeletion[2]);
+ expect(todos[2]).toEqual(todosBeforeDeletion[3]);
+ });
+
+ test("Delete the last task", () => {
+ const todos = createMockTodos();
+ const todosBeforeDeletion = createMockTodos();
+ const lengthBeforeDeletion = todos.length;
+ Todos.deleteTask(todos, todos.length - 1);
+
+ expect(todos).toHaveLength(lengthBeforeDeletion - 1);
+
+ expect(todos[0]).toEqual(todosBeforeDeletion[0]);
+ expect(todos[1]).toEqual(todosBeforeDeletion[1]);
+ expect(todos[2]).toEqual(todosBeforeDeletion[2]);
+ });
+
+ test("Delete a non-existing task", () => {
+ const todos = createMockTodos();
+ const todosBeforeDeletion = createMockTodos();
+ Todos.deleteTask(todos, 10);
+ expect(todos).toEqual(todosBeforeDeletion);
+
+ Todos.deleteTask(todos, -1);
+ expect(todos).toEqual(todosBeforeDeletion);
+ });
+});
+
+describe("toggleCompletedOnTask()", () => {
+
+ test("Expect the 'completed' property to toggle on an existing task", () => {
+ const todos = createMockTodos();
+ const taskIndex = 1;
+ const completedStateBeforeToggle = todos[taskIndex].completed;
+ Todos.toggleCompletedOnTask(todos, taskIndex);
+ expect(todos[taskIndex].completed).toEqual(!completedStateBeforeToggle);
+
+ // Toggle again
+ Todos.toggleCompletedOnTask(todos, taskIndex);
+ expect(todos[taskIndex].completed).toEqual(completedStateBeforeToggle);
+ });
+
+ test("Expect toggling on a task does not affect other tasks", () => {
+ const todos = createMockTodos();
+ const todosBeforeToggle = createMockTodos();
+ Todos.toggleCompletedOnTask(todos, 1);
+
+ expect(todos[0]).toEqual(todosBeforeToggle[0]);
+ expect(todos[2]).toEqual(todosBeforeToggle[2]);
+ expect(todos[3]).toEqual(todosBeforeToggle[3]);
+ });
+
+
+ test("Expect no change when toggling on a non-existing task", () => {
+ const todos = createMockTodos();
+ const todosBeforeToggle = createMockTodos();
+
+ Todos.toggleCompletedOnTask(todos, 10);
+ expect(todos).toEqual(todosBeforeToggle);
+
+ Todos.toggleCompletedOnTask(todos, -1);
+ expect(todos).toEqual(todosBeforeToggle);
+ });
+});
+
+describe("deleteCompleted()", () => {
+ test("removes all completed tasks from the list", () => {
+ const todos = createMockTodos();
+
+ Todos.deleteCompleted(todos);
+
+ expect(todos).toEqual([
+ { task: "Task 2 description", completed: false },
+ { task: "Task 4 description", completed: false },
+ ]);
+ });
+
+ test("does nothing if no tasks are completed", () => {
+ const todos = [
+ { task: "Task A", completed: false },
+ { task: "Task B", completed: false },
+ ];
+
+ Todos.deleteCompleted(todos);
+
+ expect(todos).toEqual([
+ { task: "Task A", completed: false },
+ { task: "Task B", completed: false },
+ ]);
+ });
+
+ test("removes all tasks if all are completed", () => {
+ const todos = [
+ { task: "Task A", completed: true },
+ { task: "Task B", completed: true },
+ ];
+
+ Todos.deleteCompleted(todos);
+
+ expect(todos).toEqual([]);
+ });
+});
\ No newline at end of file