Nudge-Screen

This commit is contained in:
2026-04-20 09:41:18 +02:00
parent a48e857ada
commit 187c3e4fc6
26 changed files with 2542 additions and 229 deletions
+108
View File
@@ -0,0 +1,108 @@
import Testing
@testable import bookstax
// MARK: - Recent Searches
@Suite("SearchViewModel Recent Searches", .serialized)
struct SearchViewModelRecentTests {
private let recentKey = "recentSearches"
init() {
// Start each test with a clean slate
UserDefaults.standard.removeObject(forKey: recentKey)
}
// MARK: addToRecent
@Test("Adding a query inserts it at position 0")
func addInsertsAtFront() {
let vm = SearchViewModel()
vm.addToRecent("swift")
vm.addToRecent("swiftui")
let recent = vm.recentSearches
#expect(recent.first == "swiftui")
#expect(recent[1] == "swift")
}
@Test("Adding duplicate moves it to front without creating duplicates")
func addDeduplicates() {
let vm = SearchViewModel()
vm.addToRecent("bookstack")
vm.addToRecent("wiki")
vm.addToRecent("bookstack") // duplicate
let recent = vm.recentSearches
#expect(recent.first == "bookstack")
#expect(recent.count == 2)
#expect(!recent.dropFirst().contains("bookstack"))
}
@Test("List is capped at 10 entries")
func cappedAtTen() {
let vm = SearchViewModel()
for i in 1...12 {
vm.addToRecent("query\(i)")
}
#expect(vm.recentSearches.count == 10)
}
@Test("Oldest entries are dropped when cap is exceeded")
func oldestDropped() {
let vm = SearchViewModel()
for i in 1...11 {
vm.addToRecent("query\(i)")
}
let recent = vm.recentSearches
// query1 was added first, so it falls off after 11 adds
#expect(!recent.contains("query1"))
#expect(recent.contains("query11"))
}
// MARK: clearRecentSearches
@Test("clearRecentSearches empties the list")
func clearResetsToEmpty() {
let vm = SearchViewModel()
vm.addToRecent("something")
vm.clearRecentSearches()
#expect(vm.recentSearches.isEmpty)
}
// MARK: Persistence
@Test("Recent searches persist across ViewModel instances")
func persistsAcrossInstances() {
let vm1 = SearchViewModel()
vm1.addToRecent("persistent")
let vm2 = SearchViewModel()
#expect(vm2.recentSearches.contains("persistent"))
}
}
// MARK: - Query Minimum Length
@Suite("SearchViewModel Query Logic")
struct SearchViewModelQueryTests {
@Test("Short query clears results without triggering search")
func shortQueryClearsResults() {
let vm = SearchViewModel()
vm.results = [SearchResultDTO(id: 1, name: "dummy", slug: "dummy",
type: .page, url: "", preview: nil)]
vm.query = "x" // only 1 character
vm.onQueryChanged()
#expect(vm.results.isEmpty)
}
@Test("Query with 2+ chars does not clear results immediately")
func sufficientQueryKeepsResults() {
let vm = SearchViewModel()
vm.query = "sw" // 2 characters triggers debounce but does not clear
vm.onQueryChanged()
// Results not cleared by onQueryChanged when query is long enough
// (actual search would require API; here we just verify results aren't wiped)
// Results were empty to start with, so we just confirm no crash
#expect(vm.results.isEmpty) // no API call, so still empty
}
}