listless

A simple list app for Apple platforms
Log | Files | Refs | README | LICENSE

ItemStoreCompletionTests.swift (6404B)


      1 import Foundation
      2 import Testing
      3 
      4 #if os(macOS)
      5 @testable import Listless
      6 #else
      7 @testable import Listless_iOS
      8 #endif
      9 
     10 @Suite("ItemStore Completion Behavior", .serialized)
     11 @MainActor
     12 struct ItemStoreCompletionTests {
     13 
     14     // MARK: - Basic Completion Tests
     15 
     16     @Test("Complete item")
     17     func completeItem() async throws {
     18         let store = makeTestStore()
     19         let item = try store.createItem(title: "Task to complete")
     20 
     21         try store.complete(itemID: item.id)
     22 
     23         let items = try store.fetchItems()
     24         #expect(items.first?.isCompleted == true)
     25     }
     26 
     27     @Test("Uncomplete item")
     28     func uncompleteItem() async throws {
     29         let store = makeTestStore()
     30         let item = try store.createItem(title: "Item")
     31         try store.complete(itemID: item.id)
     32 
     33         try store.uncomplete(itemID: item.id)
     34 
     35         let items = try store.fetchItems()
     36         #expect(items.first?.isCompleted == false)
     37     }
     38 
     39     @Test("Complete with invalid ID does nothing")
     40     func completeWithInvalidIDDoesNothing() async throws {
     41         let store = makeTestStore()
     42         let item = try store.createItem(title: "Item")
     43         let invalidID = UUID()
     44 
     45         try store.complete(itemID: invalidID)
     46 
     47         let items = try store.fetchItems()
     48         #expect(items.first?.isCompleted == false)
     49     }
     50 
     51     @Test("Uncomplete with invalid ID does nothing")
     52     func uncompleteWithInvalidIDDoesNothing() async throws {
     53         let store = makeTestStore()
     54         let item = try store.createItem(title: "Item")
     55         try store.complete(itemID: item.id)
     56         let invalidID = UUID()
     57 
     58         try store.uncomplete(itemID: invalidID)
     59 
     60         let items = try store.fetchItems()
     61         #expect(items.first?.isCompleted == true)
     62     }
     63 
     64     // MARK: - Timestamp Tests
     65 
     66     @Test("Completing item updates timestamp")
     67     func completingItemUpdatesTimestamp() async throws {
     68         let store = makeTestStore()
     69         let item = try store.createItem(title: "Item")
     70         let originalUpdatedAt = item.updatedAt
     71 
     72         // Small delay to ensure timestamp difference
     73         try await Task.sleep(nanoseconds: 10_000_000) // 10ms
     74 
     75         try store.complete(itemID: item.id)
     76 
     77         let items = try store.fetchItems()
     78         let updatedItem = items.first
     79         #expect(updatedItem?.updatedAt ?? Date() > originalUpdatedAt)
     80     }
     81 
     82     // MARK: - Sorting Tests
     83 
     84     @Test("Active items appear before completed items")
     85     func activeItemsAppearBeforeCompletedItems() async throws {
     86         let store = makeTestStore()
     87         let item1 = try store.createItem(title: "Item 1")
     88         let item2 = try store.createItem(title: "Item 2")
     89         let item3 = try store.createItem(title: "Item 3")
     90 
     91         try store.complete(itemID: item2.id)
     92 
     93         let items = try store.fetchItems()
     94         #expect(items[0].id == item1.id)
     95         #expect(items[1].id == item3.id)
     96         #expect(items[2].id == item2.id)
     97     }
     98 
     99     @Test("Completed items sorted by completedOrder")
    100     func completedItemsSortedByCompletedOrder() async throws {
    101         let store = makeTestStore()
    102         let item1 = try store.createItem(title: "Item 1")
    103         let item2 = try store.createItem(title: "Item 2")
    104         let item3 = try store.createItem(title: "Item 3")
    105 
    106         // Complete in specific order
    107         try store.complete(itemID: item2.id)
    108         try store.complete(itemID: item1.id)
    109         try store.complete(itemID: item3.id)
    110 
    111         let items = try store.fetchItems()
    112         // All completed, should be sorted by completedOrder (most recently completed first)
    113         #expect(items[0].id == item3.id)
    114         #expect(items[1].id == item1.id)
    115         #expect(items[2].id == item2.id)
    116     }
    117 
    118     @Test("Toggle completion multiple times")
    119     func toggleCompletionMultipleTimes() async throws {
    120         let store = makeTestStore()
    121         let item = try store.createItem(title: "Item")
    122 
    123         try store.complete(itemID: item.id)
    124         var items = try store.fetchItems()
    125         #expect(items.first?.isCompleted == true)
    126 
    127         try store.uncomplete(itemID: item.id)
    128         items = try store.fetchItems()
    129         #expect(items.first?.isCompleted == false)
    130 
    131         try store.complete(itemID: item.id)
    132         items = try store.fetchItems()
    133         #expect(items.first?.isCompleted == true)
    134     }
    135 
    136     @Test("Complete all items")
    137     func completeAllItems() async throws {
    138         let (store, itemIDs) = try makeTestStoreWithItems(count: 5)
    139 
    140         for id in itemIDs {
    141             try store.complete(itemID: id)
    142         }
    143 
    144         let items = try store.fetchItems()
    145         #expect(items.allSatisfy { $0.isCompleted })
    146         #expect(items.count == 5)
    147     }
    148 
    149     @Test("Uncomplete restores previous sortOrder when no active conflict")
    150     func uncompleteRestoresPreviousSortOrderWhenNoConflict() async throws {
    151         let (store, itemIDs) = try makeTestStoreWithItems(count: 3)
    152         let itemToRestoreID = itemIDs[1]
    153 
    154         let originalSortOrder = try store.fetchItems().first { $0.id == itemToRestoreID }?.sortOrder
    155         #expect(originalSortOrder != nil)
    156 
    157         try store.complete(itemID: itemToRestoreID)
    158         try store.uncomplete(itemID: itemToRestoreID)
    159 
    160         let activeItems = try store.fetchItems().filter { !$0.isCompleted }
    161         let restoredItem = activeItems.first { $0.id == itemToRestoreID }
    162 
    163         #expect(restoredItem != nil)
    164         #expect(restoredItem?.sortOrder == originalSortOrder)
    165         #expect(activeItems.count == 3)
    166     }
    167 
    168     @Test("Uncomplete appends item when restored sortOrder conflicts with active item")
    169     func uncompleteAppendsWhenRestoredSortOrderConflicts() async throws {
    170         let store = makeTestStore()
    171         let activeItem = try store.createItem(title: "Active item")
    172         let completedItem = try store.createItem(title: "Completed item")
    173 
    174         try store.complete(itemID: completedItem.id)
    175         try store.moveItem(itemID: activeItem.id, toIndex: 0)
    176         completedItem.sortOrder = activeItem.sortOrder
    177         try store.save()
    178 
    179         try store.uncomplete(itemID: completedItem.id)
    180 
    181         let activeItems = try store.fetchItems().filter { !$0.isCompleted }
    182             .sorted { $0.sortOrder < $1.sortOrder }
    183         let lastActiveItem = activeItems.last
    184 
    185         #expect(activeItems.count == 2)
    186         #expect(lastActiveItem?.id == completedItem.id)
    187         #expect(lastActiveItem?.sortOrder ?? 0 > activeItem.sortOrder)
    188     }
    189 }