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 258 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}