import AppKit import Foundation import Testing @testable import Haven @MainActor final class SearchStub: WallhavenSearching { struct Request { let query: String let categories: String let purity: String let resolutions: [String] let ratios: [String] let page: Int } var results: [Result<([Wallpaper], Meta?), Error>] = [] var requests: [Request] = [] func searchWallpapers( query: String, categories: String, purity: String, sorting: WallhavenSorting, resolutions: [String], ratios: [String], limit: Int, page: Int ) async throws -> ([Wallpaper], Meta?) { requests.append( Request( query: query, categories: categories, purity: purity, resolutions: resolutions, ratios: ratios, page: page)) return try results.removeFirst().get() } } @MainActor final class WallpaperStub: WallpaperManaging { var error: Error? var downloadedURL = URL(fileURLWithPath: "/tmp/downloaded.jpg") var downloads: [URL] = [] var desktopURLs: [URL] = [] var saves: [(URL, URL)] = [] func download(from url: URL) async throws -> URL { if let error { throw error } downloads.append(url) return downloadedURL } func setDesktopImage(from fileURL: URL, screen: NSScreen?) throws { if let error { throw error } desktopURLs.append(fileURL) } func saveToFolder(from sourceURL: URL, to folderURL: URL) throws { if let error { throw error } saves.append((sourceURL, folderURL)) } } @MainActor final class AutostartStub: AutostartManaging { var isEnabled = false } @Suite("Explore feed configuration") struct FeedModeTests { @Test( "Feed modes map to API sorting", arguments: [ (FeedMode.latest, WallhavenSorting.dateAdded), (FeedMode.toplist, WallhavenSorting.toplist), (FeedMode.random, WallhavenSorting.random), ] ) func sortingMapping(mode: FeedMode, sorting: WallhavenSorting) { #expect(mode.sorting.rawValue == sorting.rawValue) } @Test("Every feed mode has non-empty display metadata", arguments: FeedMode.allCases) func displayMetadata(mode: FeedMode) { #expect(!mode.displayName.isEmpty) #expect(!mode.systemImage.isEmpty) #expect(mode.id == mode.rawValue) } @Test("Resolution and ratio defaults retain the Any option") @MainActor func filterOptions() { let state = AppState() #expect(state.availableResolutions.first == "Any") #expect(state.availableRatios.first == "Any") #expect(state.availableResolutions.contains("3840x2160")) #expect(state.availableRatios.contains("21x9")) } } @Suite("Application state behavior", .serialized) @MainActor struct ApplicationStateTests { private func makeState( search: SearchStub? = nil, wallpaper: WallpaperStub? = nil, autostart: AutostartStub? = nil, chooseFolder: @escaping @MainActor (URL, @escaping (URL?) -> Void) -> Void = { _, completion in completion(nil) }, terminate: @escaping @MainActor () -> Void = {} ) -> AppState { let search = search ?? SearchStub() let wallpaper = wallpaper ?? WallpaperStub() let autostart = autostart ?? AutostartStub() return AppState( api: search, wallpaperManager: wallpaper, autostartManager: autostart, chooseFolder: chooseFolder, terminateApplication: terminate, observeChanges: false) } @Test("Reset and pagination apply filters, metadata, and append behavior") func fetchAndPaginate() async { let search = SearchStub() let first = makeWallpaper(id: "first") let second = makeWallpaper(id: "second") search.results = [ .success(([first], Meta(current_page: 1, last_page: 2, per_page: 24, total: 2))), .success(([second], Meta(current_page: 2, last_page: 2, per_page: 24, total: 2))), ] let state = makeState(search: search) state.searchText = "forest" state.selectedResolution = "3840x2160" state.selectedRatio = "21x9" state.categories = (true, false, true) state.purity = (false, true) await state.fetchWallpapers() #expect(state.wallpapers == [first]) #expect(state.canLoadMore) #expect(!state.isLoading) #expect(search.requests[0].query == "forest") #expect(search.requests[0].categories == "101") #expect(search.requests[0].purity == "010") #expect(search.requests[0].resolutions == ["3840x2160"]) #expect(search.requests[0].ratios == ["21x9"]) await state.loadMore() #expect(state.wallpapers == [first, second]) #expect(!state.canLoadMore) #expect(search.requests[1].page == 2) await state.loadMore() #expect(search.requests.count == 2) } @Test("Metadata-free responses and failures update fallback pagination state") func fallbackPaginationAndFailures() async { let search = SearchStub() search.results = [ .success(([makeWallpaper(id: "one")], nil)), .success(([], nil)), .failure(URLError(.notConnectedToInternet)), .failure(URLError(.timedOut)), ] let state = makeState(search: search) state.selectedResolution = "Any" state.selectedRatio = "Any" await state.fetchWallpapers() #expect(state.canLoadMore) await state.loadMore() #expect(!state.canLoadMore) #expect(search.requests[1].page == 2) await state.fetchWallpapers() #expect(state.errorMessage != nil) state.canLoadMore = true await state.loadMore() #expect(!state.isLoading) } @Test("Remote wallpaper operations cover saving and failures") func remoteWallpaperOperations() async { let wallpaperManager = WallpaperStub() let state = makeState(wallpaper: wallpaperManager) let wallpaper = makeWallpaper() state.saveFolderPath = "/tmp/haven-tests" state.saveToFolderEnabled = false await state.setWallpaper(wallpaper) #expect(wallpaperManager.desktopURLs == [wallpaperManager.downloadedURL]) #expect(wallpaperManager.saves.isEmpty) state.saveToFolderEnabled = true await state.setWallpaper(wallpaper) #expect(wallpaperManager.saves.count == 1) await state.downloadWallpaper(wallpaper) #expect(wallpaperManager.saves.count == 2) wallpaperManager.error = URLError(.cannotWriteToFile) await state.setWallpaper(wallpaper) #expect(state.errorMessage?.hasPrefix("Failed to set wallpaper") == true) await state.downloadWallpaper(wallpaper) #expect(state.errorMessage?.hasPrefix("Failed to download") == true) } @Test("Local wallpaper, autostart, folder picker, and quit are injectable") func localAndPlatformActions() async { let wallpaperManager = WallpaperStub() let autostart = AutostartStub() var terminated = false let selected = URL(fileURLWithPath: "/tmp/selected") let state = makeState( wallpaper: wallpaperManager, autostart: autostart, chooseFolder: { _, completion in completion(selected) }, terminate: { terminated = true }) let local = LocalWallpaper( id: "local", url: URL(fileURLWithPath: "/tmp/local.jpg"), filename: "local.jpg", resolution: "1x1", fileSize: 10, createdAt: Date(), modifiedAt: Date()) await state.setLocalWallpaper(local) #expect(wallpaperManager.desktopURLs == [local.url]) #expect(state.historyManager.history.first?.purity == "local") wallpaperManager.error = URLError(.cannotOpenFile) await state.setLocalWallpaper(local) #expect(state.errorMessage?.hasPrefix("Failed to set wallpaper") == true) state.launchAtLogin = true #expect(autostart.isEnabled) state.selectSaveFolder() #expect(state.saveFolderPath == selected.path) state.quit() #expect(terminated) var offeredDirectory: URL? let cancelState = makeState(chooseFolder: { directory, completion in offeredDirectory = directory completion(nil) }) let oldPath = cancelState.saveFolderPath cancelState.selectSaveFolder() #expect(offeredDirectory?.path == oldPath) #expect(cancelState.saveFolderPath == oldPath) } @Test("Observed search and settings changes trigger their pipelines") func observedChanges() async { let search = SearchStub() search.results = Array(repeating: .success(([], nil)), count: 50) let state = AppState( api: search, chooseFolder: { _, completion in completion(nil) }, terminateApplication: {}, observeChanges: true) func waitForRequests(_ expected: Int) async { for _ in 0..<100 where search.requests.count < expected { try? await Task.sleep(nanoseconds: 20_000_000) } } state.scheduleSearch() await waitForRequests(1) state.feedMode = .random await waitForRequests(2) state.searchText = "updated" await waitForRequests(3) state.categories = (false, true, true) await waitForRequests(4) state.purity = (true, true) await waitForRequests(5) state.saveToFolderEnabled.toggle() state.saveFolderPath = FileManager.default.temporaryDirectory.path #expect(search.requests.count >= 5) } }