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 }