diff --git a/App/Sources/Extensions/TodoFetchDescriptor+Inits.swift b/App/Sources/Extensions/TodoFetchDescriptor+Inits.swift index ad3382d..1acbafb 100644 --- a/App/Sources/Extensions/TodoFetchDescriptor+Inits.swift +++ b/App/Sources/Extensions/TodoFetchDescriptor+Inits.swift @@ -13,9 +13,12 @@ extension FetchDescriptor where T == Todo { /// relevance and completion status. Each group applies appropriate predicates and sort orders: /// /// - **Completed**: All tasks marked as done, sorted by completion date (most recent first) - /// - **Overdue**: Incomplete tasks with due dates before today, sorted by due date (most recent first) - /// - **Today**: Incomplete tasks due today, sorted by due date (earliest first) - /// - **Upcoming**: Incomplete tasks due after today, sorted by due date (earliest first) + /// - **Overdue**: Incomplete tasks with due dates before today, sorted by user-defined order, then by due date (most recent first) + /// - **Today**: Incomplete tasks due today, sorted by user-defined order, then by due date (earliest first) + /// - **Upcoming**: Incomplete tasks due after today, sorted by user-defined order, then by due date (earliest first) + /// + /// The user-defined order allows tasks to be manually reordered within each group through drag and drop, + /// while maintaining a secondary sort by date for tasks with the same order value. /// /// - Parameters: /// - group: The todo group category to filter by. @@ -52,11 +55,11 @@ extension FetchDescriptor where T == Todo { case .completed: [.init(\Todo.completed, order: .reverse)] case .overdue: - [.init(\Todo.due, order: .reverse)] + [.init(\Todo.order, order: .forward), .init(\Todo.due, order: .reverse)] case .today: - [.init(\Todo.due, order: .forward)] + [.init(\Todo.order, order: .forward), .init(\Todo.due, order: .forward)] case .upcoming: - [.init(\Todo.due, order: .forward)] + [.init(\Todo.order, order: .forward), .init(\Todo.due, order: .forward)] } self.init( diff --git a/UnitTests/Tests/Extensions/TodoFetchDescriptorTests.swift b/UnitTests/Tests/Extensions/TodoFetchDescriptor+InitsTests.swift similarity index 57% rename from UnitTests/Tests/Extensions/TodoFetchDescriptorTests.swift rename to UnitTests/Tests/Extensions/TodoFetchDescriptor+InitsTests.swift index 9a521cb..6703150 100644 --- a/UnitTests/Tests/Extensions/TodoFetchDescriptorTests.swift +++ b/UnitTests/Tests/Extensions/TodoFetchDescriptor+InitsTests.swift @@ -4,13 +4,13 @@ import Testing @testable import Todos -@Suite("FetchDescriptor extension tests") -struct TodoFetchDescriptorTests { +@Suite("TodoFetchDescriptor+Inits extension tests") +struct TodoFetchDescriptor_InitsTests { // MARK: Initialization tests - + @Test( - "initializes with group when empty tasks", + "initialization when empty tasks", arguments: TodoGroup.allCases ) @MainActor @@ -18,58 +18,58 @@ struct TodoFetchDescriptorTests { // GIVEN let container = try modelContainer let context = container.mainContext - + // WHEN let descriptor = FetchDescriptor( group: .today, showCompleted: .random() ) let results = try context.fetch(descriptor) - + // THEN #expect(results.isEmpty) } - + @Test( - "initializes with group and show completed disabled when filled tasks", + "initialization with show completed disabled", arguments: TodoGroup.allCases ) @MainActor - func initializationWhenFilledTasks(for group: TodoGroup) throws { + func initializationWithNoShowCompleted(for group: TodoGroup) throws { // GIVEN let container = try modelContainer let context = container.mainContext - + insert(into: context) - + // WHEN let descriptor = FetchDescriptor( group: group, showCompleted: false ) let results = try context.fetch(descriptor) - + // THEN #expect(results.count == 2) - + switch group { - case .completed: - #expect(results[0].title == "Completed task #2") - #expect(results[1].title == "Completed task #1") - case .overdue: - #expect(results[0].title == "Overdue task #2") - #expect(results[1].title == "Overdue task #1") - case .today: - #expect(results[0].title == "Today task #1") - #expect(results[1].title == "Today task #2") - case .upcoming: - #expect(results[0].title == "Upcoming task #1") - #expect(results[1].title == "Upcoming task #2") + case .completed: + #expect(results[0].title == "Completed task #2") + #expect(results[1].title == "Completed task #1") + case .overdue: + #expect(results[0].title == "Overdue task #2") + #expect(results[1].title == "Overdue task #1") + case .today: + #expect(results[0].title == "Today task #1") + #expect(results[1].title == "Today task #2") + case .upcoming: + #expect(results[0].title == "Upcoming task #1") + #expect(results[1].title == "Upcoming task #2") } } - + @Test( - "initializes with group and show completed enabled when filled tasks", + "initialization with show completed enabled", arguments: TodoGroup.allCases ) @MainActor @@ -77,38 +77,39 @@ struct TodoFetchDescriptorTests { // GIVEN let container = try modelContainer let context = container.mainContext - + insert(into: context) - + // WHEN let descriptor = FetchDescriptor( group: group, showCompleted: true ) let results = try context.fetch(descriptor) - + // THEN if group == .overdue || group == .upcoming { #expect(results.count == 3) - } else { + } + else { #expect(results.count == 2) } - + switch group { - case .completed: - #expect(results[0].title == "Completed task #2") - #expect(results[1].title == "Completed task #1") - case .overdue: - #expect(results[0].title == "Completed task #1") - #expect(results[1].title == "Overdue task #2") - #expect(results[2].title == "Overdue task #1") - case .today: - #expect(results[0].title == "Today task #1") - #expect(results[1].title == "Today task #2") - case .upcoming: - #expect(results[0].title == "Upcoming task #1") - #expect(results[1].title == "Upcoming task #2") - #expect(results[2].title == "Completed task #2") + case .completed: + #expect(results[0].title == "Completed task #2") + #expect(results[1].title == "Completed task #1") + case .overdue: + #expect(results[0].title == "Overdue task #2") + #expect(results[1].title == "Completed task #1") + #expect(results[2].title == "Overdue task #1") + case .today: + #expect(results[0].title == "Today task #1") + #expect(results[1].title == "Today task #2") + case .upcoming: + #expect(results[0].title == "Completed task #2") + #expect(results[1].title == "Upcoming task #1") + #expect(results[2].title == "Upcoming task #2") } } @@ -116,7 +117,7 @@ struct TodoFetchDescriptorTests { // MARK: - Helpers -private extension TodoFetchDescriptorTests { +private extension TodoFetchDescriptor_InitsTests { // MARK: Computed @@ -138,40 +139,48 @@ private extension TodoFetchDescriptorTests { let tasks: [Todo] = [ .init( title: "Today task #1", - due: .now + due: .now, + order: 0 ), .init( title: "Today task #2", - due: .now + due: .now, + order: 1 ), .init( title: "Upcoming task #1", - due: calendar.date(byAdding: .day, value: 1, to: .now) ?? .now + due: calendar.date(byAdding: .day, value: 2, to: .now) ?? .now, + order: 0 ), .init( title: "Upcoming task #2", - due: calendar.date(byAdding: .day, value: 1, to: .now) ?? .now + due: calendar.date(byAdding: .day, value: 3, to: .now) ?? .now, + order: 1 ), .init( title: "Overdue task #1", - due: calendar.date(byAdding: .day, value: -1, to: .now) ?? .now + due: calendar.date(byAdding: .day, value: -1, to: .now) ?? .now, + order: 1 ), .init( title: "Overdue task #2", - due: calendar.date(byAdding: .day, value: -1, to: .now) ?? .now + due: calendar.date(byAdding: .day, value: -1, to: .now) ?? .now, + order: 0 ), .init( title: "Completed task #1", due: calendar.date(byAdding: .day, value: -1, to: .now) ?? .now, - completed: .now + completed: .now, + order: 1 ), .init( title: "Completed task #2", due: calendar.date(byAdding: .day, value: 1, to: .now) ?? .now, - completed: .now - ) + completed: .now, + order: 0 + ), ] - + for task in tasks { context.insert(task) }