2026-02-11 13:00:46 +00:00
|
|
|
import SwiftUI
|
2026-02-10 15:31:52 +00:00
|
|
|
import Testing
|
|
|
|
|
|
|
|
|
|
@testable import Todos
|
|
|
|
|
|
2026-02-11 18:22:26 +00:00
|
|
|
@MainActor
|
2026-02-10 15:31:52 +00:00
|
|
|
@Suite("TaskList view model tests")
|
|
|
|
|
struct TaskListViewModelTests {
|
|
|
|
|
|
2026-02-11 13:00:46 +00:00
|
|
|
// MARK: Initializers tests
|
2026-02-10 15:31:52 +00:00
|
|
|
|
|
|
|
|
@Test(
|
2026-02-11 13:00:46 +00:00
|
|
|
"properties are set correctly during initialization",
|
2026-02-11 18:22:26 +00:00
|
|
|
arguments: [TodoGroup.today, .upcoming, .overdue, .completed]
|
2026-02-11 13:00:46 +00:00
|
|
|
)
|
|
|
|
|
func initialization(for group: TodoGroup) {
|
|
|
|
|
// GIVEN
|
|
|
|
|
// WHEN
|
|
|
|
|
let viewModel = TaskListView.ViewModel(group: group)
|
|
|
|
|
|
|
|
|
|
// THEN
|
|
|
|
|
#expect(viewModel.group == group)
|
|
|
|
|
#expect(viewModel.showCompleted == false)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: Computed property tests
|
|
|
|
|
|
|
|
|
|
@Test(
|
|
|
|
|
"buttonCompleted returns correct values when showCompleted is true",
|
2026-02-11 18:22:26 +00:00
|
|
|
arguments: [TodoGroup.today, .upcoming, .overdue, .completed], [true, false]
|
2026-02-11 13:00:46 +00:00
|
|
|
)
|
|
|
|
|
func buttonCompleted(
|
|
|
|
|
for group: TodoGroup,
|
|
|
|
|
showCompleted: Bool
|
|
|
|
|
) {
|
|
|
|
|
// GIVEN
|
|
|
|
|
let viewModel = TaskListView.ViewModel(group: group)
|
2026-02-11 15:28:22 +00:00
|
|
|
|
2026-02-11 13:00:46 +00:00
|
|
|
viewModel.showCompleted = showCompleted
|
|
|
|
|
|
|
|
|
|
// WHEN
|
|
|
|
|
let (title, icon) = viewModel.buttonCompleted
|
|
|
|
|
|
|
|
|
|
// THEN
|
|
|
|
|
if showCompleted {
|
|
|
|
|
#expect(title == "view.task-list.label.hide-completed")
|
|
|
|
|
#expect(icon == "eye.slash")
|
2026-02-11 15:28:22 +00:00
|
|
|
}
|
|
|
|
|
else {
|
2026-02-11 13:00:46 +00:00
|
|
|
#expect(title == "view.task-list.label.show-completed")
|
|
|
|
|
#expect(icon == "eye")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test(
|
|
|
|
|
"canShowCompleted returns correct value for each group",
|
|
|
|
|
arguments: zip(
|
2026-02-11 18:22:26 +00:00
|
|
|
[TodoGroup.today, .upcoming, .overdue, .completed],
|
2026-02-11 13:00:46 +00:00
|
|
|
[true, true, true, false]
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
func canShowCompleted(
|
|
|
|
|
for group: TodoGroup,
|
|
|
|
|
expects expectation: Bool
|
|
|
|
|
) {
|
|
|
|
|
// GIVEN
|
|
|
|
|
let viewModel = TaskListView.ViewModel(group: group)
|
|
|
|
|
|
|
|
|
|
// WHEN
|
|
|
|
|
let canShowCompleted = viewModel.canShowCompleted
|
|
|
|
|
|
|
|
|
|
// THEN
|
|
|
|
|
#expect(canShowCompleted == expectation)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test(
|
|
|
|
|
"navigationTitle returns correct view title for each group",
|
2026-02-11 18:22:26 +00:00
|
|
|
arguments: [TodoGroup.today, .upcoming, .overdue, .completed]
|
2026-02-10 15:31:52 +00:00
|
|
|
)
|
|
|
|
|
func navigationTitle(for group: TodoGroup) {
|
|
|
|
|
// GIVEN
|
|
|
|
|
let viewModel = TaskListView.ViewModel(group: group)
|
|
|
|
|
|
|
|
|
|
// WHEN
|
|
|
|
|
let title = viewModel.navigationTitle
|
|
|
|
|
|
|
|
|
|
// THEN
|
|
|
|
|
#expect(title == group.title)
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-11 13:00:46 +00:00
|
|
|
// MARK: Method tests
|
|
|
|
|
|
2026-02-11 18:22:26 +00:00
|
|
|
@Test("delete removes a single todo task from repository")
|
|
|
|
|
func deleteSingleTodo() {
|
2026-02-11 16:35:51 +00:00
|
|
|
// GIVEN
|
2026-02-11 18:22:26 +00:00
|
|
|
let todos: [Todo] = [
|
2026-02-11 16:35:51 +00:00
|
|
|
.init(title: "Task 1", due: .now, order: 0),
|
|
|
|
|
.init(title: "Task 2", due: .now, order: 1),
|
|
|
|
|
.init(title: "Task 3", due: .now, order: 2),
|
|
|
|
|
]
|
2026-02-11 18:22:26 +00:00
|
|
|
let repository = MockTodoRepository(todos: todos)
|
|
|
|
|
let viewModel = TaskListView.ViewModel(group: .today)
|
2026-02-11 16:35:51 +00:00
|
|
|
|
|
|
|
|
// WHEN
|
2026-02-11 18:22:26 +00:00
|
|
|
viewModel.delete(
|
|
|
|
|
todos,
|
2026-02-11 16:35:51 +00:00
|
|
|
at: IndexSet(integer: 1),
|
2026-02-11 18:22:26 +00:00
|
|
|
from: repository
|
2026-02-11 16:35:51 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// THEN
|
2026-02-11 18:22:26 +00:00
|
|
|
#expect(repository.todos.count == 2)
|
|
|
|
|
#expect(repository.todos.contains { $0.title == "Task 1" })
|
|
|
|
|
#expect(repository.todos.contains { $0.title == "Task 3" })
|
|
|
|
|
#expect(!repository.todos.contains { $0.title == "Task 2" })
|
|
|
|
|
}
|
2026-02-11 16:35:51 +00:00
|
|
|
|
2026-02-11 18:22:26 +00:00
|
|
|
@Test("delete removes multiple todo tasks from repository")
|
|
|
|
|
func deleteMultipleTodos() {
|
|
|
|
|
// GIVEN
|
|
|
|
|
let todos: [Todo] = [
|
|
|
|
|
.init(title: "Task 1", due: .now, order: 0),
|
|
|
|
|
.init(title: "Task 2", due: .now, order: 1),
|
|
|
|
|
.init(title: "Task 3", due: .now, order: 2),
|
|
|
|
|
.init(title: "Task 4", due: .now, order: 3),
|
|
|
|
|
.init(title: "Task 5", due: .now, order: 4),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
let repository = MockTodoRepository(todos: todos)
|
|
|
|
|
let viewModel = TaskListView.ViewModel(group: .today)
|
|
|
|
|
|
|
|
|
|
// WHEN
|
|
|
|
|
viewModel.delete(
|
|
|
|
|
todos,
|
|
|
|
|
at: IndexSet([1, 3]),
|
|
|
|
|
from: repository
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// THEN
|
|
|
|
|
#expect(repository.todos.count == 3)
|
|
|
|
|
#expect(repository.todos.contains { $0.title == "Task 1" })
|
|
|
|
|
#expect(repository.todos.contains { $0.title == "Task 3" })
|
|
|
|
|
#expect(repository.todos.contains { $0.title == "Task 5" })
|
|
|
|
|
#expect(!repository.todos.contains { $0.title == "Task 2" })
|
|
|
|
|
#expect(!repository.todos.contains { $0.title == "Task 4" })
|
2026-02-11 16:35:51 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-11 15:28:22 +00:00
|
|
|
@Test("reorderTasks updates order property when moving task forward")
|
|
|
|
|
func reorderTasksMovingForward() {
|
|
|
|
|
// GIVEN
|
|
|
|
|
let viewModel = TaskListView.ViewModel(group: .today)
|
|
|
|
|
let tasks: [Todo] = [
|
|
|
|
|
.init(title: "Task 1", due: .now, order: 0),
|
|
|
|
|
.init(title: "Task 2", due: .now, order: 1),
|
|
|
|
|
.init(title: "Task 3", due: .now, order: 2),
|
|
|
|
|
.init(title: "Task 4", due: .now, order: 3),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
// WHEN
|
|
|
|
|
viewModel.reorderTasks(
|
|
|
|
|
tasks,
|
|
|
|
|
from: IndexSet(integer: 0),
|
|
|
|
|
to: 3
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// THEN
|
|
|
|
|
#expect(tasks[0].order == 2)
|
|
|
|
|
#expect(tasks[1].order == 0)
|
|
|
|
|
#expect(tasks[2].order == 1)
|
|
|
|
|
#expect(tasks[3].order == 3)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test("reorderTasks updates order property when moving task backward")
|
|
|
|
|
func reorderTasksMovingBackward() {
|
|
|
|
|
// GIVEN
|
|
|
|
|
let viewModel = TaskListView.ViewModel(group: .today)
|
|
|
|
|
let tasks: [Todo] = [
|
|
|
|
|
.init(title: "Task 1", due: .now, order: 0),
|
|
|
|
|
.init(title: "Task 2", due: .now, order: 1),
|
|
|
|
|
.init(title: "Task 3", due: .now, order: 2),
|
|
|
|
|
.init(title: "Task 4", due: .now, order: 3),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
// WHEN
|
|
|
|
|
viewModel.reorderTasks(
|
|
|
|
|
tasks,
|
|
|
|
|
from: IndexSet(integer: 3),
|
|
|
|
|
to: 1
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// THEN
|
|
|
|
|
#expect(tasks[0].order == 0)
|
|
|
|
|
#expect(tasks[1].order == 2)
|
|
|
|
|
#expect(tasks[2].order == 3)
|
|
|
|
|
#expect(tasks[3].order == 1)
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-11 18:22:26 +00:00
|
|
|
@Test("toggleCompletedVisibility toggles the showCompleted property")
|
2026-02-11 13:00:46 +00:00
|
|
|
func toggleCompletedVisibility() {
|
|
|
|
|
// GIVEN
|
|
|
|
|
let viewModel = TaskListView.ViewModel(group: .today)
|
|
|
|
|
let initialValue = viewModel.showCompleted
|
|
|
|
|
|
|
|
|
|
// WHEN
|
|
|
|
|
viewModel.toggleCompletedVisibility()
|
|
|
|
|
let afterFirstToggle = viewModel.showCompleted
|
|
|
|
|
|
|
|
|
|
viewModel.toggleCompletedVisibility()
|
|
|
|
|
let afterSecondToggle = viewModel.showCompleted
|
|
|
|
|
|
|
|
|
|
// THEN
|
|
|
|
|
#expect(initialValue == false)
|
|
|
|
|
#expect(afterFirstToggle == true)
|
|
|
|
|
#expect(afterSecondToggle == false)
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 15:31:52 +00:00
|
|
|
}
|