diff --git a/TemplateApplication.xcodeproj/project.pbxproj b/TemplateApplication.xcodeproj/project.pbxproj index 7f88aa7a..dc0d26cd 100644 --- a/TemplateApplication.xcodeproj/project.pbxproj +++ b/TemplateApplication.xcodeproj/project.pbxproj @@ -53,6 +53,13 @@ 5680DD3E2AB8CD84004E6D4A /* ContributionsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5680DD3D2AB8CD84004E6D4A /* ContributionsTest.swift */; }; 56E708352BB06B7100B08F0A /* SpeziLicense in Frameworks */ = {isa = PBXBuildFile; productRef = 56E708342BB06B7100B08F0A /* SpeziLicense */; }; 56E7083B2BB06F6F00B08F0A /* SwiftPackageList in Frameworks */ = {isa = PBXBuildFile; productRef = 56E7083A2BB06F6F00B08F0A /* SwiftPackageList */; }; + 63A315532CE14A9300310EF5 /* LogViewerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63A315522CE14A8E00310EF5 /* LogViewerTests.swift */; }; + 63A315562CE14C1000310EF5 /* LogManagerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63A315552CE14C0900310EF5 /* LogManagerError.swift */; }; + 63E851782CE0FFCB005554E7 /* OSLogEntryLog+FormattedLogOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E851772CE0FFC5005554E7 /* OSLogEntryLog+FormattedLogOutput.swift */; }; + 63E8517A2CE0FFDE005554E7 /* LogLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E851792CE0FFDC005554E7 /* LogLevel.swift */; }; + 63E8517C2CE0FFF9005554E7 /* LogManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E8517B2CE0FFF6005554E7 /* LogManager.swift */; }; + 63E8517E2CE10007005554E7 /* LogViewer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E8517D2CE10005005554E7 /* LogViewer.swift */; }; + 63E851802CE10016005554E7 /* LogsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E8517F2CE10014005554E7 /* LogsListView.swift */; }; 653A2551283387FE005D4D48 /* TemplateApplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 653A2550283387FE005D4D48 /* TemplateApplication.swift */; }; 653A255528338800005D4D48 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 653A255428338800005D4D48 /* Assets.xcassets */; }; 653A256228338800005D4D48 /* TemplateApplicationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 653A256128338800005D4D48 /* TemplateApplicationTests.swift */; }; @@ -118,6 +125,13 @@ 2FE5DCAC29EE6107004B9AB4 /* AccountOnboarding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountOnboarding.swift; sourceTree = ""; }; 2FF53D8C2A8729D600042B76 /* TemplateApplicationStandard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TemplateApplicationStandard.swift; sourceTree = ""; }; 5680DD3D2AB8CD84004E6D4A /* ContributionsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContributionsTest.swift; sourceTree = ""; }; + 63A315522CE14A8E00310EF5 /* LogViewerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogViewerTests.swift; sourceTree = ""; }; + 63A315552CE14C0900310EF5 /* LogManagerError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogManagerError.swift; sourceTree = ""; }; + 63E851772CE0FFC5005554E7 /* OSLogEntryLog+FormattedLogOutput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OSLogEntryLog+FormattedLogOutput.swift"; sourceTree = ""; }; + 63E851792CE0FFDC005554E7 /* LogLevel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogLevel.swift; sourceTree = ""; }; + 63E8517B2CE0FFF6005554E7 /* LogManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogManager.swift; sourceTree = ""; }; + 63E8517D2CE10005005554E7 /* LogViewer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogViewer.swift; sourceTree = ""; }; + 63E8517F2CE10014005554E7 /* LogsListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogsListView.swift; sourceTree = ""; }; 653A254D283387FE005D4D48 /* TemplateApplication.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TemplateApplication.app; sourceTree = BUILT_PRODUCTS_DIR; }; 653A2550283387FE005D4D48 /* TemplateApplication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateApplication.swift; sourceTree = ""; }; 653A255428338800005D4D48 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -251,6 +265,19 @@ path = SharedContext; sourceTree = ""; }; + 63E851762CE0FF85005554E7 /* Logging */ = { + isa = PBXGroup; + children = ( + 63A315552CE14C0900310EF5 /* LogManagerError.swift */, + 63E8517F2CE10014005554E7 /* LogsListView.swift */, + 63E8517D2CE10005005554E7 /* LogViewer.swift */, + 63E8517B2CE0FFF6005554E7 /* LogManager.swift */, + 63E851792CE0FFDC005554E7 /* LogLevel.swift */, + 63E851772CE0FFC5005554E7 /* OSLogEntryLog+FormattedLogOutput.swift */, + ); + path = Logging; + sourceTree = ""; + }; 653A2544283387FE005D4D48 = { isa = PBXGroup; children = ( @@ -287,6 +314,7 @@ 2FE5DC2829EDD398004B9AB4 /* Onboarding */, 2FE5DC2D29EDD792004B9AB4 /* Resources */, 2FE5DC3B29EDD7D0004B9AB4 /* Schedule */, + 63E851762CE0FF85005554E7 /* Logging */, 2FE5DC3C29EDD7DA004B9AB4 /* SharedContext */, 2FC9759D2978E30800BA99FE /* Supporting Files */, ); @@ -304,6 +332,7 @@ 653A256A28338800005D4D48 /* TemplateApplicationUITests */ = { isa = PBXGroup; children = ( + 63A315522CE14A8E00310EF5 /* LogViewerTests.swift */, 2F4E237D2989A2FE0013F3D9 /* OnboardingTests.swift */, 653A256B28338800005D4D48 /* SchedulerTests.swift */, 2F4E23862989DB360013F3D9 /* ContactsTests.swift */, @@ -528,15 +557,20 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 63E8517C2CE0FFF9005554E7 /* LogManager.swift in Sources */, 2FE5DC4129EDD7EE004B9AB4 /* StorageKeys.swift in Sources */, 2FE5DCB129EE6107004B9AB4 /* AccountOnboarding.swift in Sources */, 2FE5DC3A29EDD7CA004B9AB4 /* Welcome.swift in Sources */, 2FE5DC3829EDD7CA004B9AB4 /* InterestingModules.swift in Sources */, 2FE5DC3529EDD7CA004B9AB4 /* Consent.swift in Sources */, 2FC975A82978F11A00BA99FE /* HomeView.swift in Sources */, + 63E851782CE0FFCB005554E7 /* OSLogEntryLog+FormattedLogOutput.swift in Sources */, 2FE5DC4E29EDD7FA004B9AB4 /* ScheduleView.swift in Sources */, + 63A315562CE14C1000310EF5 /* LogManagerError.swift in Sources */, + 63E851802CE10016005554E7 /* LogsListView.swift in Sources */, A9DFE8A92ABE551400428242 /* AccountButton.swift in Sources */, A9A3DCC82C75CBBD00FC9B69 /* FirebaseConfiguration.swift in Sources */, + 63E8517A2CE0FFDE005554E7 /* LogLevel.swift in Sources */, 2FE5DC3729EDD7CA004B9AB4 /* OnboardingFlow.swift in Sources */, 2F1AC9DF2B4E840E00C24973 /* TemplateApplication.docc in Sources */, 2FF53D8D2A8729D600042B76 /* TemplateApplicationStandard.swift in Sources */, @@ -551,6 +585,7 @@ 653A2551283387FE005D4D48 /* TemplateApplication.swift in Sources */, 2FE5DC3629EDD7CA004B9AB4 /* HealthKitPermissions.swift in Sources */, 2F65B44E2A3B8B0600A36932 /* NotificationPermissions.swift in Sources */, + 63E8517E2CE10007005554E7 /* LogViewer.swift in Sources */, 2FE5DC2629EDD38A004B9AB4 /* Contacts.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -568,6 +603,7 @@ buildActionMask = 2147483647; files = ( 5680DD3E2AB8CD84004E6D4A /* ContributionsTest.swift in Sources */, + 63A315532CE14A9300310EF5 /* LogViewerTests.swift in Sources */, 2F4E23872989DB360013F3D9 /* ContactsTests.swift in Sources */, 2F4E237E2989A2FE0013F3D9 /* OnboardingTests.swift in Sources */, 653A256C28338800005D4D48 /* SchedulerTests.swift in Sources */, diff --git a/TemplateApplication/Account/AccountSheet.swift b/TemplateApplication/Account/AccountSheet.swift index bdb0f8c5..4ec2f00e 100644 --- a/TemplateApplication/Account/AccountSheet.swift +++ b/TemplateApplication/Account/AccountSheet.swift @@ -32,6 +32,12 @@ struct AccountSheet: View { } label: { Text("License Information") } + + NavigationLink { + LogViewer() + } label: { + Text("View Logs") + } } } else { AccountSetup { _ in diff --git a/TemplateApplication/Logging/LogLevel.swift b/TemplateApplication/Logging/LogLevel.swift new file mode 100644 index 00000000..51e0b7d1 --- /dev/null +++ b/TemplateApplication/Logging/LogLevel.swift @@ -0,0 +1,76 @@ +// +// This source file is part of the Stanford Spezi Template Application open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import OSLog +import SwiftUI + + +enum LogLevel: String, CaseIterable, Identifiable { + case all = "All" + case info = "Info" + case debug = "Debug" + case error = "Error" + case fault = "Fault" + case notice = "Notice" + case undefined = "Undefined" + + var id: String { self.rawValue } + + var osLogLevel: OSLogEntryLog.Level? { + switch self { + case .all: + return nil + case .info: + return .info + case .debug: + return .debug + case .error: + return .error + case .fault: + return .fault + case .notice: + return .notice + case .undefined: + return .undefined + } + } + + var color: Color { + switch self { + case .info: + return .blue + case .debug: + return .green + case .error: + return .red + case .fault: + return .purple + case .notice: + return .orange + case .all, .undefined: + return .gray + } + } + + init(from osLogLevel: OSLogEntryLog.Level) { + switch osLogLevel { + case .info: + self = .info + case .debug: + self = .debug + case .error: + self = .error + case .fault: + self = .fault + case .notice: + self = .notice + @unknown default: + self = .undefined + } + } +} diff --git a/TemplateApplication/Logging/LogManager.swift b/TemplateApplication/Logging/LogManager.swift new file mode 100644 index 00000000..eb2d8d32 --- /dev/null +++ b/TemplateApplication/Logging/LogManager.swift @@ -0,0 +1,75 @@ +// +// This source file is part of the Stanford Spezi Template Application open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import OSLog +import Spezi +import SwiftUI + +/// Manages log entries within the application using `OSLogStore`, allowing querying +/// based on date ranges and log levels. +class LogManager { + /// Reference to the `OSLogStore`, which provides access to system logs. + private let store: OSLogStore? + + /// Initializes the `LogManager` and attempts to set up the `OSLogStore` with + /// a scope limited to the current process identifier. + /// + /// - Throws: An error if the `OSLogStore` cannot be initialized. + init() throws { + do { + self.store = try OSLogStore(scope: .currentProcessIdentifier) + } catch { + throw error + } + } + + /// Queries logs within a specified date range and optional log level. + /// + /// - Parameters: + /// - startDate: The start date from which logs should be queried. + /// - endDate: An optional end date up to which logs should be queried. + /// - logLevel: An optional log level filter, returning only entries of this level if specified. + /// - Returns: An array of `OSLogEntryLog` entries that match the specified criteria. + /// - Throws: `LogManagerError.invalidLogStore` if `OSLogStore` is unavailable, or + /// `LogManagerError.invalidBundleIdentifier` if the bundle identifier cannot be retrieved. + func query( + startDate: Date, + endDate: Date? = nil, + logLevel: OSLogEntryLog.Level? = nil + ) throws -> [OSLogEntryLog] { + guard let store else { + throw LogManagerError.invalidLogStore + } + + guard let bundleIdentifier = Bundle.main.bundleIdentifier else { + throw LogManagerError.invalidBundleIdentifier + } + + let position = store.position(date: startDate) + let predicate = NSPredicate(format: "subsystem == %@", bundleIdentifier) + let logs = try store.getEntries(at: position, matching: predicate) + .reversed() + .compactMap { $0 as? OSLogEntryLog } + + return logs + .filter { logEntry in + /// Filter by log type if specified + if let logLevel, logEntry.level != logLevel { + return false + } + + /// Filter by end date if specified + if let endDate, logEntry.date > endDate { + return false + } + + return true + } + } +} diff --git a/TemplateApplication/Logging/LogManagerError.swift b/TemplateApplication/Logging/LogManagerError.swift new file mode 100644 index 00000000..3fd38a6a --- /dev/null +++ b/TemplateApplication/Logging/LogManagerError.swift @@ -0,0 +1,25 @@ +// +// This source file is part of the Stanford Spezi Template Application open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University +// +// SPDX-License-Identifier: MIT +// + +enum LogManagerError: Error { + /// Throw when the log store is invalid + case invalidLogStore + /// Throw when the bundle identifier is invalid + case invalidBundleIdentifier +} + +extension LogManagerError: CustomStringConvertible { + public var description: String { + switch self { + case .invalidLogStore: + return "The OSLogStore is invalid." + case .invalidBundleIdentifier: + return "The bundle identifier is invalid." + } + } +} diff --git a/TemplateApplication/Logging/LogViewer.swift b/TemplateApplication/Logging/LogViewer.swift new file mode 100644 index 00000000..744391d4 --- /dev/null +++ b/TemplateApplication/Logging/LogViewer.swift @@ -0,0 +1,155 @@ +// +// This source file is part of the Stanford Spezi Template Application open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import OSLog +import Spezi +import SwiftUI + +/// A SwiftUI view that displays logs retrieved from `LogManager`. +/// Allows users to filter logs by date range and log level, view them in a list, and share the output. +struct LogViewer: View { + private let manager: LogManager? + + @State private var startDate: Date = Calendar.current.date(byAdding: .day, value: -1, to: Date()) ?? Date() + @State private var endDate = Date() + @State private var selectedLogLevel: LogLevel = .all + @State private var logs: [OSLogEntryLog] = [] + @State private var isLoading = false + @State private var queryTask: Task? + @State private var showingAlert = false + @State private var errorMessage = "" + @State private var searchText = "" + + private var filteredLogs: [OSLogEntryLog] { + if searchText.isEmpty { + return logs + } else { + return logs.filter { $0.composedMessage.contains(searchText) } + } + } + + var body: some View { + VStack { + VStack { + DatePicker("LOGS_FROM_DATE_LABEL", selection: $startDate, displayedComponents: [.date, .hourAndMinute]) + DatePicker("LOGS_TO_DATE_LABEL", selection: $endDate, displayedComponents: [.date, .hourAndMinute]) + HStack { + Text("LOGS_LEVEL_LABEL") + Spacer() + Picker("LOGS_LEVEL_LABEL", selection: $selectedLogLevel) { + ForEach(LogLevel.allCases) { level in + Text(level.rawValue).tag(level) + } + } + } + } + .padding() + + if isLoading { + Spacer() + ProgressView("LOGS_LOADING_LABEL").padding() + Spacer() + } else { + LogsListView(logs: filteredLogs) + } + + Spacer() + } + .navigationTitle("LOGS_VIEWER_TITLE") + .onAppear { + queryLogs() + } + .onChange(of: startDate) { + queryLogs() + } + .onChange(of: endDate) { + queryLogs() + } + .onChange(of: selectedLogLevel) { + queryLogs() + } + .toolbar { + ToolbarItemGroup(placement: .navigationBarTrailing) { + Button(action: queryLogs) { + Image(systemName: "arrow.clockwise") // swiftlint:disable:this accessibility_label_for_image + } + if !logs.isEmpty { + ShareLink( + item: logs.formattedLogOutput(), + preview: SharePreview( + "LOGS_SHARE_PREVIEW_TITLE", + image: Image(systemName: "doc.text") // swiftlint:disable:this accessibility_label_for_image + ) + ) { + Image(systemName: "square.and.arrow.up") // swiftlint:disable:this accessibility_label_for_image + } + } + } + } + .alert(errorMessage, isPresented: $showingAlert) { + Button("OK", role: .cancel) { } + } + .searchable(text: $searchText) + } + + init() { + do { + self.manager = try LogManager() + } catch { + self.manager = nil + displayError(message: error.localizedDescription) + } + } + + /// Queries logs based on the selected date range and log level. + /// Cancels any existing query, updates `isLoading` state, and performs a new asynchronous query. + private func queryLogs() { + guard let manager else { + return + } + + /// Cancel any existing query task + queryTask?.cancel() + + /// Set loading state + isLoading = true + + /// Create a new query task and store it + queryTask = Task(priority: .userInitiated) { [manager, startDate, endDate, selectedLogLevel] in + do { + /// Run the query + let result = try manager.query( + startDate: startDate, + endDate: endDate, + logLevel: selectedLogLevel.osLogLevel + ) + + /// Check to make sure the task isn't cancelled before updating UI + guard !Task.isCancelled else { + return + } + + /// Update the UI + await MainActor.run { + logs = result + isLoading = false + } + } catch { + displayError(message: error.localizedDescription) + } + } + } + + /// Displays an error message in an alert if a query fails. + /// + /// - Parameter message: The error message to display. + private func displayError(message: String) { + errorMessage = message + showingAlert = true + } +} diff --git a/TemplateApplication/Logging/LogsListView.swift b/TemplateApplication/Logging/LogsListView.swift new file mode 100644 index 00000000..92d772ef --- /dev/null +++ b/TemplateApplication/Logging/LogsListView.swift @@ -0,0 +1,49 @@ +// +// This source file is part of the Stanford Spezi Template Application open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import OSLog +import SwiftUI + + +struct LogsListView: View { + var logs: [OSLogEntryLog] + + var body: some View { + if !logs.isEmpty { + List(logs, id: \.self) { entry in + VStack(alignment: .leading) { + Text(entry.date.formatted()) + .font(.caption) + HStack { + Text(entry.category) + .font(.caption) + .fontWeight(.semibold) + .padding(2) + .background(Color(.systemGray5)) + .cornerRadius(4) + Text(LogLevel(from: entry.level).rawValue) + .font(.caption) + .fontWeight(.semibold) + .padding(2) + .background(LogLevel(from: entry.level).color) + .cornerRadius(4) + } + Text(entry.subsystem) + .font(.caption) + .fontWeight(.semibold) + .padding(2) + .background(Color(.systemGray5)) + .cornerRadius(4) + Text(entry.composedMessage) + } + } + } else { + ContentUnavailableView("NO_LOGS_AVAILABLE", systemImage: "magnifyingglass") + } + } +} diff --git a/TemplateApplication/Logging/OSLogEntryLog+FormattedLogOutput.swift b/TemplateApplication/Logging/OSLogEntryLog+FormattedLogOutput.swift new file mode 100644 index 00000000..bee47e45 --- /dev/null +++ b/TemplateApplication/Logging/OSLogEntryLog+FormattedLogOutput.swift @@ -0,0 +1,19 @@ +// +// This source file is part of the Stanford Spezi Template Application open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import OSLog + + +extension Array where Element == OSLogEntryLog { + func formattedLogOutput() -> String { + self.map { entry in + "[\(entry.date.formatted())] [\(entry.category)] [\(entry.level.rawValue)]: \(entry.composedMessage)" + } + .joined(separator: "\n") + } +} diff --git a/TemplateApplication/Resources/Localizable.xcstrings b/TemplateApplication/Resources/Localizable.xcstrings index 9e828f82..81428e45 100644 --- a/TemplateApplication/Resources/Localizable.xcstrings +++ b/TemplateApplication/Resources/Localizable.xcstrings @@ -231,6 +231,66 @@ } } }, + "LOGS_FROM_DATE_LABEL" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Date From" + } + } + } + }, + "LOGS_LEVEL_LABEL" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Log Level" + } + } + } + }, + "LOGS_LOADING_LABEL" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loading Logsā€¦" + } + } + } + }, + "LOGS_SHARE_PREVIEW_TITLE" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Logs" + } + } + } + }, + "LOGS_TO_DATE_LABEL" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Date To" + } + } + } + }, + "LOGS_VIEWER_TITLE" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Log Viewer" + } + } + } + }, "Next" : { "localizations" : { "en" : { @@ -241,6 +301,16 @@ } } }, + "NO_LOGS_AVAILABLE" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No Logs Available" + } + } + } + }, "NOTIFICATION_PERMISSIONS_DESCRIPTION" : { "localizations" : { "en" : { @@ -261,6 +331,16 @@ } } }, + "OK" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ok" + } + } + } + }, "Onboarding" : { "localizations" : { "en" : { @@ -366,6 +446,16 @@ }, "Unsupported Event" : { + }, + "View Logs" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "View Logs" + } + } + } }, "WELCOME_AREA1_DESCRIPTION" : { "localizations" : { diff --git a/TemplateApplicationUITests/LogViewerTests.swift b/TemplateApplicationUITests/LogViewerTests.swift new file mode 100644 index 00000000..6b1c8771 --- /dev/null +++ b/TemplateApplicationUITests/LogViewerTests.swift @@ -0,0 +1,45 @@ +// +// This source file is part of the Stanford Spezi Template Application open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import OSLog +import XCTest +import XCTestExtensions +import XCTHealthKit +import XCTSpeziAccount +import XCTSpeziNotifications + +class LogViewerTests: XCTestCase { + @MainActor + override func setUp() async throws { + continueAfterFailure = false + + let app = XCUIApplication() + app.launchArguments = ["--skipOnboarding"] + app.launch() + } + + + @MainActor + func testLogViewer() throws { + let app = XCUIApplication() + + XCTAssertTrue(app.wait(for: .runningForeground, timeout: 2.0)) + + sleep(2) + + XCTAssertTrue(app.navigationBars.buttons["Your Account"].waitForExistence(timeout: 6.0)) + app.navigationBars.buttons["Your Account"].tap() + + XCTAssertTrue(app.buttons["View Logs"].waitForExistence(timeout: 2.0)) + app.buttons["View Logs"].tap() + + XCTAssertTrue(app.staticTexts["Log Viewer"].waitForExistence(timeout: 2.0)) + + XCTAssertTrue(app.staticTexts["No Logs Available"].waitForExistence(timeout: 5.0)) + } +}