import AppKit import Foundation import SwiftUI import Testing @testable import Haven @Suite("SwiftUI view construction", .serialized) @MainActor struct ViewCoverageTests { private func render(_ view: V) { let hostingView = NSHostingView(rootView: view) hostingView.frame = NSRect(x: 0, y: 0, width: 450, height: 640) hostingView.layoutSubtreeIfNeeded() hostingView.displayIfNeeded() } @Test("Main and explore views render every feed selection") func mainAndExploreViews() async { let search = SearchStub() search.results = Array( repeating: .success(([], Meta(current_page: 1, last_page: 1, per_page: 24, total: 0))), count: 20) let state = AppState(api: search, observeChanges: false) for tab in [AppTab.explore, .local, .favorites, .history, .settings] { state.activeTab = tab render(MainView(appState: state)) } let explore = ExploreView(appState: state) render(explore.resolutionOption("Any")) render(explore.ratioOption("Any")) await explore.refresh() explore.refreshAction() await explore.loadMore() explore.loadMoreAction() for mode in FeedMode.allCases { state.feedMode = mode render(explore) explore.select(mode) render( FeedModePill(mode: mode, isActive: true) { state.feedMode = mode }) render(FeedModePill(mode: mode, isActive: false) {}) } } @Test("Grid, item, favorites, and history states render") func remoteWallpaperViews() async { let search = SearchStub() search.results = [.success(([], nil))] let wallpaperManager = WallpaperStub() let state = AppState( api: search, wallpaperManager: wallpaperManager, observeChanges: false) let wallpaper = makeWallpaper(id: "view") var actionCount = 0 let item = WallpaperItem( wallpaper: wallpaper, action: { actionCount += 1 }, maxAttempts: 1, retryDelayNanoseconds: 0) render(item) render(item.thumbnail(for: .empty)) render(item.thumbnail(for: .success(Image(systemName: "photo")))) render(item.thumbnail(for: .failure(URLError(.badServerResponse)))) await item.retryAfterFailure() let exhausted = WallpaperItem( wallpaper: wallpaper, action: {}, attempt: 1, maxAttempts: 1, retryDelayNanoseconds: 0) render(exhausted.thumbnail(for: .failure(URLError(.badServerResponse)))) await exhausted.retryAfterFailure() item.toggleFavorite() item.toggleFavorite() item.action() #expect(actionCount == 1) // Exponential backoff: each successive attempt doubles the wait, and a // cancelled task bails out without advancing the attempt counter. let backoffItem = WallpaperItem( wallpaper: wallpaper, action: {}, maxAttempts: 4, retryDelayNanoseconds: 1) let schedule = (0..<4).map { backoffItem.backoffDelay(forAttempt: $0) } #expect(schedule == [1, 2, 4, 8]) let cancelled = WallpaperItem( wallpaper: wallpaper, action: {}, maxAttempts: 4, retryDelayNanoseconds: 1) let cancelTask = Task { await cancelled.retryAfterFailure() } cancelTask.cancel() await cancelTask.value #expect(cancelled.exposedAttempt == 0) render(WallpaperGrid(wallpapers: [], appState: state, onEndReached: nil)) let grid = WallpaperGrid(wallpapers: [wallpaper], appState: state, onEndReached: {}) render(grid) state.wallpapers = [wallpaper] state.isLoading = true render(grid) state.isLoading = false await grid.set(wallpaper) await grid.refresh() state.favoritesManager.favorites = [] render(FavoritesView(appState: state, favoritesManager: state.favoritesManager)) state.favoritesManager.favorites = [wallpaper] render(FavoritesView(appState: state, favoritesManager: state.favoritesManager)) state.historyManager.history = [] render(HistoryView(appState: state, historyManager: state.historyManager)) state.historyManager.history = [wallpaper] let history = HistoryView(appState: state, historyManager: state.historyManager) render(history) history.clearHistory() state.historyManager.history = [wallpaper] render(history) } @Test("Local gallery renders enabled, disabled, empty, and populated states") func localViews() async throws { let defaults = UserDefaults.standard let oldEnabled = defaults.object(forKey: "saveToFolderEnabled") let oldPath = defaults.string(forKey: "saveFolderPath") defer { if let oldEnabled { defaults.set(oldEnabled, forKey: "saveToFolderEnabled") } else { defaults.removeObject(forKey: "saveToFolderEnabled") } if let oldPath { defaults.set(oldPath, forKey: "saveFolderPath") } else { defaults.removeObject(forKey: "saveFolderPath") } } let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) defer { try? FileManager.default.removeItem(at: root) } try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true) let fileURL = root.appendingPathComponent("local.png") let png = try #require( Data( base64Encoded: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=" ) ) try png.write(to: fileURL) defaults.set(false, forKey: "saveToFolderEnabled") defaults.set(root.path, forKey: "saveFolderPath") let wallpaperManager = WallpaperStub() let state = AppState(wallpaperManager: wallpaperManager, observeChanges: false) state.saveFolderPath = root.path state.saveToFolderEnabled = false let manager = LocalWallpapersManager() let localView = LocalView(appState: state, localManager: manager) render(localView) state.saveToFolderEnabled = true render(localView) let local = LocalWallpaper( id: fileURL.path, url: fileURL, filename: fileURL.lastPathComponent, resolution: "2x2", fileSize: 100, createdAt: Date(), modifiedAt: Date() ) manager.isLoading = true manager.wallpapers = [] render(localView) manager.isLoading = false state.saveToFolderEnabled = false render(localView) state.saveToFolderEnabled = true manager.wallpapers = [] render(localView) manager.wallpapers = [local] render(localView) var setCount = 0 var deleteCount = 0 render( LocalWallpaperGrid( wallpapers: [local], onSetWallpaper: { _ in setCount += 1 }, onDeleteWallpaper: { _ in deleteCount += 1 } )) let item = LocalWallpaperItem( wallpaper: local, onSet: { setCount += 1 }, onDelete: { deleteCount += 1 } ) render(item) item.onSet() item.onDelete() #expect(setCount == 1) #expect(deleteCount == 1) render( LocalWallpaperItem( wallpaper: local, onSet: {}, onDelete: {}, initialImage: NSImage(size: NSSize(width: 1, height: 1)), isLoadingImage: false)) render( LocalWallpaperItem( wallpaper: local, onSet: {}, onDelete: {}, initialImage: nil, isLoadingImage: false)) localView.scan() await localView.set(local) localView.requestDelete(local) render(localView.deleteMessage(local)) render(localView.deleteActions(local)) localView.cancelDelete() localView.delete(local) render(LoadingState()) } @Test("Settings and reusable components render and invoke actions") func settingsAndComponents() { var quitCalled = false let state = AppState( chooseFolder: { _, completion in completion(nil) }, terminateApplication: { quitCalled = true }, observeChanges: false) state.saveToFolderEnabled = false let settings = SettingsView(appState: state) render(settings) state.saveToFolderEnabled = true render(settings) settings.selectFolder() settings.quit() #expect(quitCalled) render(EmptyState(icon: "photo", text: "Empty")) var tab = AppTab.explore let tabButton = TabButton( icon: "heart", tab: .favorites, activeTab: Binding(get: { tab }, set: { tab = $0 })) render(tabButton) tabButton.select() #expect(tab == .favorites) var invoked = false let button = GlossyButton(title: "Run", icon: "play", action: { invoked = true }) render(button) button.action() #expect(invoked) render(GlossyButton(title: "Run", icon: nil, action: {})) render( VisualEffectView( material: .headerView, blendingMode: .behindWindow )) } @Suite("Local wallpaper thumbnail downsampling", .serialized) @MainActor struct LocalWallpaperItemThumbnailTests { private static let onePixelPNG = Data( base64Encoded: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=" )! @Test("makeThumbnail returns an NSImage and downsamples oversized PNGs") func thumbnailDecodeAndDownsample() throws { let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) defer { try? FileManager.default.removeItem(at: root) } try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true) let png = root.appendingPathComponent("image.png") try Self.onePixelPNG.write(to: png) let image = LocalWallpaperItem.makeThumbnail(url: png, maxPixel: 320) #expect(image != nil) // ImageIO preserves the original pixel dimensions for tiny sources, so // we only assert that decoding returned something usable. #expect(image?.size.width ?? 0 >= 1) } @Test("makeThumbnail returns nil for a missing file rather than trapping") func missingFileReturnsNil() { let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) defer { try? FileManager.default.removeItem(at: root) } try? FileManager.default.createDirectory(at: root, withIntermediateDirectories: true) let missing = root.appendingPathComponent("missing.png") #expect(LocalWallpaperItem.makeThumbnail(url: missing, maxPixel: 320) == nil) } } }