listless

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

ItemStoreEdgeCaseTests.swift (9519B)


      1 import Foundation
      2 import CoreData
      3 import Testing
      4 
      5 #if os(macOS)
      6 @testable import Listless
      7 #else
      8 @testable import Listless_iOS
      9 #endif
     10 
     11 @Suite("ItemStore Edge Cases", .serialized)
     12 @MainActor
     13 struct ItemStoreEdgeCaseTests {
     14 
     15     // MARK: - Title Edge Cases
     16 
     17     @Test("Task with empty title")
     18     func itemWithEmptyTitle() async throws {
     19         let store = makeTestStore()
     20 
     21         let item = try store.createItem(title: "")
     22 
     23         let items = try store.fetchItems()
     24         #expect(items.first?.title == "")
     25     }
     26 
     27     @Test("Task with very long title")
     28     func itemWithVeryLongTitle() async throws {
     29         let store = makeTestStore()
     30         let longTitle = String(repeating: "A", count: 10_000)
     31 
     32         let item = try store.createItem(title: longTitle)
     33 
     34         let items = try store.fetchItems()
     35         #expect(items.first?.title.count == 10_000)
     36     }
     37 
     38     @Test("Task with special characters")
     39     func itemWithSpecialCharacters() async throws {
     40         let store = makeTestStore()
     41         let specialTitle = "Test 🎉 with émojis & spëcial çharacters! @#$%^&*()"
     42 
     43         let item = try store.createItem(title: specialTitle)
     44 
     45         let items = try store.fetchItems()
     46         #expect(items.first?.title == specialTitle)
     47     }
     48 
     49     @Test("Task with newlines and tabs")
     50     func itemWithNewlinesAndTabs() async throws {
     51         let store = makeTestStore()
     52         let multilineTitle = "Line 1\nLine 2\tTabbed"
     53 
     54         let item = try store.createItem(title: multilineTitle)
     55 
     56         let items = try store.fetchItems()
     57         #expect(items.first?.title == multilineTitle)
     58     }
     59 
     60     // MARK: - Large Data Sets
     61 
     62     @Test("Create many items")
     63     func createManyItems() async throws {
     64         let store = makeTestStore()
     65         let count = 100
     66 
     67         for i in 0..<count {
     68             _ = try store.createItem(title: "Task \(i)")
     69         }
     70 
     71         let items = try store.fetchItems()
     72         #expect(items.count == count)
     73     }
     74 
     75     @Test("Delete all items from large set")
     76     func deleteAllItemsFromLargeSet() async throws {
     77         let store = makeTestStore()
     78         var itemIDs: [UUID] = []
     79 
     80         for i in 0..<50 {
     81             let item = try store.createItem(title: "Task \(i)")
     82             itemIDs.append(item.id)
     83         }
     84 
     85         for id in itemIDs {
     86             try store.delete(itemID: id)
     87         }
     88 
     89         let items = try store.fetchItems()
     90         #expect(items.isEmpty)
     91     }
     92 
     93     // MARK: - State Transitions
     94 
     95     @Test("Create item after completing all items")
     96     func createItemAfterCompletingAllItems() async throws {
     97         let (store, itemIDs) = try makeTestStoreWithItems(count: 3)
     98 
     99         for id in itemIDs {
    100             try store.complete(itemID: id)
    101         }
    102 
    103         let newItem = try store.createItem(title: "New item")
    104 
    105         let items = try store.fetchItems()
    106         let activeItems = items.filter { !$0.isCompleted }
    107         #expect(activeItems.count == 1)
    108         #expect(activeItems[0].id == newItem.id)
    109     }
    110 
    111     @Test("Rapid updates to same item")
    112     func rapidUpdatesToSameItem() async throws {
    113         let store = makeTestStore()
    114         let item = try store.createItem(title: "Original")
    115 
    116         for i in 0..<10 {
    117             try store.update(itemID: item.id, title: "Update \(i)")
    118         }
    119 
    120         let items = try store.fetchItems()
    121         #expect(items.first?.title == "Update 9")
    122     }
    123 
    124     // MARK: - Store State Tests
    125 
    126     @Test("Store with only completed items")
    127     func storeWithOnlyCompletedItems() async throws {
    128         let (store, itemIDs) = try makeTestStoreWithItems(count: 5)
    129 
    130         for id in itemIDs {
    131             try store.complete(itemID: id)
    132         }
    133 
    134         let items = try store.fetchItems()
    135         #expect(items.allSatisfy { $0.isCompleted })
    136         #expect(items.count == 5)
    137     }
    138 
    139     @Test("SortOrder after completing all items")
    140     func sortOrderAfterCompletingAllItems() async throws {
    141         let (store, itemIDs) = try makeTestStoreWithItems(count: 3)
    142 
    143         for id in itemIDs {
    144             try store.complete(itemID: id)
    145         }
    146 
    147         let newItem1 = try store.createItem(title: "New 1")
    148         let newItem2 = try store.createItem(title: "New 2")
    149 
    150         let activeItems = try store.fetchItems().filter { !$0.isCompleted }
    151         #expect(activeItems[0].id == newItem1.id)
    152         #expect(activeItems[1].id == newItem2.id)
    153         #expect(activeItems[1].sortOrder > activeItems[0].sortOrder)
    154     }
    155 
    156     @Test("Uncompleting item moves it back to active")
    157     func uncompletingItemMovesItBackToActive() async throws {
    158         let (store, itemIDs) = try makeTestStoreWithItems(count: 3)
    159         try store.complete(itemID: itemIDs[1])
    160 
    161         try store.uncomplete(itemID: itemIDs[1])
    162 
    163         let items = try store.fetchItems()
    164         let activeItems = items.filter { !$0.isCompleted }
    165         #expect(activeItems.count == 3)
    166         #expect(activeItems.contains { $0.id == itemIDs[1] })
    167     }
    168 
    169     @Test("Uncomplete legacy sortOrder zero conflict appends to end")
    170     func uncompleteLegacyZeroConflictAppendsToEnd() async throws {
    171         let store = makeTestStore()
    172         let activeItem = try store.createItem(title: "Active")
    173         let completedItem = try store.createItem(title: "Completed")
    174 
    175         activeItem.sortOrder = 0
    176         try store.complete(itemID: completedItem.id)
    177         completedItem.sortOrder = 0
    178         try store.save()
    179 
    180         try store.uncomplete(itemID: completedItem.id)
    181 
    182         let activeItems = try store.fetchItems().filter { !$0.isCompleted }
    183             .sorted { $0.sortOrder < $1.sortOrder }
    184         #expect(activeItems.count == 2)
    185         #expect(activeItems[0].id == activeItem.id)
    186         #expect(activeItems[1].id == completedItem.id)
    187         #expect(activeItems[1].sortOrder > activeItems[0].sortOrder)
    188     }
    189 
    190     @Test("Merge policy prefers store when store updatedAt is newer")
    191     func mergePolicyPrefersStoreWhenStoreIsNewer() async throws {
    192         let controller = PersistenceController(inMemory: true)
    193         let viewContext = controller.viewContext
    194         viewContext.automaticallyMergesChangesFromParent = false
    195 
    196         let item = ItemEntity(context: viewContext)
    197         item.title = "Original"
    198         item.updatedAt = Date(timeIntervalSince1970: 100)
    199         try viewContext.save()
    200 
    201         let itemID = item.id
    202 
    203         let backgroundContext = controller.container.newBackgroundContext()
    204         try await backgroundContext.perform {
    205             let request = ItemEntity.fetchRequest()
    206             request.predicate = NSPredicate(format: "id == %@", itemID as CVarArg)
    207             request.fetchLimit = 1
    208 
    209             guard let remote = try backgroundContext.fetch(request).first else {
    210                 Issue.record("Expected item to exist in background context")
    211                 return
    212             }
    213 
    214             remote.title = "Remote newer"
    215             remote.updatedAt = Date(timeIntervalSince1970: 200)
    216             try backgroundContext.save()
    217         }
    218 
    219         item.updatedAt = Date(timeIntervalSince1970: 150)
    220         try viewContext.save()
    221 
    222         let verifyContext = controller.container.newBackgroundContext()
    223         try await verifyContext.perform {
    224             let request = ItemEntity.fetchRequest()
    225             request.predicate = NSPredicate(format: "id == %@", itemID as CVarArg)
    226             request.fetchLimit = 1
    227 
    228             guard let resolved = try verifyContext.fetch(request).first else {
    229                 Issue.record("Expected item to exist in verify context")
    230                 return
    231             }
    232 
    233             #expect(resolved.title == "Remote newer")
    234             #expect(resolved.updatedAt == Date(timeIntervalSince1970: 200))
    235         }
    236     }
    237 
    238     @Test("Merge policy prefers local when local updatedAt is newer")
    239     func mergePolicyPrefersLocalWhenLocalIsNewer() async throws {
    240         let controller = PersistenceController(inMemory: true)
    241         let viewContext = controller.viewContext
    242         viewContext.automaticallyMergesChangesFromParent = false
    243 
    244         let item = ItemEntity(context: viewContext)
    245         item.title = "Original"
    246         item.updatedAt = Date(timeIntervalSince1970: 100)
    247         try viewContext.save()
    248 
    249         let itemID = item.id
    250 
    251         let backgroundContext = controller.container.newBackgroundContext()
    252         try await backgroundContext.perform {
    253             let request = ItemEntity.fetchRequest()
    254             request.predicate = NSPredicate(format: "id == %@", itemID as CVarArg)
    255             request.fetchLimit = 1
    256 
    257             guard let remote = try backgroundContext.fetch(request).first else {
    258                 Issue.record("Expected item to exist in background context")
    259                 return
    260             }
    261 
    262             remote.title = "Remote older"
    263             remote.updatedAt = Date(timeIntervalSince1970: 200)
    264             try backgroundContext.save()
    265         }
    266 
    267         item.updatedAt = Date(timeIntervalSince1970: 300)
    268         try viewContext.save()
    269 
    270         let verifyContext = controller.container.newBackgroundContext()
    271         try await verifyContext.perform {
    272             let request = ItemEntity.fetchRequest()
    273             request.predicate = NSPredicate(format: "id == %@", itemID as CVarArg)
    274             request.fetchLimit = 1
    275 
    276             guard let resolved = try verifyContext.fetch(request).first else {
    277                 Issue.record("Expected item to exist in verify context")
    278                 return
    279             }
    280 
    281             #expect(resolved.title == "Original")
    282             #expect(resolved.updatedAt == Date(timeIntervalSince1970: 300))
    283         }
    284     }
    285 }