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()3738 39 40 if let status = WallhavenStatus(41 error: WallhavenError.unavailable("Wallhaven is unavailable right now (HTTP 521)."))42 {43 state.wallhavenStatus = status44 render(explore)45 let panel = WallhavenStatusPanel(status: status) {}46 render(panel)47 panel.retry()48 }49 state.wallhavenStatus = nil5051 for mode in FeedMode.allCases {52 state.feedMode = mode53 render(explore)54 explore.select(mode)55 render(56 FeedModePill(mode: mode, isActive: true) {57 state.feedMode = mode58 })59 render(FeedModePill(mode: mode, isActive: false) {})60 }61 }6263 @Test("Grid, item, favorites, and history states render")64 func remoteWallpaperViews() async {65 let search = SearchStub()66 search.results = [.success(([], nil))]67 let wallpaperManager = WallpaperStub()68 let state = AppState(69 api: search, wallpaperManager: wallpaperManager, observeChanges: false)70 let wallpaper = makeWallpaper(id: "view")71 var actionCount = 07273 let item = WallpaperItem(74 wallpaper: wallpaper, action: { actionCount += 1 }, maxAttempts: 1,75 retryDelayNanoseconds: 0)76 render(item)77 render(item.thumbnail(for: .empty))78 render(item.thumbnail(for: .success(Image(systemName: "photo"))))79 render(item.thumbnail(for: .failure(URLError(.badServerResponse))))80 await item.retryAfterFailure()81 let exhausted = WallpaperItem(82 wallpaper: wallpaper, action: {}, attempt: 1, maxAttempts: 1,83 retryDelayNanoseconds: 0)84 render(exhausted.thumbnail(for: .failure(URLError(.badServerResponse))))85 await exhausted.retryAfterFailure()86 item.toggleFavorite()87 item.toggleFavorite()88 item.action()89 #expect(actionCount == 1)9091 92 93 let backoffItem = WallpaperItem(94 wallpaper: wallpaper, action: {}, maxAttempts: 4, retryDelayNanoseconds: 1)95 let schedule = (0..<4).map { backoffItem.backoffDelay(forAttempt: $0) }96 #expect(schedule == [1, 2, 4, 8])9798 let cancelled = WallpaperItem(99 wallpaper: wallpaper, action: {}, maxAttempts: 4, retryDelayNanoseconds: 1)100 let cancelTask = Task { await cancelled.retryAfterFailure() }101 cancelTask.cancel()102 await cancelTask.value103 #expect(cancelled.exposedAttempt == 0)104105 render(WallpaperGrid(wallpapers: [], appState: state, onEndReached: nil))106 let grid = WallpaperGrid(wallpapers: [wallpaper], appState: state, onEndReached: {})107 render(grid)108 state.wallpapers = [wallpaper]109 state.isLoading = true110 render(grid)111 state.isLoading = false112 await grid.set(wallpaper)113 await grid.refresh()114115 state.favoritesManager.favorites = []116 render(FavoritesView(appState: state, favoritesManager: state.favoritesManager))117 state.favoritesManager.favorites = [wallpaper]118 render(FavoritesView(appState: state, favoritesManager: state.favoritesManager))119120 state.historyManager.history = []121 render(HistoryView(appState: state, historyManager: state.historyManager))122 state.historyManager.history = [wallpaper]123 let history = HistoryView(appState: state, historyManager: state.historyManager)124 render(history)125 history.clearHistory()126 state.historyManager.history = [wallpaper]127 render(history)128 }129130 @Test("Local gallery renders enabled, disabled, empty, and populated states")131 func localViews() async throws {132 let defaults = UserDefaults.standard133 let oldEnabled = defaults.object(forKey: "saveToFolderEnabled")134 let oldPath = defaults.string(forKey: "saveFolderPath")135 defer {136 if let oldEnabled {137 defaults.set(oldEnabled, forKey: "saveToFolderEnabled")138 } else {139 defaults.removeObject(forKey: "saveToFolderEnabled")140 }141 if let oldPath {142 defaults.set(oldPath, forKey: "saveFolderPath")143 } else {144 defaults.removeObject(forKey: "saveFolderPath")145 }146 }147148 let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)149 defer { try? FileManager.default.removeItem(at: root) }150 try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)151 let fileURL = root.appendingPathComponent("local.png")152 let png = try #require(153 Data(154 base64Encoded:155 "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="156 )157 )158 try png.write(to: fileURL)159160 defaults.set(false, forKey: "saveToFolderEnabled")161 defaults.set(root.path, forKey: "saveFolderPath")162 let wallpaperManager = WallpaperStub()163 let state = AppState(wallpaperManager: wallpaperManager, observeChanges: false)164 state.saveFolderPath = root.path165 state.saveToFolderEnabled = false166 let manager = LocalWallpapersManager()167 let localView = LocalView(appState: state, localManager: manager)168 render(localView)169170 state.saveToFolderEnabled = true171 render(localView)172173 let local = LocalWallpaper(174 id: fileURL.path,175 url: fileURL,176 filename: fileURL.lastPathComponent,177 resolution: "2x2",178 fileSize: 100,179 createdAt: Date(),180 modifiedAt: Date()181 )182 manager.isLoading = true183 manager.wallpapers = []184 render(localView)185 manager.isLoading = false186 state.saveToFolderEnabled = false187 render(localView)188 state.saveToFolderEnabled = true189 manager.wallpapers = []190 render(localView)191 manager.wallpapers = [local]192 render(localView)193 var setCount = 0194 var deleteCount = 0195 render(196 LocalWallpaperGrid(197 wallpapers: [local],198 onSetWallpaper: { _ in setCount += 1 },199 onDeleteWallpaper: { _ in deleteCount += 1 }200 ))201 let item = LocalWallpaperItem(202 wallpaper: local,203 onSet: { setCount += 1 },204 onDelete: { deleteCount += 1 }205 )206 render(item)207 item.onSet()208 item.onDelete()209 #expect(setCount == 1)210 #expect(deleteCount == 1)211 render(212 LocalWallpaperItem(213 wallpaper: local, onSet: {}, onDelete: {},214 initialImage: NSImage(size: NSSize(width: 1, height: 1)), isLoadingImage: false))215 render(216 LocalWallpaperItem(217 wallpaper: local, onSet: {}, onDelete: {}, initialImage: nil,218 isLoadingImage: false))219 localView.scan()220 await localView.set(local)221 localView.requestDelete(local)222 render(localView.deleteMessage(local))223 render(localView.deleteActions(local))224 localView.cancelDelete()225 localView.delete(local)226 render(LoadingState())227 }228229 @Test("Settings and reusable components render and invoke actions")230 func settingsAndComponents() {231 var quitCalled = false232 let state = AppState(233 chooseFolder: { _, completion in completion(nil) },234 terminateApplication: { quitCalled = true }, observeChanges: false)235 state.saveToFolderEnabled = false236 let settings = SettingsView(appState: state)237 render(settings)238 state.saveToFolderEnabled = true239 render(settings)240 settings.selectFolder()241 settings.quit()242 #expect(quitCalled)243244 render(EmptyState(icon: "photo", text: "Empty"))245 var tab = AppTab.explore246 let tabButton = TabButton(247 icon: "heart", tab: .favorites,248 activeTab: Binding(get: { tab }, set: { tab = $0 }))249 render(tabButton)250 tabButton.select()251 #expect(tab == .favorites)252253 var invoked = false254 let button = GlossyButton(title: "Run", icon: "play", action: { invoked = true })255 render(button)256 button.action()257 #expect(invoked)258 render(GlossyButton(title: "Run", icon: nil, action: {}))259260 render(261 VisualEffectView(262 material: .headerView,263 blendingMode: .behindWindow264 ))265 }266267 @Suite("Local wallpaper thumbnail downsampling", .serialized)268 @MainActor269 struct LocalWallpaperItemThumbnailTests {270 private static let onePixelPNG = Data(271 base64Encoded:272 "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="273 )!274275 @Test("makeThumbnail returns an NSImage and downsamples oversized PNGs")276 func thumbnailDecodeAndDownsample() throws {277 let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)278 defer { try? FileManager.default.removeItem(at: root) }279 try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)280 let png = root.appendingPathComponent("image.png")281 try Self.onePixelPNG.write(to: png)282283 let image = LocalWallpaperItem.makeThumbnail(url: png, maxPixel: 320)284 #expect(image != nil)285 286 287 #expect(image?.size.width ?? 0 >= 1)288 }289290 @Test("makeThumbnail returns nil for a missing file rather than trapping")291 func missingFileReturnsNil() {292 let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)293 defer { try? FileManager.default.removeItem(at: root) }294 try? FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)295 let missing = root.appendingPathComponent("missing.png")296297 #expect(LocalWallpaperItem.makeThumbnail(url: missing, maxPixel: 320) == nil)298 }299 }300}