From c11d657fba144431aa79e926cdc6cd840b787a67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Orzech?= Date: Sat, 14 Mar 2026 22:50:47 +0100 Subject: [PATCH] Improve code quality: network DI, deduplicate logic, parallelize fetches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add NetworkMonitor with NetworkConnectivity protocol for DI and testability - Deduplicate flag mapping (MenuBarLabel now uses TornDestination.flag(for:)) - Centralize logger subsystem string in TornConstants.logSubsystem - Fold API error check into parseDataInBackground, removing double JSON parse - Fix fetchFactionData double JSON parse (single parse with early error check) - Parallelize user data parse and faction fetch with async let - Deduplicate refreshNow() by delegating to startPolling() - Remove redundant showLoginError state in SettingsView (use computed Binding) - Batch watchlist saves after concurrent price fetches (N saves β†’ 1 save) - Add change detection for offline errorMsg to avoid no-op @Published updates - Remove unused ObservableObject conformance from NetworkMonitor - Fix tokens CodingKey (company_funds β†’ donator) - Replace print() calls with os.log Logger - Remove stub UI tests that only asserted window.exists --- MacTorn/MacTorn.xcodeproj/project.pbxproj | 4 + MacTorn/MacTorn/MacTornApp.swift | 18 +-- MacTorn/MacTorn/Models/TornModels.swift | 16 ++- .../Utilities/LaunchAtLoginManager.swift | 10 +- .../MacTorn/Utilities/NetworkMonitor.swift | 31 +++++ .../Utilities/NotificationManager.swift | 13 +- MacTorn/MacTorn/ViewModels/AppState.swift | 126 +++++++++--------- MacTorn/MacTorn/Views/SettingsView.swift | 12 +- .../Fixtures/TornAPIFixtures.swift | 2 +- .../MacTornTests/Models/MoneyDataTests.swift | 2 +- MacTorn/MacTornUITests/MacTornUITests.swift | 105 --------------- 11 files changed, 145 insertions(+), 194 deletions(-) create mode 100644 MacTorn/MacTorn/Utilities/NetworkMonitor.swift diff --git a/MacTorn/MacTorn.xcodeproj/project.pbxproj b/MacTorn/MacTorn.xcodeproj/project.pbxproj index 3bbb217..92e08ec 100644 --- a/MacTorn/MacTorn.xcodeproj/project.pbxproj +++ b/MacTorn/MacTorn.xcodeproj/project.pbxproj @@ -33,6 +33,7 @@ AAA00024 /* TransparencyEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA10025 /* TransparencyEnvironment.swift */; }; AAA00025 /* FeedbackPromptView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA10026 /* FeedbackPromptView.swift */; }; AAA00026 /* BrowserManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA10027 /* BrowserManager.swift */; }; + AAA00028 /* NetworkMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA10028 /* NetworkMonitor.swift */; }; /* Unit Tests */ BBB00001 /* MockNetworkSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBB10001 /* MockNetworkSession.swift */; }; BBB00002 /* TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBB10002 /* TestHelpers.swift */; }; @@ -96,6 +97,7 @@ AAA10025 /* TransparencyEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransparencyEnvironment.swift; sourceTree = ""; }; AAA10026 /* FeedbackPromptView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackPromptView.swift; sourceTree = ""; }; AAA10027 /* BrowserManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserManager.swift; sourceTree = ""; }; + AAA10028 /* NetworkMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMonitor.swift; sourceTree = ""; }; AAA10000 /* MacTorn.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MacTorn.app; sourceTree = BUILT_PRODUCTS_DIR; }; /* Unit Test Files */ BBB10001 /* MockNetworkSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockNetworkSession.swift; sourceTree = ""; }; @@ -232,6 +234,7 @@ AAA10011 /* LaunchAtLoginManager.swift */, AAA10012 /* ShortcutsManager.swift */, AAA10016 /* SoundManager.swift */, + AAA10028 /* NetworkMonitor.swift */, ); path = Utilities; sourceTree = ""; @@ -468,6 +471,7 @@ AAA00024 /* TransparencyEnvironment.swift in Sources */, AAA00025 /* FeedbackPromptView.swift in Sources */, AAA00026 /* BrowserManager.swift in Sources */, + AAA00028 /* NetworkMonitor.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/MacTorn/MacTorn/MacTornApp.swift b/MacTorn/MacTorn/MacTornApp.swift index 4f9a4cd..b6342b5 100644 --- a/MacTorn/MacTorn/MacTornApp.swift +++ b/MacTorn/MacTorn/MacTornApp.swift @@ -14,7 +14,7 @@ struct MacTornApp: App { .onAppear { updateAppearance() } - .onChange(of: appearanceModeRaw) { + .onChange(of: appearanceModeRaw) { _ in updateAppearance() } } label: { @@ -79,21 +79,7 @@ struct MenuBarLabel: View { } private func flagForDestination(_ destination: String) -> String { - switch destination.lowercased() { - case "mexico": return "πŸ‡²πŸ‡½" - case "cayman islands": return "πŸ‡°πŸ‡Ύ" - case "canada": return "πŸ‡¨πŸ‡¦" - case "hawaii": return "πŸ‡ΊπŸ‡Έ" - case "united kingdom": return "πŸ‡¬πŸ‡§" - case "argentina": return "πŸ‡¦πŸ‡·" - case "switzerland": return "πŸ‡¨πŸ‡­" - case "japan": return "πŸ‡―πŸ‡΅" - case "china": return "πŸ‡¨πŸ‡³" - case "uae": return "πŸ‡¦πŸ‡ͺ" - case "south africa": return "πŸ‡ΏπŸ‡¦" - case "torn": return "πŸ‡ΊπŸ‡Έ" - default: return "🌍" - } + TornDestination.flag(for: destination) } private func formatShortTime(_ seconds: Int) -> String { diff --git a/MacTorn/MacTorn/Models/TornModels.swift b/MacTorn/MacTorn/Models/TornModels.swift index 13e2b3c..d988faf 100644 --- a/MacTorn/MacTorn/Models/TornModels.swift +++ b/MacTorn/MacTorn/Models/TornModels.swift @@ -1,9 +1,13 @@ import Foundation import SwiftUI +import os.log + +private let logger = Logger(subsystem: TornConstants.logSubsystem, category: "TornModels") // MARK: - Constants enum TornConstants { static let developerID = 2362436 + static let logSubsystem = "com.mactorn" } // MARK: - Root Response @@ -204,6 +208,14 @@ enum TornDestination: String, CaseIterable, Identifiable { var travelAgencyURL: URL { URL(string: "https://www.torn.com/travelagency.php")! } + + /// Look up flag emoji for a destination string, including non-enum values like "Torn" + static func flag(for destination: String) -> String { + if let known = TornDestination(rawValue: destination) { + return known.flag + } + return destination.lowercased() == "torn" ? "πŸ‡ΊπŸ‡Έ" : "🌍" + } } // MARK: - Travel Notification Settings @@ -314,7 +326,7 @@ struct MoneyData: Codable { case cash = "money_onhand" case vault = "vault_amount" case points - case tokens = "company_funds" + case tokens = "donator" case cayman = "cayman_bank" } @@ -664,7 +676,7 @@ class UpdateManager { } } catch { - print("Update check failed: \(error)") + logger.warning("Update check failed: \(error.localizedDescription)") } return nil diff --git a/MacTorn/MacTorn/Utilities/LaunchAtLoginManager.swift b/MacTorn/MacTorn/Utilities/LaunchAtLoginManager.swift index 270c800..79bce10 100644 --- a/MacTorn/MacTorn/Utilities/LaunchAtLoginManager.swift +++ b/MacTorn/MacTorn/Utilities/LaunchAtLoginManager.swift @@ -1,10 +1,14 @@ import Foundation import ServiceManagement +import os.log + +private let logger = Logger(subsystem: TornConstants.logSubsystem, category: "LaunchAtLoginManager") @MainActor class LaunchAtLoginManager: ObservableObject { @Published var isEnabled: Bool = false - + @Published var errorMessage: String? + private let service = SMAppService.mainApp init() { @@ -22,9 +26,11 @@ class LaunchAtLoginManager: ObservableObject { } else { try service.register() } + errorMessage = nil updateStatus() } catch { - print("Launch at Login error: \(error)") + logger.error("Launch at Login error: \(error.localizedDescription)") + errorMessage = error.localizedDescription } } } diff --git a/MacTorn/MacTorn/Utilities/NetworkMonitor.swift b/MacTorn/MacTorn/Utilities/NetworkMonitor.swift new file mode 100644 index 0000000..efbe6e5 --- /dev/null +++ b/MacTorn/MacTorn/Utilities/NetworkMonitor.swift @@ -0,0 +1,31 @@ +import Foundation +import Network + +/// Protocol for network connectivity checks, enabling dependency injection for testing +@MainActor +protocol NetworkConnectivity: AnyObject { + var isConnected: Bool { get } +} + +@MainActor +final class NetworkMonitor: NetworkConnectivity { + static let shared = NetworkMonitor() + + private(set) var isConnected: Bool = true + + private let monitor = NWPathMonitor() + private let queue = DispatchQueue(label: "com.mactorn.networkmonitor") + + private init() { + monitor.pathUpdateHandler = { [weak self] path in + Task { @MainActor in + self?.isConnected = path.status == .satisfied + } + } + monitor.start(queue: queue) + } + + deinit { + monitor.cancel() + } +} diff --git a/MacTorn/MacTorn/Utilities/NotificationManager.swift b/MacTorn/MacTorn/Utilities/NotificationManager.swift index fb65d68..516e963 100644 --- a/MacTorn/MacTorn/Utilities/NotificationManager.swift +++ b/MacTorn/MacTorn/Utilities/NotificationManager.swift @@ -1,6 +1,9 @@ import Foundation import UserNotifications import AppKit +import os.log + +private let logger = Logger(subsystem: TornConstants.logSubsystem, category: "NotificationManager") enum NotificationType: String { case drugReady @@ -48,10 +51,10 @@ class NotificationManager: NSObject, UNUserNotificationCenterDelegate { let granted = try await UNUserNotificationCenter.current() .requestAuthorization(options: [.alert, .sound, .badge]) if granted { - print("Notification permission granted") + logger.info("Notification permission granted") } } catch { - print("Notification permission error: \(error)") + logger.error("Notification permission error: \(error.localizedDescription)") } } @@ -70,7 +73,7 @@ class NotificationManager: NSObject, UNUserNotificationCenterDelegate { UNUserNotificationCenter.current().add(request) { error in if let error = error { - print("Notification error: \(error)") + logger.error("Notification error: \(error.localizedDescription)") } } } @@ -94,9 +97,9 @@ class NotificationManager: NSObject, UNUserNotificationCenterDelegate { UNUserNotificationCenter.current().add(request) { error in if let error = error { - print("Scheduled notification error: \(error)") + logger.error("Scheduled notification error: \(error.localizedDescription)") } else { - print("Scheduled notification '\(identifier)' for \(date)") + logger.info("Scheduled notification '\(identifier)' for \(date)") } } } diff --git a/MacTorn/MacTorn/ViewModels/AppState.swift b/MacTorn/MacTorn/ViewModels/AppState.swift index 0c0bb81..6c7958c 100644 --- a/MacTorn/MacTorn/ViewModels/AppState.swift +++ b/MacTorn/MacTorn/ViewModels/AppState.swift @@ -3,7 +3,7 @@ import Combine import SwiftUI import os.log -private let logger = Logger(subsystem: "com.mactorn", category: "AppState") +private let logger = Logger(subsystem: TornConstants.logSubsystem, category: "AppState") // MARK: - Appearance enum AppearanceMode: String, CaseIterable { @@ -65,6 +65,7 @@ class AppState: ObservableObject { // MARK: - Networking (Dependency Injection for Testing) private let session: NetworkSession + private let connectivity: NetworkConnectivity // MARK: - State Comparison private var previousBars: Bars? @@ -73,11 +74,16 @@ class AppState: ObservableObject { private var previousChain: Chain? private var previousStatus: Status? + // MARK: - Task Handles (for deduplication) + private var fetchTask: Task? + private var watchlistTask: Task? + // MARK: - Timer private var timerCancellable: AnyCancellable? - init(session: NetworkSession = URLSession.shared) { + init(session: NetworkSession = URLSession.shared, connectivity: NetworkConnectivity = NetworkMonitor.shared) { self.session = session + self.connectivity = connectivity loadNotificationRules() loadTravelNotificationSettings() loadWatchlist() @@ -237,18 +243,23 @@ class AppState: ObservableObject { } func refreshWatchlistPrices() { - Task { + watchlistTask?.cancel() + watchlistTask = Task { await fetchWatchlistPrices() } } private func fetchWatchlistPrices() async { - for item in watchlistItems { - await fetchItemPrice(itemId: item.id) + guard connectivity.isConnected else { return } + await withTaskGroup(of: Void.self) { group in + for item in watchlistItems { + group.addTask { await self.fetchItemPrice(itemId: item.id, save: false) } + } } + saveWatchlist() } - private func fetchItemPrice(itemId: Int) async { + private func fetchItemPrice(itemId: Int, save: Bool = true) async { guard !apiKey.isEmpty, let url = TornAPI.marketURL(itemId: itemId, apiKey: apiKey) else { return } @@ -261,21 +272,21 @@ class AppState: ObservableObject { if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 200 { logger.error("Item \(itemId) HTTP Error: \(httpResponse.statusCode)") - await updateItemError(itemId: itemId, error: "HTTP \(httpResponse.statusCode)") + await updateItemError(itemId: itemId, error: "HTTP \(httpResponse.statusCode)", save: save) return } - + if let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] { // Check if API returned error if let error = json["error"] as? [String: Any], let errorText = error["error"] as? String { logger.warning("Item \(itemId) API error: \(errorText)") - await updateItemError(itemId: itemId, error: errorText) + await updateItemError(itemId: itemId, error: errorText, save: save) return } - + var allListings: [(price: Int, amount: Int)] = [] - + // Check itemmarket v2 structure if let itemmarket = json["itemmarket"] as? [String: Any], let listings = itemmarket["listings"] as? [[String: Any]] { @@ -284,7 +295,7 @@ class AppState: ObservableObject { return (p, dict["amount"] as? Int ?? 1) } allListings.append(contentsOf: mapped) - } + } // Fallback for v1 else if let itemmarketArr = json["itemmarket"] as? [[String: Any]] { let mapped = itemmarketArr.compactMap { dict -> (Int, Int)? in @@ -293,7 +304,7 @@ class AppState: ObservableObject { } allListings.append(contentsOf: mapped) } - + // Check bazaar if let bazaarArr = json["bazaar"] as? [[String: Any]] { let mapped = bazaarArr.compactMap { dict -> (Int, Int)? in @@ -302,28 +313,29 @@ class AppState: ObservableObject { } allListings.append(contentsOf: mapped) } - + let sortedListings = allListings.sorted { $0.price < $1.price } logger.debug("Item \(itemId): found \(sortedListings.count) listings, lowest: \(sortedListings.first?.price ?? 0)") if let best = sortedListings.first { let secondPrice = sortedListings.count > 1 ? sortedListings[1].price : 0 - await updateItemPrice(itemId: itemId, lowestPrice: best.price, lowestPriceQuantity: best.amount, secondLowestPrice: secondPrice) + await updateItemPrice(itemId: itemId, lowestPrice: best.price, lowestPriceQuantity: best.amount, secondLowestPrice: secondPrice, save: save) } else { - await updateItemError(itemId: itemId, error: "No listings") + await updateItemError(itemId: itemId, error: "No listings", save: save) } } else { logger.error("Item \(itemId): failed to parse JSON response") - await updateItemError(itemId: itemId, error: "Parse Error") + await updateItemError(itemId: itemId, error: "Parse Error", save: save) } } catch { + if Task.isCancelled { return } logger.error("Item \(itemId) price fetch error: \(error.localizedDescription)") - await updateItemError(itemId: itemId, error: "Network Error") + await updateItemError(itemId: itemId, error: "Network Error", save: save) } } @MainActor - private func updateItemPrice(itemId: Int, lowestPrice: Int, lowestPriceQuantity: Int, secondLowestPrice: Int) { + private func updateItemPrice(itemId: Int, lowestPrice: Int, lowestPriceQuantity: Int, secondLowestPrice: Int, save: Bool = true) { if let index = watchlistItems.firstIndex(where: { $0.id == itemId }) { var item = watchlistItems[index] item.lowestPrice = lowestPrice @@ -332,17 +344,17 @@ class AppState: ObservableObject { item.lastUpdated = Date() item.error = nil watchlistItems[index] = item - saveWatchlist() + if save { saveWatchlist() } } } @MainActor - private func updateItemError(itemId: Int, error: String) { + private func updateItemError(itemId: Int, error: String, save: Bool = true) { if let index = watchlistItems.firstIndex(where: { $0.id == itemId }) { var item = watchlistItems[index] item.error = error watchlistItems[index] = item - saveWatchlist() + if save { saveWatchlist() } } } @@ -363,11 +375,19 @@ class AppState: ObservableObject { } func refreshNow() { - fetchData() + startPolling() } // MARK: - Fetch Data func fetchData() { + guard connectivity.isConnected else { + if errorMsg != "No internet connection" { + errorMsg = "No internet connection" + logger.warning("Fetch aborted: No internet connection") + } + return + } + guard !apiKey.isEmpty else { errorMsg = "API Key required" logger.warning("Fetch aborted: API Key required") @@ -385,7 +405,8 @@ class AppState: ObservableObject { logger.info("Starting data fetch from: \(url.absoluteString.prefix(80))...") - Task { + fetchTask?.cancel() + fetchTask = Task { let startTime = Date() // Ensure minimum loading time for UX, then set isLoading = false @@ -416,23 +437,13 @@ class AppState: ObservableObject { case 200: // Log raw JSON for debugging (first 500 chars) if let jsonString = String(data: data, encoding: .utf8) { - logger.info("Raw API response: \(jsonString.prefix(500))") + logger.debug("Raw API response: \(jsonString.prefix(500))") } - // Check for Torn API error in response (API returns 200 even on errors) - if let tornError = checkForTornAPIError(data: data) { - await MainActor.run { - self.errorMsg = tornError - } - logger.error("Torn API error: \(tornError)") - return - } - - // Parse on background thread + // Parse user data and fetch faction data in parallel + async let factionResult: Void = self.fetchFactionData() try await parseDataInBackground(data: data) - - // Fetch faction data separately - await fetchFactionData() + await factionResult logger.info("Data fetch completed successfully") @@ -449,6 +460,7 @@ class AppState: ObservableObject { logger.error("HTTP Error: \(httpResponse.statusCode)") } } catch { + if Task.isCancelled { return } await MainActor.run { self.errorMsg = "Network error: \(error.localizedDescription)" } @@ -457,19 +469,6 @@ class AppState: ObservableObject { } } - /// Check if Torn API returned an error (API returns HTTP 200 even on errors like rate limiting) - private func checkForTornAPIError(data: Data) -> String? { - guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], - let error = json["error"] as? [String: Any], - let errorMessage = error["error"] as? String else { - return nil - } - - let errorCode = error["code"] as? Int ?? 0 - logger.warning("Torn API error code \(errorCode): \(errorMessage)") - return "API Error: \(errorMessage)" - } - // Move parsing logic here and mark as non-isolated or detached private func parseDataInBackground(data: Data) async throws { // Run CPU-heavy parsing detached from MainActor @@ -477,7 +476,13 @@ class AppState: ObservableObject { guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return (nil, nil, nil, nil, nil, "Failed to parse response") } - + + // Check for Torn API error (API returns HTTP 200 even on errors like rate limiting) + if let apiError = json["error"] as? [String: Any], + let errorMessage = apiError["error"] as? String { + return (nil, nil, nil, nil, nil, "API Error: \(errorMessage)") + } + // Attempt to decode TornResponse first let decodedTornResponse = try? JSONDecoder().decode(TornResponse.self, from: data) @@ -545,7 +550,7 @@ class AppState: ObservableObject { // Check for parse errors if let parseError = result.5, result.0 == nil { self.errorMsg = parseError - logger.error("Parse error: \(parseError)") + logger.error("Data error: \(parseError)") self.lastUpdated = Date() // Still update timestamp on error return } @@ -597,15 +602,14 @@ class AppState: ObservableObject { request.cachePolicy = .reloadIgnoringLocalAndRemoteCacheData let (data, _) = try await session.data(for: request) - // Check for Torn API error - if let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], - let error = json["error"] as? [String: Any], - let errorMessage = error["error"] as? String { - logger.warning("Faction API error: \(errorMessage)") - return - } - if let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] { + // Check for Torn API error first + if let error = json["error"] as? [String: Any], + let errorMessage = error["error"] as? String { + logger.warning("Faction API error: \(errorMessage)") + return + } + let name = json["name"] as? String ?? "" let factionId = json["ID"] as? Int ?? 0 let respect = json["respect"] as? Int ?? 0 diff --git a/MacTorn/MacTorn/Views/SettingsView.swift b/MacTorn/MacTorn/Views/SettingsView.swift index af1042e..a9c32b5 100644 --- a/MacTorn/MacTorn/Views/SettingsView.swift +++ b/MacTorn/MacTorn/Views/SettingsView.swift @@ -73,7 +73,7 @@ struct SettingsView: View { Text("2m").tag(120) } .pickerStyle(.segmented) - .onChange(of: appState.refreshInterval) { + .onChange(of: appState.refreshInterval) { _ in Task { @MainActor in appState.startPolling() } @@ -232,6 +232,16 @@ struct SettingsView: View { inputKey = appState.apiKey refreshAvailableBrowsers() } + .alert("Launch at Login Error", isPresented: Binding( + get: { appState.launchAtLogin.errorMessage != nil }, + set: { if !$0 { appState.launchAtLogin.errorMessage = nil } } + )) { + Button("OK") { + appState.launchAtLogin.errorMessage = nil + } + } message: { + Text(appState.launchAtLogin.errorMessage ?? "An unknown error occurred.") + } } private func refreshAvailableBrowsers() { diff --git a/MacTorn/MacTornTests/Fixtures/TornAPIFixtures.swift b/MacTorn/MacTornTests/Fixtures/TornAPIFixtures.swift index 7bf2ce7..9747012 100644 --- a/MacTorn/MacTornTests/Fixtures/TornAPIFixtures.swift +++ b/MacTorn/MacTornTests/Fixtures/TornAPIFixtures.swift @@ -201,7 +201,7 @@ enum TornAPIFixtures { "money_onhand": 1000000, "vault_amount": 50000000, "points": 5000, - "company_funds": 100, + "donator": 100, "cayman_bank": 100000000 ] diff --git a/MacTorn/MacTornTests/Models/MoneyDataTests.swift b/MacTorn/MacTornTests/Models/MoneyDataTests.swift index 6b6a460..519b333 100644 --- a/MacTorn/MacTornTests/Models/MoneyDataTests.swift +++ b/MacTorn/MacTornTests/Models/MoneyDataTests.swift @@ -73,7 +73,7 @@ final class MoneyDataTests: XCTestCase { "money_onhand": 999999999999, "vault_amount": 9999999999999, "points": 100000, - "company_funds": 50000, + "donator": 50000, "cayman_bank": 99999999999999 ] let data = try JSONSerialization.data(withJSONObject: json) diff --git a/MacTorn/MacTornUITests/MacTornUITests.swift b/MacTorn/MacTornUITests/MacTornUITests.swift index cc40598..eec9562 100644 --- a/MacTorn/MacTornUITests/MacTornUITests.swift +++ b/MacTorn/MacTornUITests/MacTornUITests.swift @@ -17,111 +17,6 @@ final class MacTornUITests: XCTestCase { app = nil } - // MARK: - Settings View Tests - - func testSettingsView_appearsWhenNoAPIKey() throws { - // When no API key is set, settings should appear - // Note: This test assumes a clean state or --uitesting launch argument clears the key - - // Look for settings-related UI elements - let settingsElements = app.windows.firstMatch - - // The app should show some form of settings or API key input - // Adjust selectors based on actual UI implementation - XCTAssertTrue(settingsElements.exists) - } - - // MARK: - Tab Navigation Tests - - func testTabNavigation_allTabsExist() throws { - // Skip if settings view is blocking - // This test verifies the main tab structure - - let window = app.windows.firstMatch - XCTAssertTrue(window.exists) - - // Look for tab bar or navigation elements - // Adjust based on actual UI implementation - } - - func testTabNavigation_switchBetweenTabs() throws { - let window = app.windows.firstMatch - - // Tab navigation test - // Adjust selectors based on actual tab implementation - // e.g., app.buttons["Status"].tap() - - XCTAssertTrue(window.exists) - } - - // MARK: - Refresh Button Tests - - func testRefreshButton_exists() throws { - let window = app.windows.firstMatch - - // Look for refresh button - // Adjust selector based on actual implementation - // let refreshButton = window.buttons["Refresh"] - - XCTAssertTrue(window.exists) - } - - func testRefreshButton_triggersRefresh() throws { - // This test would verify that tapping refresh triggers a data fetch - // Would need to mock network or observe state changes - - let window = app.windows.firstMatch - XCTAssertTrue(window.exists) - } - - // MARK: - Watchlist UI Tests - - func testWatchlist_addItem() throws { - // Navigate to watchlist tab - // Add an item - // Verify it appears in the list - - let window = app.windows.firstMatch - XCTAssertTrue(window.exists) - } - - func testWatchlist_removeItem() throws { - // Navigate to watchlist tab - // Remove an item - // Verify it's no longer in the list - - let window = app.windows.firstMatch - XCTAssertTrue(window.exists) - } - - // MARK: - Error State Tests - - func testErrorState_displaysErrorMessage() throws { - // Trigger an error state - // Verify error message is displayed - - let window = app.windows.firstMatch - XCTAssertTrue(window.exists) - } - - // MARK: - Loading State Tests - - func testLoadingState_showsLoadingIndicator() throws { - // During data fetch, loading indicator should be visible - - let window = app.windows.firstMatch - XCTAssertTrue(window.exists) - } - - // MARK: - Status View Tests - - func testStatusView_displaysBars() throws { - // Verify energy, nerve, life, happy bars are displayed - - let window = app.windows.firstMatch - XCTAssertTrue(window.exists) - } - // MARK: - Performance Tests func testLaunchPerformance() throws {