Tests/Unit/FocusStateDataTests.swift (19022B)
1import Foundation
2import Testing
3
4#if os(macOS)
5@testable import Listless
6#else
7@testable import Listless_iOS
8#endif
9
10@Suite("FocusStateData Selection", .serialized)
11@MainActor
12struct FocusStateDataSelectionTests {
13
14
15 static let ids = (0..<5).map { _ in UUID() }
16 static var displayOrder: [UUID] { ids }
17
18
19
20 @Test("selectedItemID setter establishes single selection")
21 func selectedItemIDSetter() {
22 var state = FocusStateData()
23 let id = Self.ids[2]
24
25 state.selectedItemID = id
26
27 #expect(state.selectedItemID == id)
28 #expect(state.cursorItemID == id)
29 #expect(state.anchorItemID == id)
30 #expect(state.selectedItemIDs == [id])
31 #expect(state.inactiveSelections.isEmpty)
32 }
33
34 @Test("selectedItemID setter clears selection when set to nil")
35 func selectedItemIDSetterNil() {
36 var state = FocusStateData()
37 state.selectedItemID = Self.ids[0]
38
39 state.selectedItemID = nil
40
41 #expect(state.selectedItemID == nil)
42 #expect(state.cursorItemID == nil)
43 #expect(state.anchorItemID == nil)
44 #expect(state.selectedItemIDs.isEmpty)
45 #expect(state.inactiveSelections.isEmpty)
46 }
47
48
49
50 @Test("isItemSelected returns true only for selected items")
51 func isItemSelected() {
52 var state = FocusStateData()
53 state.selectedItemID = Self.ids[1]
54
55 #expect(state.isItemSelected(Self.ids[1]))
56 #expect(!state.isItemSelected(Self.ids[0]))
57 }
58
59 @Test("hasMultipleSelection is false for single selection")
60 func hasMultipleSelectionSingle() {
61 var state = FocusStateData()
62 state.selectedItemID = Self.ids[0]
63
64 #expect(!state.hasMultipleSelection)
65 }
66
67
68
69 @Test("selectAll selects every item in display order")
70 func selectAllBasic() {
71 var state = FocusStateData()
72 let order = Self.displayOrder
73
74 state.selectAll(displayOrder: order)
75
76 #expect(state.selectedItemIDs == Set(order))
77 #expect(state.anchorItemID == order.first)
78 #expect(state.cursorItemID == order.last)
79 #expect(state.inactiveSelections.isEmpty)
80 #expect(state.hasMultipleSelection)
81 }
82
83 @Test("selectAll with empty display order does nothing")
84 func selectAllEmpty() {
85 var state = FocusStateData()
86 state.selectedItemID = Self.ids[0]
87
88 state.selectAll(displayOrder: [])
89
90 #expect(state.selectedItemID == Self.ids[0])
91 }
92
93 @Test("selectAll with single item sets anchor and cursor to same item")
94 func selectAllSingleItem() {
95 var state = FocusStateData()
96 let single = [Self.ids[0]]
97
98 state.selectAll(displayOrder: single)
99
100 #expect(state.anchorItemID == Self.ids[0])
101 #expect(state.cursorItemID == Self.ids[0])
102 #expect(state.selectedItemIDs == Set(single))
103 #expect(!state.hasMultipleSelection)
104 }
105
106 @Test("selectAll clears inactive selections from prior Cmd+Click")
107 func selectAllClearsInactive() {
108 var state = FocusStateData()
109 let order = Self.displayOrder
110
111 state.toggleSelection(itemID: order[0], displayOrder: order)
112 state.toggleSelection(itemID: order[3], displayOrder: order)
113
114 state.selectAll(displayOrder: order)
115
116 #expect(state.inactiveSelections.isEmpty)
117 #expect(state.selectedItemIDs == Set(order))
118 }
119
120
121
122 @Test("Toggle adds an unselected item to the selection")
123 func toggleAddsItem() {
124 var state = FocusStateData()
125 let order = Self.displayOrder
126
127 state.toggleSelection(itemID: order[1], displayOrder: order)
128
129 #expect(state.isItemSelected(order[1]))
130 #expect(state.selectedItemIDs.count == 1)
131 }
132
133 @Test("Toggle removes a selected item from the selection")
134 func toggleRemovesItem() {
135 var state = FocusStateData()
136 let order = Self.displayOrder
137 state.toggleSelection(itemID: order[1], displayOrder: order)
138
139 state.toggleSelection(itemID: order[1], displayOrder: order)
140
141 #expect(!state.isItemSelected(order[1]))
142 #expect(state.selectedItemIDs.isEmpty)
143 }
144
145 @Test("Toggle sets anchor to item below toggled item")
146 func toggleAnchorBelowToggledItem() {
147 var state = FocusStateData()
148 let order = Self.displayOrder
149
150 state.toggleSelection(itemID: order[1], displayOrder: order)
151
152
153 #expect(state.anchorItemID == order[2])
154 }
155
156 @Test("Toggle at bottom of list sets anchor to self")
157 func toggleAnchorAtBottom() {
158 var state = FocusStateData()
159 let order = Self.displayOrder
160
161 state.toggleSelection(itemID: order[4], displayOrder: order)
162
163 #expect(state.anchorItemID == order[4])
164 }
165
166 @Test("Adding via toggle resets cursor to anchor")
167 func toggleAddResetsCursor() {
168 var state = FocusStateData()
169 let order = Self.displayOrder
170 state.toggleSelection(itemID: order[0], displayOrder: order)
171
172 state.toggleSelection(itemID: order[3], displayOrder: order)
173
174
175 #expect(state.cursorItemID == order[4])
176 }
177
178 @Test("Deselecting via toggle keeps cursor at its previous position")
179 func toggleDeselectKeepsCursor() {
180 var state = FocusStateData()
181 let order = Self.displayOrder
182
183 state.toggleSelection(itemID: order[1], displayOrder: order)
184 state.toggleSelection(itemID: order[3], displayOrder: order)
185 let cursorBefore = state.cursorItemID
186
187
188 state.toggleSelection(itemID: order[1], displayOrder: order)
189
190 #expect(state.cursorItemID == cursorBefore)
191 }
192
193 @Test("Deselecting the last selected item clears all state")
194 func toggleDeselectLast() {
195 var state = FocusStateData()
196 let order = Self.displayOrder
197 state.toggleSelection(itemID: order[2], displayOrder: order)
198
199 state.toggleSelection(itemID: order[2], displayOrder: order)
200
201 #expect(state.selectedItemIDs.isEmpty)
202 #expect(state.anchorItemID == nil)
203 #expect(state.cursorItemID == nil)
204 #expect(state.inactiveSelections.isEmpty)
205 }
206
207 @Test("Toggle with ID not in display order is ignored")
208 func toggleUnknownID() {
209 var state = FocusStateData()
210 let unknown = UUID()
211
212 state.toggleSelection(itemID: unknown, displayOrder: Self.displayOrder)
213
214 #expect(state.selectedItemIDs.isEmpty)
215 }
216
217 @Test("Multiple toggles build discontinuous selection")
218 func toggleMultipleDiscontinuous() {
219 var state = FocusStateData()
220 let order = Self.displayOrder
221
222 state.toggleSelection(itemID: order[0], displayOrder: order)
223 state.toggleSelection(itemID: order[2], displayOrder: order)
224 state.toggleSelection(itemID: order[4], displayOrder: order)
225
226 #expect(state.isItemSelected(order[0]))
227 #expect(state.isItemSelected(order[2]))
228 #expect(state.isItemSelected(order[4]))
229 #expect(!state.isItemSelected(order[1]))
230 #expect(!state.isItemSelected(order[3]))
231 #expect(state.hasMultipleSelection)
232 }
233
234
235
236 @Test("Extend selection downward from anchor")
237 func extendDown() {
238 var state = FocusStateData()
239 let order = Self.displayOrder
240 state.selectedItemID = order[1]
241
242 state.extendSelection(to: order[3], displayOrder: order)
243
244 #expect(state.selectedItemIDs == Set(order[1...3]))
245 #expect(state.cursorItemID == order[3])
246 #expect(state.anchorItemID == order[1])
247 }
248
249 @Test("Extend selection upward from anchor")
250 func extendUp() {
251 var state = FocusStateData()
252 let order = Self.displayOrder
253 state.selectedItemID = order[3]
254
255 state.extendSelection(to: order[1], displayOrder: order)
256
257 #expect(state.selectedItemIDs == Set(order[1...3]))
258 #expect(state.cursorItemID == order[1])
259 #expect(state.anchorItemID == order[3])
260 }
261
262 @Test("Extend selection contracts when cursor moves back toward anchor")
263 func extendContract() {
264 var state = FocusStateData()
265 let order = Self.displayOrder
266 state.selectedItemID = order[1]
267
268 state.extendSelection(to: order[4], displayOrder: order)
269 state.extendSelection(to: order[2], displayOrder: order)
270
271 #expect(state.selectedItemIDs == Set(order[1...2]))
272 #expect(state.cursorItemID == order[2])
273 }
274
275 @Test("Extend to same position as anchor selects only anchor")
276 func extendToAnchor() {
277 var state = FocusStateData()
278 let order = Self.displayOrder
279 state.selectedItemID = order[2]
280
281 state.extendSelection(to: order[2], displayOrder: order)
282
283 #expect(state.selectedItemIDs == [order[2]])
284 #expect(state.cursorItemID == order[2])
285 }
286
287 @Test("Extend without anchor is ignored")
288 func extendWithoutAnchor() {
289 var state = FocusStateData()
290
291 state.extendSelection(to: Self.ids[2], displayOrder: Self.displayOrder)
292
293 #expect(state.selectedItemIDs.isEmpty)
294 }
295
296 @Test("Extend with unknown target ID is ignored")
297 func extendUnknownTarget() {
298 var state = FocusStateData()
299 state.selectedItemID = Self.ids[0]
300
301 state.extendSelection(to: UUID(), displayOrder: Self.displayOrder)
302
303 #expect(state.selectedItemIDs == [Self.ids[0]])
304 }
305
306
307
308 @Test("Shift+Arrow after Cmd+Click preserves inactive selections")
309 func extendPreservesInactive() {
310 var state = FocusStateData()
311 let order = Self.displayOrder
312
313
314 state.toggleSelection(itemID: order[0], displayOrder: order)
315 state.toggleSelection(itemID: order[4], displayOrder: order)
316
317
318
319 state.extendSelection(to: order[4], displayOrder: order)
320
321 #expect(state.isItemSelected(order[0]))
322 #expect(state.isItemSelected(order[4]))
323 }
324
325 @Test("Adjacent inactive selections merge into active range")
326 func mergeAdjacentInactive() {
327 var state = FocusStateData()
328 let order = Self.displayOrder
329
330
331 state.toggleSelection(itemID: order[0], displayOrder: order)
332 state.toggleSelection(itemID: order[2], displayOrder: order)
333
334
335
336
337
338
339
340
341 state.extendSelection(to: order[1], displayOrder: order)
342
343 #expect(state.isItemSelected(order[0]))
344 #expect(state.isItemSelected(order[1]))
345 #expect(state.isItemSelected(order[2]))
346 #expect(state.isItemSelected(order[3]))
347 #expect(state.inactiveSelections.isEmpty)
348 }
349
350 @Test("Non-adjacent inactive selections stay inactive")
351 func nonAdjacentStaysInactive() {
352 var state = FocusStateData()
353 let order = Self.displayOrder
354
355
356 state.toggleSelection(itemID: order[0], displayOrder: order)
357 state.toggleSelection(itemID: order[4], displayOrder: order)
358
359
360
361
362
363
364
365
366 state.extendSelection(to: order[3], displayOrder: order)
367
368 #expect(state.isItemSelected(order[0]))
369 #expect(state.isItemSelected(order[3]))
370 #expect(state.isItemSelected(order[4]))
371 #expect(!state.isItemSelected(order[1]))
372 #expect(!state.isItemSelected(order[2]))
373 #expect(state.inactiveSelections.contains(order[0]))
374 }
375
376
377
378 @Test("Prune then extend after item is removed from display order")
379 func pruneAndExtendAfterItemRemoved() {
380 var state = FocusStateData()
381 let order = Self.displayOrder
382
383
384 state.toggleSelection(itemID: order[1], displayOrder: order)
385 state.toggleSelection(itemID: order[3], displayOrder: order)
386
387
388
389 let reducedOrder = [order[0], order[1], order[2], order[4]]
390 state.pruneDeletedItems(displayOrder: reducedOrder)
391
392
393 #expect(!state.isItemSelected(order[3]))
394 #expect(state.isItemSelected(order[1]))
395 #expect(state.anchorItemID == order[4])
396
397
398 state.extendSelection(to: order[2], displayOrder: reducedOrder)
399
400
401 #expect(state.isItemSelected(order[1]))
402 #expect(state.isItemSelected(order[2]))
403 #expect(state.isItemSelected(order[4]))
404 #expect(!state.isItemSelected(order[3]))
405 #expect(state.inactiveSelections.isEmpty)
406 }
407
408 @Test("Prune then extend when anchor was the deleted item")
409 func pruneRemovedAnchorThenExtend() {
410 var state = FocusStateData()
411 let order = Self.displayOrder
412 state.selectedItemID = order[2]
413
414
415 let reducedOrder = [order[0], order[1], order[3], order[4]]
416 state.pruneDeletedItems(displayOrder: reducedOrder)
417
418
419 #expect(state.selectedItemIDs.isEmpty)
420 #expect(state.anchorItemID == nil)
421 #expect(state.cursorItemID == nil)
422 }
423
424 @Test("Toggle after display order is reordered uses new positions")
425 func toggleWithReorderedDisplay() {
426 var state = FocusStateData()
427 let order = Self.displayOrder
428
429
430 let reversed = Array(order.reversed())
431
432
433 state.toggleSelection(itemID: order[4], displayOrder: reversed)
434
435
436 #expect(state.anchorItemID == order[3])
437 #expect(state.isItemSelected(order[4]))
438 }
439
440 @Test("Prune after selectAll with deleted items cleans up selection")
441 func pruneAfterSelectAll() {
442 var state = FocusStateData()
443 let order = Self.displayOrder
444 state.selectAll(displayOrder: order)
445
446
447 let shrunken = [order[1], order[3], order[4]]
448 state.pruneDeletedItems(displayOrder: shrunken)
449
450 #expect(state.selectedItemIDs == Set([order[1], order[3], order[4]]))
451
452 #expect(state.anchorItemID == nil)
453 #expect(state.cursorItemID == order[4])
454 }
455
456
457
458 @Test("Prune with no deletions changes nothing")
459 func pruneNoOp() {
460 var state = FocusStateData()
461 let order = Self.displayOrder
462 state.toggleSelection(itemID: order[1], displayOrder: order)
463 state.toggleSelection(itemID: order[3], displayOrder: order)
464 let selectedBefore = state.selectedItemIDs
465 let inactiveBefore = state.inactiveSelections
466
467 state.pruneDeletedItems(displayOrder: order)
468
469 #expect(state.selectedItemIDs == selectedBefore)
470 #expect(state.inactiveSelections == inactiveBefore)
471 }
472
473 @Test("Prune removes ghost IDs from selectedItemIDs and inactiveSelections")
474 func pruneRemovesGhosts() {
475 var state = FocusStateData()
476 let order = Self.displayOrder
477
478 state.toggleSelection(itemID: order[0], displayOrder: order)
479 state.toggleSelection(itemID: order[2], displayOrder: order)
480
481
482 let reducedOrder = [order[1], order[2], order[3], order[4]]
483 state.pruneDeletedItems(displayOrder: reducedOrder)
484
485 #expect(!state.isItemSelected(order[0]))
486 #expect(!state.inactiveSelections.contains(order[0]))
487 #expect(state.isItemSelected(order[2]))
488 }
489
490 @Test("Prune with all items deleted clears everything")
491 func pruneAllDeleted() {
492 var state = FocusStateData()
493 let order = Self.displayOrder
494 state.selectAll(displayOrder: order)
495
496 state.pruneDeletedItems(displayOrder: [])
497
498 #expect(state.selectedItemIDs.isEmpty)
499 #expect(state.anchorItemID == nil)
500 #expect(state.cursorItemID == nil)
501 #expect(state.inactiveSelections.isEmpty)
502 }
503
504 @Test("Prune falls back cursor to anchor when cursor is deleted")
505 func pruneCursorFallsBackToAnchor() {
506 var state = FocusStateData()
507 let order = Self.displayOrder
508 state.selectedItemID = order[1]
509 state.extendSelection(to: order[3], displayOrder: order)
510
511
512
513 let reducedOrder = [order[0], order[1], order[2], order[4]]
514 state.pruneDeletedItems(displayOrder: reducedOrder)
515
516 #expect(state.cursorItemID == order[1])
517 #expect(state.anchorItemID == order[1])
518 }
519
520
521
522 @Test("Setting selectedItemID collapses multi-select to single")
523 func setterResetsMultiSelect() {
524 var state = FocusStateData()
525 let order = Self.displayOrder
526 state.selectAll(displayOrder: order)
527
528 state.selectedItemID = order[2]
529
530 #expect(state.selectedItemIDs == [order[2]])
531 #expect(state.anchorItemID == order[2])
532 #expect(state.cursorItemID == order[2])
533 #expect(state.inactiveSelections.isEmpty)
534 #expect(!state.hasMultipleSelection)
535 }
536}