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 78 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, favoritesManager: state.favoritesManager))103 state.favoritesManager.favorites = [wallpaper]104 render(FavoritesView(appState: state, favoritesManager: state.favoritesManager))105106 state.historyManager.history = []107 render(HistoryView(appState: state, historyManager: state.historyManager))108 state.historyManager.history = [wallpaper]109 let history = HistoryView(appState: state, historyManager: state.historyManager)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 272 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}