ViewCoverageTests.swift

9.1 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    render(WallpaperGrid(wallpapers: [], appState: state, onEndReached: nil))78    let grid = WallpaperGrid(wallpapers: [wallpaper], appState: state, onEndReached: {})79    render(grid)80    state.wallpapers = [wallpaper]81    state.isLoading = true82    render(grid)83    state.isLoading = false84    await grid.set(wallpaper)85    await grid.refresh()8687    state.favoritesManager.favorites = []88    render(FavoritesView(appState: state))89    state.favoritesManager.favorites = [wallpaper]90    render(FavoritesView(appState: state))9192    state.historyManager.history = []93    render(HistoryView(appState: state))94    state.historyManager.history = [wallpaper]95    let history = HistoryView(appState: state)96    render(history)97    history.clearHistory()98    state.historyManager.history = [wallpaper]99    render(history)100  }101102  @Test("Local gallery renders enabled, disabled, empty, and populated states")103  func localViews() async throws {104    let defaults = UserDefaults.standard105    let oldEnabled = defaults.object(forKey: "saveToFolderEnabled")106    let oldPath = defaults.string(forKey: "saveFolderPath")107    defer {108      if let oldEnabled {109        defaults.set(oldEnabled, forKey: "saveToFolderEnabled")110      } else {111        defaults.removeObject(forKey: "saveToFolderEnabled")112      }113      if let oldPath {114        defaults.set(oldPath, forKey: "saveFolderPath")115      } else {116        defaults.removeObject(forKey: "saveFolderPath")117      }118    }119120    let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)121    defer { try? FileManager.default.removeItem(at: root) }122    try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)123    let fileURL = root.appendingPathComponent("local.png")124    let png = try #require(125      Data(126        base64Encoded:127          "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="128      )129    )130    try png.write(to: fileURL)131132    defaults.set(false, forKey: "saveToFolderEnabled")133    defaults.set(root.path, forKey: "saveFolderPath")134    let wallpaperManager = WallpaperStub()135    let state = AppState(wallpaperManager: wallpaperManager, observeChanges: false)136    state.saveFolderPath = root.path137    state.saveToFolderEnabled = false138    let manager = LocalWallpapersManager()139    let localView = LocalView(appState: state, localManager: manager)140    render(localView)141142    state.saveToFolderEnabled = true143    render(localView)144145    let local = LocalWallpaper(146      id: fileURL.path,147      url: fileURL,148      filename: fileURL.lastPathComponent,149      resolution: "2x2",150      fileSize: 100,151      createdAt: Date(),152      modifiedAt: Date()153    )154    manager.isLoading = true155    manager.wallpapers = []156    render(localView)157    manager.isLoading = false158    state.saveToFolderEnabled = false159    render(localView)160    state.saveToFolderEnabled = true161    manager.wallpapers = []162    render(localView)163    manager.wallpapers = [local]164    render(localView)165    var setCount = 0166    var deleteCount = 0167    render(168      LocalWallpaperGrid(169        wallpapers: [local],170        onSetWallpaper: { _ in setCount += 1 },171        onDeleteWallpaper: { _ in deleteCount += 1 }172      ))173    let item = LocalWallpaperItem(174      wallpaper: local,175      onSet: { setCount += 1 },176      onDelete: { deleteCount += 1 }177    )178    render(item)179    item.onSet()180    item.onDelete()181    #expect(setCount == 1)182    #expect(deleteCount == 1)183    render(184      LocalWallpaperItem(185        wallpaper: local, onSet: {}, onDelete: {},186        initialImage: NSImage(size: NSSize(width: 1, height: 1)), isLoadingImage: false))187    render(188      LocalWallpaperItem(189        wallpaper: local, onSet: {}, onDelete: {}, initialImage: nil,190        isLoadingImage: false))191    localView.scan()192    await localView.set(local)193    localView.requestDelete(local)194    render(localView.deleteMessage(local))195    render(localView.deleteActions(local))196    localView.cancelDelete()197    localView.delete(local)198    render(LoadingState())199  }200201  @Test("Settings and reusable components render and invoke actions")202  func settingsAndComponents() {203    var quitCalled = false204    let state = AppState(205      chooseFolder: { _, completion in completion(nil) },206      terminateApplication: { quitCalled = true }, observeChanges: false)207    state.saveToFolderEnabled = false208    let settings = SettingsView(appState: state)209    render(settings)210    state.saveToFolderEnabled = true211    render(settings)212    settings.selectFolder()213    settings.quit()214    #expect(quitCalled)215216    render(EmptyState(icon: "photo", text: "Empty"))217    var tab = AppTab.explore218    let tabButton = TabButton(219      icon: "heart", tab: .favorites,220      activeTab: Binding(get: { tab }, set: { tab = $0 }))221    render(tabButton)222    tabButton.select()223    #expect(tab == .favorites)224225    var invoked = false226    let button = GlossyButton(title: "Run", icon: "play", action: { invoked = true })227    render(button)228    button.action()229    #expect(invoked)230    render(GlossyButton(title: "Run", icon: nil, action: {}))231232    render(233      VisualEffectView(234        material: .headerView,235        blendingMode: .behindWindow236      ))237  }238239  @Suite("Local wallpaper thumbnail downsampling", .serialized)240  @MainActor241  struct LocalWallpaperItemThumbnailTests {242    private static let onePixelPNG = Data(243      base64Encoded:244        "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="245    )!246247    @Test("makeThumbnail returns an NSImage and downsamples oversized PNGs")248    func thumbnailDecodeAndDownsample() throws {249      let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)250      defer { try? FileManager.default.removeItem(at: root) }251      try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)252      let png = root.appendingPathComponent("image.png")253      try Self.onePixelPNG.write(to: png)254255      let image = LocalWallpaperItem.makeThumbnail(url: png, maxPixel: 320)256      #expect(image != nil)257      // ImageIO preserves the original pixel dimensions for tiny sources, so258      // we only assert that decoding returned something usable.259      #expect(image?.size.width ?? 0 >= 1)260    }261262    @Test("makeThumbnail returns nil for a missing file rather than trapping")263    func missingFileReturnsNil() {264      let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)265      defer { try? FileManager.default.removeItem(at: root) }266      try? FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)267      let missing = root.appendingPathComponent("missing.png")268269      #expect(LocalWallpaperItem.makeThumbnail(url: missing, maxPixel: 320) == nil)270    }271  }272}