ViewCoverageTests.swift

9.8 KB · raw

1import AppKit2import Foundation3import SwiftUI4import Testing56@testable import Haven78@Suite("SwiftUI view construction", .serialized)9@MainActor10struct ViewCoverageTests {11  private func render<V: View>(_ view: V) {12    let hostingView = NSHostingView(rootView: view)13    hostingView.frame = NSRect(x: 0, y: 0, width: 450, height: 640)14    hostingView.layoutSubtreeIfNeeded()15    hostingView.displayIfNeeded()16  }1718  @Test("Main and explore views render every feed selection")19  func mainAndExploreViews() async {20    let search = SearchStub()21    search.results = Array(22      repeating: .success(([], Meta(current_page: 1, last_page: 1, per_page: 24, total: 0))),23      count: 20)24    let state = AppState(api: search, observeChanges: false)25    for tab in [AppTab.explore, .local, .favorites, .history, .settings] {26      state.activeTab = tab27      render(MainView(appState: state))28    }2930    let explore = ExploreView(appState: state)31    render(explore.resolutionOption("Any"))32    render(explore.ratioOption("Any"))33    await explore.refresh()34    explore.refreshAction()35    await explore.loadMore()36    explore.loadMoreAction()37    for mode in FeedMode.allCases {38      state.feedMode = mode39      render(explore)40      explore.select(mode)41      render(42        FeedModePill(mode: mode, isActive: true) {43          state.feedMode = mode44        })45      render(FeedModePill(mode: mode, isActive: false) {})46    }47  }4849  @Test("Grid, item, favorites, and history states render")50  func remoteWallpaperViews() async {51    let search = SearchStub()52    search.results = [.success(([], nil))]53    let wallpaperManager = WallpaperStub()54    let state = AppState(55      api: search, wallpaperManager: wallpaperManager, observeChanges: false)56    let wallpaper = makeWallpaper(id: "view")57    var actionCount = 05859    let item = WallpaperItem(60      wallpaper: wallpaper, action: { actionCount += 1 }, maxAttempts: 1,61      retryDelayNanoseconds: 0)62    render(item)63    render(item.thumbnail(for: .empty))64    render(item.thumbnail(for: .success(Image(systemName: "photo"))))65    render(item.thumbnail(for: .failure(URLError(.badServerResponse))))66    await item.retryAfterFailure()67    let exhausted = WallpaperItem(68      wallpaper: wallpaper, action: {}, attempt: 1, maxAttempts: 1,69      retryDelayNanoseconds: 0)70    render(exhausted.thumbnail(for: .failure(URLError(.badServerResponse))))71    await exhausted.retryAfterFailure()72    item.toggleFavorite()73    item.toggleFavorite()74    item.action()75    #expect(actionCount == 1)7677    // Exponential backoff: each successive attempt doubles the wait, and a78    // cancelled task bails out without advancing the attempt counter.79    let backoffItem = WallpaperItem(80      wallpaper: wallpaper, action: {}, maxAttempts: 4, retryDelayNanoseconds: 1)81    let schedule = (0..<4).map { backoffItem.backoffDelay(forAttempt: $0) }82    #expect(schedule == [1, 2, 4, 8])8384    let cancelled = WallpaperItem(85      wallpaper: wallpaper, action: {}, maxAttempts: 4, retryDelayNanoseconds: 1)86    let cancelTask = Task { await cancelled.retryAfterFailure() }87    cancelTask.cancel()88    await cancelTask.value89    #expect(cancelled.exposedAttempt == 0)9091    render(WallpaperGrid(wallpapers: [], appState: state, onEndReached: nil))92    let grid = WallpaperGrid(wallpapers: [wallpaper], appState: state, onEndReached: {})93    render(grid)94    state.wallpapers = [wallpaper]95    state.isLoading = true96    render(grid)97    state.isLoading = false98    await grid.set(wallpaper)99    await grid.refresh()100101    state.favoritesManager.favorites = []102    render(FavoritesView(appState: state))103    state.favoritesManager.favorites = [wallpaper]104    render(FavoritesView(appState: state))105106    state.historyManager.history = []107    render(HistoryView(appState: state))108    state.historyManager.history = [wallpaper]109    let history = HistoryView(appState: state)110    render(history)111    history.clearHistory()112    state.historyManager.history = [wallpaper]113    render(history)114  }115116  @Test("Local gallery renders enabled, disabled, empty, and populated states")117  func localViews() async throws {118    let defaults = UserDefaults.standard119    let oldEnabled = defaults.object(forKey: "saveToFolderEnabled")120    let oldPath = defaults.string(forKey: "saveFolderPath")121    defer {122      if let oldEnabled {123        defaults.set(oldEnabled, forKey: "saveToFolderEnabled")124      } else {125        defaults.removeObject(forKey: "saveToFolderEnabled")126      }127      if let oldPath {128        defaults.set(oldPath, forKey: "saveFolderPath")129      } else {130        defaults.removeObject(forKey: "saveFolderPath")131      }132    }133134    let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)135    defer { try? FileManager.default.removeItem(at: root) }136    try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)137    let fileURL = root.appendingPathComponent("local.png")138    let png = try #require(139      Data(140        base64Encoded:141          "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="142      )143    )144    try png.write(to: fileURL)145146    defaults.set(false, forKey: "saveToFolderEnabled")147    defaults.set(root.path, forKey: "saveFolderPath")148    let wallpaperManager = WallpaperStub()149    let state = AppState(wallpaperManager: wallpaperManager, observeChanges: false)150    state.saveFolderPath = root.path151    state.saveToFolderEnabled = false152    let manager = LocalWallpapersManager()153    let localView = LocalView(appState: state, localManager: manager)154    render(localView)155156    state.saveToFolderEnabled = true157    render(localView)158159    let local = LocalWallpaper(160      id: fileURL.path,161      url: fileURL,162      filename: fileURL.lastPathComponent,163      resolution: "2x2",164      fileSize: 100,165      createdAt: Date(),166      modifiedAt: Date()167    )168    manager.isLoading = true169    manager.wallpapers = []170    render(localView)171    manager.isLoading = false172    state.saveToFolderEnabled = false173    render(localView)174    state.saveToFolderEnabled = true175    manager.wallpapers = []176    render(localView)177    manager.wallpapers = [local]178    render(localView)179    var setCount = 0180    var deleteCount = 0181    render(182      LocalWallpaperGrid(183        wallpapers: [local],184        onSetWallpaper: { _ in setCount += 1 },185        onDeleteWallpaper: { _ in deleteCount += 1 }186      ))187    let item = LocalWallpaperItem(188      wallpaper: local,189      onSet: { setCount += 1 },190      onDelete: { deleteCount += 1 }191    )192    render(item)193    item.onSet()194    item.onDelete()195    #expect(setCount == 1)196    #expect(deleteCount == 1)197    render(198      LocalWallpaperItem(199        wallpaper: local, onSet: {}, onDelete: {},200        initialImage: NSImage(size: NSSize(width: 1, height: 1)), isLoadingImage: false))201    render(202      LocalWallpaperItem(203        wallpaper: local, onSet: {}, onDelete: {}, initialImage: nil,204        isLoadingImage: false))205    localView.scan()206    await localView.set(local)207    localView.requestDelete(local)208    render(localView.deleteMessage(local))209    render(localView.deleteActions(local))210    localView.cancelDelete()211    localView.delete(local)212    render(LoadingState())213  }214215  @Test("Settings and reusable components render and invoke actions")216  func settingsAndComponents() {217    var quitCalled = false218    let state = AppState(219      chooseFolder: { _, completion in completion(nil) },220      terminateApplication: { quitCalled = true }, observeChanges: false)221    state.saveToFolderEnabled = false222    let settings = SettingsView(appState: state)223    render(settings)224    state.saveToFolderEnabled = true225    render(settings)226    settings.selectFolder()227    settings.quit()228    #expect(quitCalled)229230    render(EmptyState(icon: "photo", text: "Empty"))231    var tab = AppTab.explore232    let tabButton = TabButton(233      icon: "heart", tab: .favorites,234      activeTab: Binding(get: { tab }, set: { tab = $0 }))235    render(tabButton)236    tabButton.select()237    #expect(tab == .favorites)238239    var invoked = false240    let button = GlossyButton(title: "Run", icon: "play", action: { invoked = true })241    render(button)242    button.action()243    #expect(invoked)244    render(GlossyButton(title: "Run", icon: nil, action: {}))245246    render(247      VisualEffectView(248        material: .headerView,249        blendingMode: .behindWindow250      ))251  }252253  @Suite("Local wallpaper thumbnail downsampling", .serialized)254  @MainActor255  struct LocalWallpaperItemThumbnailTests {256    private static let onePixelPNG = Data(257      base64Encoded:258        "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="259    )!260261    @Test("makeThumbnail returns an NSImage and downsamples oversized PNGs")262    func thumbnailDecodeAndDownsample() throws {263      let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)264      defer { try? FileManager.default.removeItem(at: root) }265      try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)266      let png = root.appendingPathComponent("image.png")267      try Self.onePixelPNG.write(to: png)268269      let image = LocalWallpaperItem.makeThumbnail(url: png, maxPixel: 320)270      #expect(image != nil)271      // ImageIO preserves the original pixel dimensions for tiny sources, so272      // we only assert that decoding returned something usable.273      #expect(image?.size.width ?? 0 >= 1)274    }275276    @Test("makeThumbnail returns nil for a missing file rather than trapping")277    func missingFileReturnsNil() {278      let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)279      defer { try? FileManager.default.removeItem(at: root) }280      try? FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)281      let missing = root.appendingPathComponent("missing.png")282283      #expect(LocalWallpaperItem.makeThumbnail(url: missing, maxPixel: 320) == nil)284    }285  }286}