1import AppKit2import Foundation3import Testing45@testable import Haven67@MainActor8final class SearchStub: WallhavenSearching {9 struct Request {10 let query: String11 let categories: String12 let purity: String13 let resolutions: [String]14 let ratios: [String]15 let page: Int16 }1718 var results: [Result<([Wallpaper], Meta?), Error>] = []19 var requests: [Request] = []2021 func searchWallpapers(22 query: String, categories: String, purity: String, sorting: WallhavenSorting,23 resolutions: [String], ratios: [String], limit: Int, page: Int24 ) async throws -> ([Wallpaper], Meta?) {25 requests.append(26 Request(27 query: query, categories: categories, purity: purity, resolutions: resolutions,28 ratios: ratios, page: page))29 return try results.removeFirst().get()30 }31}3233@MainActor34final class WallpaperStub: WallpaperManaging {35 var error: Error?36 var downloadedURL = URL(fileURLWithPath: "/tmp/downloaded.jpg")37 var downloads: [URL] = []38 var desktopURLs: [URL] = []39 var saves: [(URL, URL)] = []4041 func download(from url: URL) async throws -> URL {42 if let error { throw error }43 downloads.append(url)44 return downloadedURL45 }4647 func setDesktopImage(from fileURL: URL, screen: NSScreen?) throws {48 if let error { throw error }49 desktopURLs.append(fileURL)50 }5152 func saveToFolder(from sourceURL: URL, to folderURL: URL) throws {53 if let error { throw error }54 saves.append((sourceURL, folderURL))55 }56}5758@MainActor59final class AutostartStub: AutostartManaging {60 var isEnabled = false61}6263@Suite("Explore feed configuration")64struct FeedModeTests {65 @Test(66 "Feed modes map to API sorting",67 arguments: [68 (FeedMode.latest, WallhavenSorting.dateAdded),69 (FeedMode.toplist, WallhavenSorting.toplist),70 (FeedMode.random, WallhavenSorting.random),71 ]72 )73 func sortingMapping(mode: FeedMode, sorting: WallhavenSorting) {74 #expect(mode.sorting.rawValue == sorting.rawValue)75 }7677 @Test("Every feed mode has non-empty display metadata", arguments: FeedMode.allCases)78 func displayMetadata(mode: FeedMode) {79 #expect(!mode.displayName.isEmpty)80 #expect(!mode.systemImage.isEmpty)81 #expect(mode.id == mode.rawValue)82 }8384 @Test("Resolution and ratio defaults retain the Any option")85 @MainActor86 func filterOptions() {87 let state = AppState()8889 #expect(state.availableResolutions.first == "Any")90 #expect(state.availableRatios.first == "Any")91 #expect(state.availableResolutions.contains("3840x2160"))92 #expect(state.availableRatios.contains("21x9"))93 }94}9596@Suite("Application state behavior", .serialized)97@MainActor98struct ApplicationStateTests {99 private func makeState(100 search: SearchStub? = nil, wallpaper: WallpaperStub? = nil,101 autostart: AutostartStub? = nil,102 chooseFolder: @escaping @MainActor (URL, @escaping (URL?) -> Void) -> Void = { _, completion in103 completion(nil)104 },105 terminate: @escaping @MainActor () -> Void = {}106 ) -> AppState {107 let search = search ?? SearchStub()108 let wallpaper = wallpaper ?? WallpaperStub()109 let autostart = autostart ?? AutostartStub()110 return AppState(111 api: search, wallpaperManager: wallpaper, autostartManager: autostart,112 chooseFolder: chooseFolder, terminateApplication: terminate, observeChanges: false)113 }114115 @Test("Reset and pagination apply filters, metadata, and append behavior")116 func fetchAndPaginate() async {117 let search = SearchStub()118 let first = makeWallpaper(id: "first")119 let second = makeWallpaper(id: "second")120 search.results = [121 .success(([first], Meta(current_page: 1, last_page: 2, per_page: 24, total: 2))),122 .success(([second], Meta(current_page: 2, last_page: 2, per_page: 24, total: 2))),123 ]124 let state = makeState(search: search)125 state.searchText = "forest"126 state.selectedResolution = "3840x2160"127 state.selectedRatio = "21x9"128 state.categories = (true, false, true)129 state.purity = (false, true)130131 await state.fetchWallpapers()132 #expect(state.wallpapers == [first])133 #expect(state.canLoadMore)134 #expect(!state.isLoading)135 #expect(search.requests[0].query == "forest")136 #expect(search.requests[0].categories == "101")137 #expect(search.requests[0].purity == "010")138 #expect(search.requests[0].resolutions == ["3840x2160"])139 #expect(search.requests[0].ratios == ["21x9"])140141 await state.loadMore()142 #expect(state.wallpapers == [first, second])143 #expect(!state.canLoadMore)144 #expect(search.requests[1].page == 2)145146 await state.loadMore()147 #expect(search.requests.count == 2)148 }149150 @Test("Metadata-free responses and failures update fallback pagination state")151 func fallbackPaginationAndFailures() async {152 let search = SearchStub()153 search.results = [154 .success(([makeWallpaper(id: "one")], nil)),155 .success(([], nil)),156 .failure(URLError(.notConnectedToInternet)),157 .failure(URLError(.timedOut)),158 ]159 let state = makeState(search: search)160 state.selectedResolution = "Any"161 state.selectedRatio = "Any"162163 await state.fetchWallpapers()164 #expect(state.canLoadMore)165 await state.loadMore()166 #expect(!state.canLoadMore)167 #expect(search.requests[1].page == 2)168169 await state.fetchWallpapers()170 #expect(state.errorMessage != nil)171 state.canLoadMore = true172 await state.loadMore()173 #expect(!state.isLoading)174 }175176 @Test("Remote wallpaper operations cover saving and failures")177 func remoteWallpaperOperations() async {178 let wallpaperManager = WallpaperStub()179 let state = makeState(wallpaper: wallpaperManager)180 let wallpaper = makeWallpaper()181 state.saveFolderPath = "/tmp/haven-tests"182183 state.saveToFolderEnabled = false184 await state.setWallpaper(wallpaper)185 #expect(wallpaperManager.desktopURLs == [wallpaperManager.downloadedURL])186 #expect(wallpaperManager.saves.isEmpty)187188 state.saveToFolderEnabled = true189 await state.setWallpaper(wallpaper)190 #expect(wallpaperManager.saves.count == 1)191192 await state.downloadWallpaper(wallpaper)193 #expect(wallpaperManager.saves.count == 2)194195 wallpaperManager.error = URLError(.cannotWriteToFile)196 await state.setWallpaper(wallpaper)197 #expect(state.errorMessage?.hasPrefix("Failed to set wallpaper") == true)198 await state.downloadWallpaper(wallpaper)199 #expect(state.errorMessage?.hasPrefix("Failed to download") == true)200 }201202 @Test("Local wallpaper, autostart, folder picker, and quit are injectable")203 func localAndPlatformActions() async {204 let wallpaperManager = WallpaperStub()205 let autostart = AutostartStub()206 var terminated = false207 let selected = URL(fileURLWithPath: "/tmp/selected")208 let state = makeState(209 wallpaper: wallpaperManager, autostart: autostart,210 chooseFolder: { _, completion in completion(selected) },211 terminate: { terminated = true })212 let local = LocalWallpaper(213 id: "local", url: URL(fileURLWithPath: "/tmp/local.jpg"), filename: "local.jpg",214 resolution: "1x1", fileSize: 10, createdAt: Date(), modifiedAt: Date())215216 await state.setLocalWallpaper(local)217 #expect(wallpaperManager.desktopURLs == [local.url])218 #expect(state.historyManager.history.first?.purity == "local")219220 wallpaperManager.error = URLError(.cannotOpenFile)221 await state.setLocalWallpaper(local)222 #expect(state.errorMessage?.hasPrefix("Failed to set wallpaper") == true)223224 state.launchAtLogin = true225 #expect(autostart.isEnabled)226 state.selectSaveFolder()227 #expect(state.saveFolderPath == selected.path)228 state.quit()229 #expect(terminated)230231 var offeredDirectory: URL?232 let cancelState = makeState(chooseFolder: { directory, completion in233 offeredDirectory = directory234 completion(nil)235 })236 let oldPath = cancelState.saveFolderPath237 cancelState.selectSaveFolder()238 #expect(offeredDirectory?.path == oldPath)239 #expect(cancelState.saveFolderPath == oldPath)240 }241242 @Test("Observed search and settings changes trigger their pipelines")243 func observedChanges() async {244 let search = SearchStub()245 search.results = Array(repeating: .success(([], nil)), count: 50)246 let state = AppState(247 api: search, chooseFolder: { _, completion in completion(nil) },248 terminateApplication: {}, observeChanges: true)249250 func waitForRequests(_ expected: Int) async {251 for _ in 0..<100 where search.requests.count < expected {252 try? await Task.sleep(nanoseconds: 20_000_000)253 }254 }255256 state.scheduleSearch()257 await waitForRequests(1)258259 state.feedMode = .random260 await waitForRequests(2)261262 state.searchText = "updated"263 await waitForRequests(3)264265 state.categories = (false, true, true)266 await waitForRequests(4)267268 state.purity = (true, true)269 await waitForRequests(5)270271 state.saveToFolderEnabled.toggle()272 state.saveFolderPath = FileManager.default.temporaryDirectory.path273274 #expect(search.requests.count >= 5)275 }276}