AppStateTests.swift

8.8 KB · raw

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}