listless

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

ItemStoreDeleteAndNormalizeTests.swift (6516B)


      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 Delete Multiple & Normalize", .serialized)
     11 @MainActor
     12 struct ItemStoreDeleteAndNormalizeTests {
     13 
     14     // MARK: - deleteMultiple
     15 
     16     @Test("Delete multiple items removes all specified items")
     17     func deleteMultipleBasic() async throws {
     18         let (store, ids) = try makeTestStoreWithItems(count: 4)
     19 
     20         try store.deleteMultiple(itemIDs: [ids[0], ids[2]])
     21 
     22         let remaining = try store.fetchItems()
     23         let remainingIDs = remaining.map(\.id)
     24         #expect(remainingIDs.contains(ids[1]))
     25         #expect(remainingIDs.contains(ids[3]))
     26         #expect(!remainingIDs.contains(ids[0]))
     27         #expect(!remainingIDs.contains(ids[2]))
     28         #expect(remaining.count == 2)
     29     }
     30 
     31     @Test("Delete multiple with empty array does nothing")
     32     func deleteMultipleEmpty() async throws {
     33         let (store, _) = try makeTestStoreWithItems(count: 3)
     34 
     35         try store.deleteMultiple(itemIDs: [])
     36 
     37         let items = try store.fetchItems()
     38         #expect(items.count == 3)
     39     }
     40 
     41     @Test("Delete multiple skips unknown IDs without error")
     42     func deleteMultipleWithUnknownIDs() async throws {
     43         let (store, ids) = try makeTestStoreWithItems(count: 3)
     44 
     45         try store.deleteMultiple(itemIDs: [ids[0], UUID(), ids[2]])
     46 
     47         let remaining = try store.fetchItems()
     48         #expect(remaining.count == 1)
     49         #expect(remaining[0].id == ids[1])
     50     }
     51 
     52     @Test("Delete multiple handles mix of active and completed items")
     53     func deleteMultipleMixedState() async throws {
     54         let (store, ids) = try makeTestStoreWithItems(count: 4)
     55         try store.complete(itemID: ids[1])
     56         try store.complete(itemID: ids[3])
     57 
     58         try store.deleteMultiple(itemIDs: [ids[0], ids[1]])
     59 
     60         let remaining = try store.fetchItems()
     61         let remainingIDs = Set(remaining.map(\.id))
     62         #expect(remainingIDs == Set([ids[2], ids[3]]))
     63     }
     64 
     65     @Test("Delete multiple with all items leaves store empty")
     66     func deleteMultipleAll() async throws {
     67         let (store, ids) = try makeTestStoreWithItems(count: 3)
     68 
     69         try store.deleteMultiple(itemIDs: ids)
     70 
     71         let items = try store.fetchItems()
     72         #expect(items.isEmpty)
     73     }
     74 
     75     // MARK: - normalizeSortOrders
     76 
     77     @Test("Normalize assigns evenly spaced sort orders")
     78     func normalizeBasic() async throws {
     79         let (store, ids) = try makeTestStoreWithItems(count: 3)
     80         // Move items around to create irregular spacing.
     81         try store.moveItem(itemID: ids[2], toIndex: 0)
     82 
     83         try store.normalizeSortOrders()
     84 
     85         let items = try store.fetchItems().filter { !$0.isCompleted }
     86         #expect(items.count == 3)
     87         for (index, item) in items.enumerated() {
     88             #expect(item.sortOrder == Int64(index) * 1000)
     89         }
     90     }
     91 
     92     @Test("Normalize preserves existing order")
     93     func normalizePreservesOrder() async throws {
     94         let (store, _) = try makeTestStoreWithItems(
     95             count: 3, titles: ["First", "Second", "Third"])
     96 
     97         try store.normalizeSortOrders()
     98 
     99         let items = try store.fetchItems().filter { !$0.isCompleted }
    100         #expect(items.map(\.title) == ["First", "Second", "Third"])
    101     }
    102 
    103     @Test("Normalize on empty store does nothing")
    104     func normalizeEmpty() async throws {
    105         let store = makeTestStore()
    106 
    107         try store.normalizeSortOrders()
    108 
    109         let items = try store.fetchItems()
    110         #expect(items.isEmpty)
    111     }
    112 
    113     @Test("Normalize ignores completed items")
    114     func normalizeIgnoresCompleted() async throws {
    115         let (store, ids) = try makeTestStoreWithItems(count: 4)
    116         try store.complete(itemID: ids[1])
    117         try store.complete(itemID: ids[3])
    118 
    119         try store.normalizeSortOrders()
    120 
    121         let active = try store.fetchItems().filter { !$0.isCompleted }
    122         #expect(active.count == 2)
    123         #expect(active[0].sortOrder == 0)
    124         #expect(active[1].sortOrder == 1000)
    125 
    126         // Completed items should still exist.
    127         let all = try store.fetchItems()
    128         #expect(all.count == 4)
    129     }
    130 
    131     @Test("Normalize then create at beginning uses correct offset")
    132     func normalizeFollowedByPrepend() async throws {
    133         let (store, _) = try makeTestStoreWithItems(count: 3)
    134         try store.normalizeSortOrders()
    135 
    136         // After normalize: sort orders are 0, 1000, 2000.
    137         // Creating at beginning should use minSortOrder - 1000 = -1000.
    138         _ = try store.createItem(title: "Prepended", atBeginning: true)
    139         try store.save()
    140 
    141         let items = try store.fetchItems().filter { !$0.isCompleted }
    142         #expect(items.first?.title == "Prepended")
    143         #expect(items.first?.sortOrder == -1000)
    144     }
    145 
    146     @Test("Normalize with single item sets sort order to zero")
    147     func normalizeSingleItem() async throws {
    148         let store = makeTestStore()
    149         _ = try store.createItem(title: "Only", atBeginning: true)
    150         try store.save()
    151 
    152         try store.normalizeSortOrders()
    153 
    154         let items = try store.fetchItems().filter { !$0.isCompleted }
    155         #expect(items.count == 1)
    156         #expect(items[0].sortOrder == 0)
    157     }
    158 
    159     @Test("Double normalize is idempotent")
    160     func doubleNormalize() async throws {
    161         let (store, _) = try makeTestStoreWithItems(count: 4)
    162         try store.moveItem(itemID: try store.fetchItems()[3].id, toIndex: 0)
    163 
    164         try store.normalizeSortOrders()
    165         let afterFirst = try store.fetchItems().filter { !$0.isCompleted }.map(\.sortOrder)
    166 
    167         try store.normalizeSortOrders()
    168         let afterSecond = try store.fetchItems().filter { !$0.isCompleted }.map(\.sortOrder)
    169 
    170         #expect(afterFirst == afterSecond)
    171     }
    172 
    173     @Test("Normalize fixes negative sort orders from prepend operations")
    174     func normalizeFixesNegativeOrders() async throws {
    175         let store = makeTestStore()
    176         _ = try store.createItem(title: "Original")
    177         try store.save()
    178         _ = try store.createItem(title: "Prepended", atBeginning: true)
    179         try store.save()
    180 
    181         let beforeNormalize = try store.fetchItems().filter { !$0.isCompleted }
    182         #expect(beforeNormalize[0].sortOrder < 0)
    183 
    184         try store.normalizeSortOrders()
    185 
    186         let afterNormalize = try store.fetchItems().filter { !$0.isCompleted }
    187         #expect(afterNormalize[0].sortOrder == 0)
    188         #expect(afterNormalize[1].sortOrder == 1000)
    189         #expect(afterNormalize[0].title == "Prepended")
    190     }
    191 }