diff --git a/Stanford360.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Stanford360.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 7dc47d0..28a7a97 100644 --- a/Stanford360.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Stanford360.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,8 +6,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/abseil-cpp-binary.git", "state" : { - "revision" : "194a6706acbd25e4ef639bcaddea16e8758a3e27", - "version" : "1.2024011602.0" + "revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5", + "version" : "1.2024072200.0" } }, { @@ -60,8 +60,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/firebase/firebase-ios-sdk.git", "state" : { - "revision" : "6318278e8e64d21f0fdcc69004395e4d34048aaf", - "version" : "11.8.1" + "revision" : "eb523407e4293568ed55590728205c359cbecc5b", + "version" : "11.9.0" } }, { @@ -69,8 +69,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleAppMeasurement.git", "state" : { - "revision" : "be0881ff728eca210ccb628092af400c086abda3", - "version" : "11.7.0" + "revision" : "d80e25104abe76d69a134d4ec18f011edd8df06c", + "version" : "11.9.0" } }, { @@ -96,8 +96,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/grpc-binary.git", "state" : { - "revision" : "f56d8fc3162de9a498377c7b6cea43431f4f5083", - "version" : "1.65.1" + "revision" : "cc0001a0cf963aa40501d9c2b181e7fc9fd8ec71", + "version" : "1.69.0" } }, { @@ -105,8 +105,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/gtm-session-fetcher.git", "state" : { - "revision" : "3cdb78efb79b4a5383c3911488d8025bfc545b5e", - "version" : "4.3.0" + "revision" : "4d70340d55d7d07cc2fdf8e8125c4c126c1d5f35", + "version" : "4.4.0" } }, { @@ -132,8 +132,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/interop-ios-for-google-sdks.git", "state" : { - "revision" : "2d12673670417654f08f5f90fdd62926dc3a2648", - "version" : "100.0.0" + "revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe", + "version" : "101.0.0" } }, { @@ -240,8 +240,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/Spezi.git", "state" : { - "revision" : "4513a697572e8e1faea1e0ee52e6fad4b8d3dd8d", - "version" : "1.8.0" + "revision" : "49ed1ddcfb7d0f753990b55578c33a887513fb39", + "version" : "1.8.1" } }, { @@ -285,8 +285,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziFoundation.git", "state" : { - "revision" : "c844b98242829fe44e7908739374d4c8b88d6da7", - "version" : "2.1.0" + "revision" : "78f3d912a82f951564d7e133867cf01351f53062", + "version" : "2.1.1" } }, { @@ -321,8 +321,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziNotifications.git", "state" : { - "revision" : "b886f192282a925f600ec5ecbc94acfc75460293", - "version" : "1.0.3" + "revision" : "21427fff437e6bc17db74b125fa43970a2ab9b0b", + "version" : "1.0.5" } }, { @@ -375,8 +375,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziViews.git", "state" : { - "revision" : "80c7cdfd5e50c3e279ab889cc90bbcfc88c4f24c", - "version" : "1.9.0" + "revision" : "76be1e28a88c9a102524aded45f6c76850266110", + "version" : "1.9.1" } }, { @@ -393,8 +393,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-argument-parser.git", "state" : { - "revision" : "41982a3656a71c768319979febd796c6fd111d5c", - "version" : "1.5.0" + "revision" : "0fbc8848e389af3bb55c182bc19ca9d5dc2f255b", + "version" : "1.4.0" } }, { @@ -420,8 +420,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-numerics.git", "state" : { - "revision" : "0a5bc04095a675662cf24757cc0640aa2204253b", - "version" : "1.0.2" + "revision" : "e0ec0f5f3af6f3e4d5e7a19d2af26b481acb6ba8", + "version" : "1.0.3" } }, { @@ -429,8 +429,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/FelixHerrmann/swift-package-list", "state" : { - "revision" : "5e954ec39ce2374ff28a38224fd4e6bba7c57cdc", - "version" : "4.4.2" + "revision" : "b3ee4748096984ea58dade510f63b12027e0de84", + "version" : "4.6.0" } }, { @@ -438,8 +438,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-protobuf.git", "state" : { - "revision" : "ebc7251dd5b37f627c93698e4374084d98409633", - "version" : "1.28.2" + "revision" : "d72aed98f8253ec1aa9ea1141e28150f408cf17f", + "version" : "1.29.0" } }, { @@ -456,8 +456,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/huggingface/swift-transformers", "state" : { - "revision" : "8a83416cc00ab07a5de9991e6ad817a9b8588d20", - "version" : "0.1.15" + "revision" : "be855fac725dbae27264e47a3eb535cc422a4ba8", + "version" : "0.1.18" } }, { @@ -492,8 +492,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordBDHG/XCTestExtensions.git", "state" : { - "revision" : "03eb0646dbceededbbb9d46b289f6eb50a4ec791", - "version" : "1.1.2" + "revision" : "3b44aad358897cf85139b46f32902cd60d9e5cb4", + "version" : "1.2.1" } }, { @@ -519,8 +519,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/jpsim/Yams.git", "state" : { - "revision" : "3036ba9d69cf1fd04d433527bc339dc0dc75433d", - "version" : "5.1.3" + "revision" : "b4b8042411dc7bbb696300a34a4bf3ba1b7ad19b", + "version" : "5.3.1" } } ], diff --git a/Stanford360/Activity/Model/ActivityManager.swift b/Stanford360/Activity/Model/ActivityManager.swift index 7a46fe9..51840ac 100644 --- a/Stanford360/Activity/Model/ActivityManager.swift +++ b/Stanford360/Activity/Model/ActivityManager.swift @@ -86,14 +86,4 @@ class ActivityManager: Module, EnvironmentAccessible { return "Start your activity today and move towards your goal! πŸ’ͺ" } } - - func saveToStorage() { - do { - let data = try JSONEncoder().encode(activities) - UserDefaults.standard.set(data, forKey: "activities") - print("[ActivityManager] [saveToStorage] Successfully saved activities to UserDefaults") - } catch { - print("[ActivityManager] [saveToStorage] Error ❌ : \(error)") - } - } } diff --git a/Stanford360/Activity/Model/HealthKitManager.swift b/Stanford360/Activity/Model/HealthKitManager.swift index 0107e3c..d7c54d5 100644 --- a/Stanford360/Activity/Model/HealthKitManager.swift +++ b/Stanford360/Activity/Model/HealthKitManager.swift @@ -70,11 +70,6 @@ class HealthKitManager: Module, EnvironmentAccessible { ) } -// func saveActivityToHealthKit(_ activity: Activity) async throws { -// let samples = createHealthKitSamples(for: activity) -// try await healthStore.save(samples) -// } - private func fetchSteps(startDate: Date, endDate: Date) async throws -> Int { guard let stepType = HKObjectType.quantityType(forIdentifier: .stepCount) else { throw HealthKitError.fetchingStepsFailed @@ -115,37 +110,7 @@ class HealthKitManager: Module, EnvironmentAccessible { healthStore.execute(query) } } - -// private func createHealthKitSamples(for activity: Activity) -> [HKSample] { -// var samples: [HKSample] = [] -// -// if let stepType = HKObjectType.quantityType(forIdentifier: .stepCount) { -// // Convert activity minutes to steps (assuming moderate pace of 100 steps/minute) -// let estimatedSteps = activity.activeMinutes * 100 -// let stepQuantity = HKQuantity(unit: .count(), doubleValue: Double(estimatedSteps)) -// let stepSample = HKQuantitySample( -// type: stepType, -// quantity: stepQuantity, -// start: activity.date, -// end: activity.date.addingTimeInterval(60 * Double(activity.activeMinutes)) -// ) -// samples.append(stepSample) -// } -// -// if let exerciseType = HKObjectType.quantityType(forIdentifier: .appleExerciseTime) { -// let exerciseQuantity = HKQuantity(unit: .minute(), doubleValue: Double(activity.activeMinutes)) -// let exerciseSample = HKQuantitySample( -// type: exerciseType, -// quantity: exerciseQuantity, -// start: activity.date, -// end: activity.date.addingTimeInterval(60 * Double(activity.activeMinutes)) -// ) -// samples.append(exerciseSample) -// } -// -// return samples -// } -// + /// Converts HealthKit metrics into equivalent active minutes private func calculateActiveMinutes(steps: Int, exerciseMinutes: Int) -> Int { // Convert steps to minutes (assuming 100 steps per minute of activity) @@ -169,7 +134,7 @@ class HealthKitManager: Module, EnvironmentAccessible { date: date, steps: healthKitActivity.steps, activeMinutes: convertedActiveMinutes, - activityType: "HealthKit Import" + activityType: "Walking (HealthKit)" ) } } diff --git a/Stanford360/Activity/View/ActivityButtonView.swift b/Stanford360/Activity/View/ActivityButtonView.swift new file mode 100644 index 0000000..97cbb19 --- /dev/null +++ b/Stanford360/Activity/View/ActivityButtonView.swift @@ -0,0 +1,53 @@ +// +// ActivityButtonView.swift +// Stanford360 +// +// Created by Elsa Bismuth on 13/02/2025. +// +// SPDX-FileCopyrightText: 2025 Stanford University +// +// SPDX-License-Identifier: MIT + +import SwiftUI + +struct ActivityButtonView: View { + let activityName: String + let iconName: String + @Binding var selectedActivity: String + + var body: some View { + VStack(spacing: 6) { + Image(systemName: iconName) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 40, height: 40) + .foregroundColor(.blue) // Change color as needed + .accessibilityLabel(activityName) + + Text(activityName) + .font(.subheadline) + .foregroundColor(.primary) + } + .frame(width: 65, height: 65) + .padding() + .background( + ZStack { + RoundedRectangle(cornerRadius: 12).fill(Color.white) + if selectedActivity == activityName { + RoundedRectangle(cornerRadius: 12) + .stroke(Color.blue, lineWidth: 3) + } + } + ) + .shadow(radius: 2) + .onTapGesture { + selectedActivity = activityName + } + } +} + +#Preview { + @Previewable @State var selectedActivity = "Walking" + + ActivityButtonView(activityName: "Walking", iconName: "figure.walk", selectedActivity: $selectedActivity) +} diff --git a/Stanford360/Activity/View/ActivityControlPanel.swift b/Stanford360/Activity/View/ActivityControlPanel.swift new file mode 100644 index 0000000..2c2f8e0 --- /dev/null +++ b/Stanford360/Activity/View/ActivityControlPanel.swift @@ -0,0 +1,173 @@ +// +// ActivityControlPanel.swift +// Stanford360 +// +// Created by Elsa Bismuth on 11/03/2025. +// +// SPDX-FileCopyrightText: 2025 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import SwiftUI + +struct ActivityControlPanel: View { + @Environment(Stanford360Standard.self) private var standard + @Environment(ActivityManager.self) private var activityManager + @Environment(Stanford360Scheduler.self) var scheduler + + // Activity properties + @State private var activeMinutes: String = "" + @State private var selectedActivity: String = "Walking" + @State private var selectedDate = Date() + @State private var showingDateError = false + @State private var showingSuccessMessage = false + + var body: some View { + VStack(spacing: 15) { + activityPickerSection + dateAndMinutesSection + saveButton + } + .padding(.horizontal) + .padding(.vertical, 10) + .cornerRadius(12) + .shadow(radius: 1) + .alert("Invalid Date", isPresented: $showingDateError) { + Button("OK", role: .cancel) { } + } message: { + Text("Please select a date that isn't in the future.") + } + .alert("Activity Saved", isPresented: $showingSuccessMessage) { + Button("OK", role: .cancel) { } + } message: { + Text("Your activity has been recorded successfully!") + } + } + + private var dateAndMinutesSection: some View { + HStack(spacing: 15) { + // Date picker section + VStack(alignment: .leading) { + Text("Date") + .font(.subheadline) + .foregroundColor(.secondary) + + DatePicker( + "Activity Date", + selection: $selectedDate, + in: ...Date(), + displayedComponents: [.date] + ) + .datePickerStyle(.compact) + .labelsHidden() + } + .frame(maxWidth: .infinity) + + // Minutes input section + VStack(alignment: .leading) { + Text("Minutes") + .font(.subheadline) + .foregroundColor(.secondary) + + TextField("Minutes", text: $activeMinutes) + .textFieldStyle(.roundedBorder) + .keyboardType(.numberPad) + .frame(height: 34) + } + .frame(maxWidth: .infinity) + } + .padding(.horizontal, 10) + } + + private var activityPickerSection: some View { + VStack(alignment: .leading, spacing: 5) { + Text("Activity Type") + .font(.subheadline) + .foregroundColor(.secondary) + .padding(.horizontal, 10) + + let activities = [ + ("Walking", "figure.walk"), + ("Running", "figure.run"), + ("Dancing", "figure.dance"), + ("Sports", "soccerball"), + ("PE", "person.3"), + ("Other", "questionmark") + ] + + LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())], spacing: 12) { + ForEach(activities, id: \.0) { activity in + ActivityButtonView(activityName: activity.0, iconName: activity.1, selectedActivity: $selectedActivity) + } + } + .padding(.horizontal) + } + } + + private var saveButton: some View { + Button { + Task { + await saveNewActivity() + } + } label: { + HStack { + Image(systemName: "plus.circle.fill") + .accessibilityLabel("Add button with plus symbol") + Text("Add Activity") + } + .font(.headline) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .padding(.vertical, 12) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(activeMinutes.isEmpty ? Color.gray : Color.blue) + ) + } + .disabled(activeMinutes.isEmpty) + .padding(.horizontal, 10) + } + + private func getStepsFromMinutes(_ minutes: Int) -> Int { + minutes * 100 + } + + private func saveNewActivity() async { + // Validate date isn't in the future + guard selectedDate <= Date() else { + showingDateError = true + return + } + + let minutes = Int(activeMinutes) ?? 0 + let estimatedSteps = getStepsFromMinutes(minutes) + + let newActivity = Activity( + date: selectedDate, + steps: estimatedSteps, + activeMinutes: minutes, + activityType: selectedActivity + ) + + let prevActivityMinutes = activityManager.getTodayTotalMinutes() + let lastRecordedMilestone = activityManager.getLatestMilestone() + activityManager.activities.append(newActivity) + let activityMinutes = activityManager.getTodayTotalMinutes() + await standard.addActivityToFirestore(newActivity) + await scheduler.handleNotificationsOnLoggedActivity(prevActivityMinutes: prevActivityMinutes, newActivityMinutes: activityMinutes) + activityManager.milestoneManager.displayMilestoneMessage( + newTotal: Double(activityManager.getTodayTotalMinutes()), + lastMilestone: lastRecordedMilestone, + unit: "minutes of activity" + ) + } +} + +#Preview { + ActivityControlPanel() + .environment(ActivityManager()) + .environment(PatientManager()) + .environment(Stanford360Standard()) + .environment(Stanford360Scheduler()) +} diff --git a/Stanford360/Activity/View/ActivityDiscoverView.swift b/Stanford360/Activity/View/ActivityDiscoverView.swift index 1e03190..c5a33c4 100644 --- a/Stanford360/Activity/View/ActivityDiscoverView.swift +++ b/Stanford360/Activity/View/ActivityDiscoverView.swift @@ -13,14 +13,27 @@ import SwiftUI struct ActivityDiscoverView: View { var body: some View { - VStack { - Image("activityRecs") - .resizable() - .scaledToFit() - .frame(width: 350, height: 350) - .padding() - .accessibilityLabel("Activity Recommendations") - } + VStack { + Image("activityRecs") + .resizable() + .scaledToFit() + .frame(width: 350, height: 350) + .padding() + .accessibilityLabel("Activity Recommendations") + + if let videoURL = URL(string: "https://www.youtube.com/channel/UC0dS8MBi0l1sQoFjP1fmpMg/videos?view=0&sort=dd&shelf_id=0") { + Link("Click here for sport videos!", destination: videoURL) + .font(.headline) + .foregroundColor(.blue) + .padding() + .accessibilityHint("Opens sports video website in browser") + } else { + Text("Click here for sport videos!") + .font(.headline) + .foregroundColor(.gray) // Dimmed to indicate it's not clickable + .padding() + } + } } } diff --git a/Stanford360/Activity/View/ActivityTabView.swift b/Stanford360/Activity/View/ActivityTabView.swift index 40f1a4e..6227294 100644 --- a/Stanford360/Activity/View/ActivityTabView.swift +++ b/Stanford360/Activity/View/ActivityTabView.swift @@ -24,6 +24,10 @@ struct ActivityTabView: View { ActivityDiscoverView().tag(TrackerSection.discover) } .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never)) + + if selectedTrackerSection == .add { + ActivityControlPanel() + } } } } diff --git a/Stanford360/Activity/View/AddActivitySheetView.swift b/Stanford360/Activity/View/AddActivitySheetView.swift index d71c2f6..7cd5443 100644 --- a/Stanford360/Activity/View/AddActivitySheetView.swift +++ b/Stanford360/Activity/View/AddActivitySheetView.swift @@ -15,7 +15,7 @@ struct AddActivitySheet: View { @Environment(\.dismiss) private var dismiss @Environment(Stanford360Standard.self) private var standard @Environment(ActivityManager.self) private var activityManager - @Environment(Stanford360Scheduler.self) var scheduler + @Environment(Stanford360Scheduler.self) var scheduler // Activity properties that can be initialized for editing @State private var activeMinutes: String @@ -27,12 +27,6 @@ struct AddActivitySheet: View { private var activityId: String? private var isEditing: Bool - let activityTypes = [ - "Walking πŸšΆβ€β™‚οΈ", "Running πŸƒβ€β™‚οΈ", "Swimming πŸŠβ€β™‚οΈ", - "Dancing πŸ’ƒ", "Basketball πŸ€", "Soccer ⚽️", - "Cycling 🚲", "Other 🌟" - ] - var body: some View { NavigationStack { VStack(spacing: 25) { @@ -85,15 +79,23 @@ struct AddActivitySheet: View { VStack(alignment: .leading) { Text("What did you do?") .font(.headline) - - Picker("Activity", selection: $selectedActivity) { - ForEach(activityTypes, id: \.self) { activity in - Text(activity) + + let activities = [ + ("Walking", "figure.walk"), + ("Running", "figure.run"), + ("Dancing", "figure.dance"), + ("Sports", "soccerball"), + ("PE", "person.3"), + ("Other", "questionmark") + ] + + LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())], spacing: 12) { + ForEach(activities, id: \.0) { activity in + ActivityButtonView(activityName: activity.0, iconName: activity.1, selectedActivity: $selectedActivity) } } - .pickerStyle(.wheel) + .padding(.horizontal) } - .padding() } private var minutesInputSection: some View { @@ -155,10 +157,10 @@ struct AddActivitySheet: View { self.activityId = activity.id self.isEditing = true } - - private func getStepsFromMinutes(_ minutes: Int) -> Int { - minutes * 100 - } + + private func getStepsFromMinutes(_ minutes: Int) -> Int { + minutes * 100 + } private func saveNewActivity() async { // Validate date isn't in the future @@ -168,7 +170,7 @@ struct AddActivitySheet: View { } let minutes = Int(activeMinutes) ?? 0 - let estimatedSteps = getStepsFromMinutes(minutes) + let estimatedSteps = getStepsFromMinutes(minutes) let newActivity = Activity( date: selectedDate, @@ -176,13 +178,13 @@ struct AddActivitySheet: View { activeMinutes: minutes, activityType: selectedActivity ) - - let prevActivityMinutes = activityManager.getTodayTotalMinutes() + + let prevActivityMinutes = activityManager.getTodayTotalMinutes() let lastRecordedMilestone = activityManager.getLatestMilestone() activityManager.activities.append(newActivity) - let activityMinutes = activityManager.getTodayTotalMinutes() + let activityMinutes = activityManager.getTodayTotalMinutes() await standard.addActivityToFirestore(newActivity) - await scheduler.handleNotificationsOnLoggedActivity(prevActivityMinutes: prevActivityMinutes, newActivityMinutes: activityMinutes) + await scheduler.handleNotificationsOnLoggedActivity(prevActivityMinutes: prevActivityMinutes, newActivityMinutes: activityMinutes) activityManager.milestoneManager.displayMilestoneMessage( newTotal: Double(activityManager.getTodayTotalMinutes()), lastMilestone: lastRecordedMilestone, diff --git a/Stanford360/Dashboard/Models/PatientManager.swift b/Stanford360/Dashboard/Models/PatientManager.swift index d6e26fb..ec1b79f 100644 --- a/Stanford360/Dashboard/Models/PatientManager.swift +++ b/Stanford360/Dashboard/Models/PatientManager.swift @@ -74,7 +74,7 @@ class PatientManager: Module, EnvironmentAccessible { // Remove any existing HealthKit activities for today - use consistent activity type let today = Calendar.current.startOfDay(for: Date()) activityManager.activities.removeAll { activity in - activity.activityType == "HealthKit Import" && + activity.activityType == "Walking (HealthKit)" && Calendar.current.startOfDay(for: activity.date) == today } @@ -83,9 +83,8 @@ class PatientManager: Module, EnvironmentAccessible { print("Adding HealthKit activity with \(healthKitActivity.activeMinutes) minutes") // Make sure we're not adding this activity to HealthKit again var activityCopy = healthKitActivity - activityCopy.activityType = "HealthKit Import" + activityCopy.activityType = "Walking (HealthKit)" activityManager.activities.append(activityCopy) - activityManager.saveToStorage() } else { print("No significant HealthKit activity found for today") } diff --git a/Stanford360/Onboarding/KidsOnboarding.swift b/Stanford360/Onboarding/KidsOnboarding.swift index 8045466..f6c7f0a 100644 --- a/Stanford360/Onboarding/KidsOnboarding.swift +++ b/Stanford360/Onboarding/KidsOnboarding.swift @@ -32,7 +32,7 @@ struct KidsOnboarding: View { ), OnboardingInformationView.Content( icon: { - Image(systemName: "lightbulb.fill") + Image(systemName: "questionmark") .accessibilityHidden(true) }, title: "Get Fun Suggestions 🎈", diff --git a/Stanford360/Resources/Localizable.xcstrings b/Stanford360/Resources/Localizable.xcstrings index 8d85e38..62d1f0d 100644 --- a/Stanford360/Resources/Localizable.xcstrings +++ b/Stanford360/Resources/Localizable.xcstrings @@ -105,6 +105,9 @@ }, "Activity" : { + }, + "Activity Date" : { + }, "Activity Minutes" : { @@ -114,9 +117,21 @@ }, "Activity Recommendations" : { + }, + "Activity Saved" : { + }, "Activity Time" : { + }, + "Activity Type" : { + + }, + "Add Activity" : { + + }, + "Add button with plus symbol" : { + }, "Add Your Activity! 🎯" : { @@ -148,6 +163,9 @@ }, "Choose Image Source" : { + }, + "Click here for sport videos!" : { + }, "clock" : { @@ -202,6 +220,9 @@ }, "Dashboard" : { + }, + "Date" : { + }, "Delete" : { @@ -540,6 +561,9 @@ }, "Open Settings" : { + }, + "Opens sports video website in browser" : { + }, "pencil" : { @@ -557,6 +581,9 @@ }, "Please select a date and time that isn't in the future." : { + }, + "Please select a date that isn't in the future." : { + }, "Please upload your meal, you are doing great!πŸŽ‰πŸŽ‰πŸŽ‰" : { @@ -839,6 +866,9 @@ } } } + }, + "Your activity has been recorded successfully!" : { + } }, "version" : "1.0" diff --git a/Stanford360Tests/ActivityTests/ActivityManagerComprehensiveTests.swift b/Stanford360Tests/ActivityTests/ActivityManagerComprehensiveTests.swift new file mode 100644 index 0000000..abc77eb --- /dev/null +++ b/Stanford360Tests/ActivityTests/ActivityManagerComprehensiveTests.swift @@ -0,0 +1,370 @@ +// +// ActivityManagerComprehensiveTests.swift +// Stanford360 +// +// Created by Elsa Bismuth on 11/03/2025. +// +// SPDX-FileCopyrightText: 2025 Stanford University +// +// SPDX-License-Identifier: MIT + +import Foundation +@testable import Stanford360 +import SwiftUICore +import Testing + +struct ActivityManagerComprehensiveTests { + // Helper to create dates with specific times + func createDate(day: Int, month: Int, year: Int, hour: Int = 10, minute: Int = 0) -> Date { + var components = DateComponents() + components.day = day + components.month = month + components.year = year + components.hour = hour + components.minute = minute + + return Calendar.current.date(from: components) ?? Date() + } + + // MARK: - Detailed closure tests + + @Test + func testImplicitClosureInStreakGetter() { + let activityManager = ActivityManager() + + // Create activities that will specifically test the closure in the streak getter + let today = Date() + let calendar = Calendar.current + + guard let yesterday = calendar.date(byAdding: .day, value: -1, to: today), + let twoDaysAgo = calendar.date(byAdding: .day, value: -2, to: today) else { + #expect(Bool(false), "Failed to create test dates") + return + } + + // Add activities with varying minutes to test the specific condition in the closure + activityManager.activities = [ + Activity(date: today, steps: 5500, activeMinutes: 55, activityType: "Running"), + Activity(date: today, steps: 500, activeMinutes: 5, activityType: "Walking"), + Activity(date: yesterday, steps: 6100, activeMinutes: 61, activityType: "Cycling"), + Activity(date: twoDaysAgo, steps: 5900, activeMinutes: 59, activityType: "Swimming") + ] + + // This will execute the implicit closure in the streak getter + let currentStreak = activityManager.streak + + // Verify the streak calculation is correct based on the 60 minute threshold + #expect(currentStreak == 2, "Streak should be 2 with today and yesterday having sufficient minutes") + + // Test boundary conditions + activityManager.activities = [ + Activity(date: today, steps: 6000, activeMinutes: 60, activityType: "Running"), // Exactly 60 minutes + Activity(date: yesterday, steps: 5900, activeMinutes: 59, activityType: "Cycling") // Just under threshold + ] + + #expect(activityManager.streak == 1, "Streak should be 1 with today having exactly 60 minutes and yesterday under threshold") + } + + @Test + func testClosuresInGetTodayTotalMinutes() { + let activityManager = ActivityManager() + + // Create activities for today with different times + let today = Date() + + // Create activities at different times today to test the filter closure + guard let yesterday = Calendar.current.date(byAdding: .day, value: -1, to: today), + let morningToday = Calendar.current.date(bySettingHour: 9, minute: 0, second: 0, of: today), + let afternoonToday = Calendar.current.date(bySettingHour: 14, minute: 30, second: 0, of: today), + let eveningToday = Calendar.current.date(bySettingHour: 19, minute: 45, second: 0, of: today) else { + #expect(Bool(false), "Failed to create test dates") + return + } + + // Activities that should be counted for today + activityManager.activities = [ + Activity(date: morningToday, steps: 1000, activeMinutes: 10, activityType: "Running"), + Activity(date: afternoonToday, steps: 2000, activeMinutes: 20, activityType: "Walking"), + Activity(date: eveningToday, steps: 3000, activeMinutes: 30, activityType: "Cycling"), + // Activity from yesterday that should NOT be counted + Activity(date: yesterday, steps: 4000, activeMinutes: 40, activityType: "Swimming") + ] + + // This will execute both closures in getTodayTotalMinutes + let todayMinutes = activityManager.getTodayTotalMinutes() + + #expect(todayMinutes == 60, "Today's total minutes should be 60 (10+20+30)") + + // Edge case: no activities today + activityManager.activities = [ + Activity(date: yesterday, steps: 4000, activeMinutes: 40, activityType: "Swimming") + ] + + #expect(activityManager.getTodayTotalMinutes() == 0, "Today's total minutes should be 0 with no activities today") + + // Edge case: empty activities array + activityManager.activities = [] + #expect(activityManager.getTodayTotalMinutes() == 0, "Today's total minutes should be 0 with empty activities") + } + + @Test + func testClosureInGetTotalActivityMinutes() { + let activityManager = ActivityManager() + + // Create activities with varying minutes + let activities = [ + Activity(date: Date(), steps: 100, activeMinutes: 1, activityType: "Walking"), + Activity(date: Date(), steps: 200, activeMinutes: 2, activityType: "Running"), + Activity(date: Date(), steps: 300, activeMinutes: 3, activityType: "Cycling") + ] + + // This will execute the closure in getTotalActivityMinutes + let totalMinutes = activityManager.getTotalActivityMinutes(activities) + + #expect(totalMinutes == 6, "Total activity minutes should be 6 (1+2+3)") + + // Test with activities having zero minutes + let zeroActivities = [ + Activity(date: Date(), steps: 0, activeMinutes: 0, activityType: "Walking"), + Activity(date: Date(), steps: 100, activeMinutes: 1, activityType: "Running"), + Activity(date: Date(), steps: 0, activeMinutes: 0, activityType: "Cycling") + ] + + #expect(activityManager.getTotalActivityMinutes(zeroActivities) == 1, + "Total minutes should be 1 with some zero minute activities") + } + + @Test + func testGetLatestMilestone() { + let activityManager = ActivityManager() + + // Set up different activity totals and test milestone calculations + + // Test case 1: 0 minutes + activityManager.activities = [] + let milestone0 = activityManager.getLatestMilestone() + #expect(milestone0 == 0, "Milestone should be 0 for 0 minutes") + + // Test case 2: 30 minutes (0.5 milestone) + activityManager.activities = [ + Activity(date: Date(), steps: 3000, activeMinutes: 30, activityType: "Walking") + ] + let milestone30 = activityManager.getLatestMilestone() + #expect(milestone30 == 20, "Milestone should be 20 for 30 minutes") + + // Test case 3: 60 minutes (1.0 milestone) + activityManager.activities = [ + Activity(date: Date(), steps: 6000, activeMinutes: 60, activityType: "Running") + ] + let milestone60 = activityManager.getLatestMilestone() + #expect(milestone60 == 60, "Milestone should be 60 for 60 minutes") + + // Test case 4: 90 minutes (1.5 milestone) + activityManager.activities = [ + Activity(date: Date(), steps: 9000, activeMinutes: 90, activityType: "Cycling") + ] + let milestone90 = activityManager.getLatestMilestone() + #expect(milestone90 == 80, "Milestone should be 80 for 90 minutes") + + // Test case 5: 120 minutes (2.0 milestone) + activityManager.activities = [ + Activity(date: Date(), steps: 12000, activeMinutes: 120, activityType: "Swimming") + ] + let milestone120 = activityManager.getLatestMilestone() + #expect(milestone120 == 120, "Milestone should be 120 for 120 minutes") + } + + @Test + func testTriggerMotivationAllBranches() { + let activityManager = ActivityManager() + + // Test case 1: No activities (0 minutes) + activityManager.activities = [] + let message0 = activityManager.triggerMotivation() + #expect(message0.contains("Start your activity today"), "Message should encourage starting activity") + + // Test case 2: Some activity but less than 60 minutes + activityManager.activities = [ + Activity(date: Date(), steps: 3000, activeMinutes: 30, activityType: "Walking") + ] + let message30 = activityManager.triggerMotivation() + #expect(message30.contains("Keep going!"), "Message should encourage continuing activity") + #expect(message30.contains("30"), "Message should mention remaining minutes") + + // Test case 3: Exactly 60 minutes + activityManager.activities = [ + Activity(date: Date(), steps: 6000, activeMinutes: 60, activityType: "Running") + ] + let message60 = activityManager.triggerMotivation() + #expect(message60.contains("Amazing!"), "Message should congratulate for reaching goal") + + // Test case 4: More than 60 minutes + activityManager.activities = [ + Activity(date: Date(), steps: 9000, activeMinutes: 90, activityType: "Cycling") + ] + let message90 = activityManager.triggerMotivation() + #expect(message90.contains("Amazing!"), "Message should congratulate for exceeding goal") + } + + // MARK: - Additional comprehensive tests + + @Test + func testMultipleDayActivityPatterns() { + let activityManager = ActivityManager() + + // Create a complex pattern of activities across several days + let today = Date() + let calendar = Calendar.current + + var dates: [Date] = [] + for int in 0..<7 { + if let date = calendar.date(byAdding: .day, value: -int, to: today) { + dates.append(date) + } + } + + // Ensure we have enough dates + guard dates.count >= 7 else { + #expect(Bool(false), "Failed to create test dates") + return + } + + // Create a complex pattern: + // Today: 65 minutes (above threshold) + // Yesterday: 45 minutes (below threshold) + // 2 days ago: 70 minutes (above threshold) + // 3 days ago: 80 minutes (above threshold) + // 4 days ago: No activity + // 5 days ago: 90 minutes (above threshold) + // 6 days ago: 100 minutes (above threshold) + activityManager.activities = [ + // Today + Activity(date: dates[0], steps: 4000, activeMinutes: 40, activityType: "Running"), + Activity(date: dates[0], steps: 2500, activeMinutes: 25, activityType: "Walking"), + + // Yesterday + Activity(date: dates[1], steps: 4500, activeMinutes: 45, activityType: "Cycling"), + + // 2 days ago + Activity(date: dates[2], steps: 7000, activeMinutes: 70, activityType: "Swimming"), + + // 3 days ago + Activity(date: dates[3], steps: 8000, activeMinutes: 80, activityType: "Dancing"), + + // 4 days ago - No activity + + // 5 days ago + Activity(date: dates[5], steps: 9000, activeMinutes: 90, activityType: "Sports"), + + // 6 days ago + Activity(date: dates[6], steps: 10000, activeMinutes: 100, activityType: "PE") + ] + + // Test streak calculation + let streak = activityManager.streak + #expect(streak == 1, "Streak should be 1 because yesterday was below threshold") + + // Test activitiesByDate + let activitiesByDate = activityManager.activitiesByDate + #expect(activitiesByDate.count == 6, "Should have activities for 6 days") + + // Check counts for each day + let todayStart = calendar.startOfDay(for: dates[0]) + let yesterdayStart = calendar.startOfDay(for: dates[1]) + + #expect(activitiesByDate[todayStart]?.count == 2, "Today should have 2 activities") + #expect(activitiesByDate[yesterdayStart]?.count == 1, "Yesterday should have 1 activity") + } + + @Test + func testReverseSortWithIdenticalDates() { + let activityManager = ActivityManager() + + // Create activities with identical dates but different properties + let now = Date() + + let activities = [ + Activity(date: now, steps: 1000, activeMinutes: 10, activityType: "Walking"), + Activity(date: now, steps: 2000, activeMinutes: 20, activityType: "Running"), + Activity(date: now, steps: 3000, activeMinutes: 30, activityType: "Cycling") + ] + + // Test sorting + let sorted = activityManager.reverseSortActivitiesByDate(activities) + + #expect(sorted.count == 3, "Should contain all activities") + + // Since dates are identical, the original order should be preserved + #expect(sorted[0].activeMinutes == 10, "First activity should have 10 minutes") + #expect(sorted[1].activeMinutes == 20, "Second activity should have 20 minutes") + #expect(sorted[2].activeMinutes == 30, "Third activity should have 30 minutes") + } + + @Test + func testActivitiesByDateEdgeCases() { + let activityManager = ActivityManager() + + // Test with empty activities + activityManager.activities = [] + let emptyResult = activityManager.activitiesByDate + #expect(emptyResult.isEmpty, "Result should be empty for empty activities") + + // Test with activities at different times on the same day + let today = Date() + let calendar = Calendar.current + + guard let morning = calendar.date(bySettingHour: 8, minute: 0, second: 0, of: today), + let evening = calendar.date(bySettingHour: 20, minute: 0, second: 0, of: today) else { + #expect(Bool(false), "Failed to create test time-of-day dates") + return + } + + activityManager.activities = [ + Activity(date: morning, steps: 1000, activeMinutes: 10, activityType: "Walking"), + Activity(date: evening, steps: 2000, activeMinutes: 20, activityType: "Running") + ] + + let sameDayResult = activityManager.activitiesByDate + let todayStart = calendar.startOfDay(for: today) + + #expect(sameDayResult.count == 1, "Should have activities for 1 day") + #expect(sameDayResult[todayStart]?.count == 2, "Should have 2 activities for today") + } + + @Test + func testGetTodayTotalMinutesEdgeCases() { + let activityManager = ActivityManager() + + // Test with activities at midnight + let calendar = Calendar.current + let today = Date() + + // Create a date that's exactly midnight today + let midnight = calendar.startOfDay(for: today) + + activityManager.activities = [ + Activity(date: midnight, steps: 1000, activeMinutes: 10, activityType: "Walking") + ] + + let midnightMinutes = activityManager.getTodayTotalMinutes() + #expect(midnightMinutes == 10, "Should count activity at midnight") + + // Test with activities at 23:59:59 today + var components = calendar.dateComponents([.year, .month, .day], from: today) + components.hour = 23 + components.minute = 59 + components.second = 59 + + guard let endOfDay = calendar.date(from: components) else { + #expect(Bool(false), "Failed to create end of day date") + return + } + + activityManager.activities = [ + Activity(date: endOfDay, steps: 2000, activeMinutes: 20, activityType: "Running") + ] + + let endOfDayMinutes = activityManager.getTodayTotalMinutes() + #expect(endOfDayMinutes == 20, "Should count activity at end of day") + } +} diff --git a/Stanford360Tests/ActivityTests/ActivityManagerCoverageTests.swift b/Stanford360Tests/ActivityTests/ActivityManagerCoverageTests.swift new file mode 100644 index 0000000..735f5a5 --- /dev/null +++ b/Stanford360Tests/ActivityTests/ActivityManagerCoverageTests.swift @@ -0,0 +1,222 @@ +// ActivityManagerCoverageTests.swift +// Stanford360Tests +// +// Created by Elsa Bismuth on 11/03/2025. +// +// SPDX-FileCopyrightText: 2025 Stanford University +// +// SPDX-License-Identifier: MIT + +import Foundation +@testable import Stanford360 +import SwiftUICore +import Testing + +struct ActivityManagerCoverageTests { + // Helper to create dates + func createDate(day: Int, month: Int, year: Int) -> Date { + var components = DateComponents() + components.day = day + components.month = month + components.year = year + components.hour = 10 + components.minute = 0 + + return Calendar.current.date(from: components) ?? Date() + } + + @Test + func testActivitiesByDateGetter() { + // Create activities across multiple dates + let activityManager = ActivityManager() + + // March 1, 2, and 3, 2025 + let day1 = createDate(day: 1, month: 3, year: 2025) + let day2 = createDate(day: 2, month: 3, year: 2025) + let day3 = createDate(day: 3, month: 3, year: 2025) + + // Create 2 activities for day 1, 1 for day 2, and 3 for day 3 + let activities = [ + Activity(date: day1, steps: 1000, activeMinutes: 10, activityType: "Walking"), + Activity(date: day1, steps: 2000, activeMinutes: 20, activityType: "Running"), + Activity(date: day2, steps: 3000, activeMinutes: 30, activityType: "Dancing"), + Activity(date: day3, steps: 4000, activeMinutes: 40, activityType: "Sports"), + Activity(date: day3, steps: 5000, activeMinutes: 50, activityType: "PE"), + Activity(date: day3, steps: 6000, activeMinutes: 60, activityType: "Other") + ] + + activityManager.activities = activities + + // Access activitiesByDate to trigger the getter + let activitiesByDate = activityManager.activitiesByDate + + // Verify the grouping is correct + let day1Start = Calendar.current.startOfDay(for: day1) + let day2Start = Calendar.current.startOfDay(for: day2) + let day3Start = Calendar.current.startOfDay(for: day3) + + #expect(activitiesByDate.count == 3, "Should have 3 days of activities") + #expect(activitiesByDate[day1Start]?.count == 2, "Day 1 should have 2 activities") + #expect(activitiesByDate[day2Start]?.count == 1, "Day 2 should have 1 activity") + #expect(activitiesByDate[day3Start]?.count == 3, "Day 3 should have 3 activities") + } + + @Test + func testStreakGetter() { + let activityManager = ActivityManager() + + // Get current date to work with + let today = Date() + let calendar = Calendar.current + + // Create dates for yesterday and the day before + guard let yesterday = calendar.date(byAdding: .day, value: -1, to: today), + let twoDaysAgo = calendar.date(byAdding: .day, value: -2, to: today), + let threeDaysAgo = calendar.date(byAdding: .day, value: -3, to: today), + let fourDaysAgo = calendar.date(byAdding: .day, value: -4, to: today) else { + #expect(Bool(false), "Failed to create test dates") + return + } + + // Test case 1: No activities + #expect(activityManager.streak == 0, "Streak should be 0 with no activities") + + // Test case 2: Today only with sufficient minutes + activityManager.activities = [ + Activity(date: today, steps: 6000, activeMinutes: 60, activityType: "Running") + ] + #expect(activityManager.streak == 1, "Streak should be 1 with only today's activity") + + // Test case 3: Today only with insufficient minutes + activityManager.activities = [ + Activity(date: today, steps: 3000, activeMinutes: 30, activityType: "Walking") + ] + #expect(activityManager.streak == 0, "Streak should be 0 with insufficient minutes today") + + // Test case 4: Today and yesterday, both sufficient + activityManager.activities = [ + Activity(date: today, steps: 6000, activeMinutes: 60, activityType: "Running"), + Activity(date: yesterday, steps: 7000, activeMinutes: 70, activityType: "Cycling") + ] + #expect(activityManager.streak == 2, "Streak should be 2 with today and yesterday sufficient") + + // Test case 5: Today sufficient, yesterday insufficient + activityManager.activities = [ + Activity(date: today, steps: 6000, activeMinutes: 60, activityType: "Running"), + Activity(date: yesterday, steps: 3000, activeMinutes: 30, activityType: "Walking") + ] + #expect(activityManager.streak == 1, "Streak should be 1 with today sufficient, yesterday insufficient") + + // Test case 6: Several days of sufficient activities + activityManager.activities = [ + Activity(date: today, steps: 6000, activeMinutes: 60, activityType: "Running"), + Activity(date: yesterday, steps: 7000, activeMinutes: 70, activityType: "Cycling"), + Activity(date: twoDaysAgo, steps: 8000, activeMinutes: 80, activityType: "Swimming"), + Activity(date: threeDaysAgo, steps: 9000, activeMinutes: 90, activityType: "Dancing") + ] + #expect(activityManager.streak == 4, "Streak should be 4 with four days of sufficient activities") + + // Test case 7: Broken streak (gap in days) + activityManager.activities = [ + Activity(date: today, steps: 6000, activeMinutes: 60, activityType: "Running"), + Activity(date: yesterday, steps: 7000, activeMinutes: 70, activityType: "Cycling"), + // No activity for twoDaysAgo + Activity(date: threeDaysAgo, steps: 9000, activeMinutes: 90, activityType: "Dancing"), + Activity(date: fourDaysAgo, steps: 10000, activeMinutes: 100, activityType: "Sports") + ] + #expect(activityManager.streak == 2, "Streak should be 2 due to the gap in days") + + // Test case 8: Multiple activities in a day summing to sufficient + activityManager.activities = [ + Activity(date: today, steps: 3000, activeMinutes: 30, activityType: "Walking"), + Activity(date: today, steps: 3000, activeMinutes: 30, activityType: "Running"), + Activity(date: yesterday, steps: 7000, activeMinutes: 70, activityType: "Cycling") + ] + #expect(activityManager.streak == 2, "Streak should be 2 with combined activities for today") + } + + @Test + func testReverseSortActivitiesByDate() { + let activityManager = ActivityManager() + + // Create activities with different dates + let today = Date() + let calendar = Calendar.current + + guard let yesterday = calendar.date(byAdding: .day, value: -1, to: today), + let twoDaysAgo = calendar.date(byAdding: .day, value: -2, to: today), + let threeDaysAgo = calendar.date(byAdding: .day, value: -3, to: today) else { + #expect(Bool(false), "Failed to create test dates") + return + } + + // Create activities in random order + let activities = [ + Activity(date: yesterday, steps: 5000, activeMinutes: 50, activityType: "Cycling"), + Activity(date: threeDaysAgo, steps: 7000, activeMinutes: 70, activityType: "Swimming"), + Activity(date: today, steps: 3000, activeMinutes: 30, activityType: "Running"), + Activity(date: twoDaysAgo, steps: 4000, activeMinutes: 45, activityType: "Walking") + ] + + // Call the method to sort activities + let sortedActivities = activityManager.reverseSortActivitiesByDate(activities) + + // Verify the sorting + #expect(sortedActivities.count == 4, "Should contain all activities") + #expect(sortedActivities[0].date == today, "First activity should be today's") + #expect(sortedActivities[1].date == yesterday, "Second activity should be yesterday's") + #expect(sortedActivities[2].date == twoDaysAgo, "Third activity should be from two days ago") + #expect(sortedActivities[3].date == threeDaysAgo, "Fourth activity should be from three days ago") + + // Test with empty array + let emptyResult = activityManager.reverseSortActivitiesByDate([]) + #expect(emptyResult.isEmpty, "Result should be empty for empty input") + + // Test with activities on the same day + let sameDay = [ + Activity(date: today, steps: 3000, activeMinutes: 30, activityType: "Running"), + Activity(date: today, steps: 5000, activeMinutes: 50, activityType: "Walking") + ] + + let sameDaySorted = activityManager.reverseSortActivitiesByDate(sameDay) + #expect(sameDaySorted.count == 2, "Should contain both activities") + #expect(sameDaySorted[0].date == today && sameDaySorted[1].date == today, + "Both activities should have today's date") + } + + @Test + func testGetTotalActivityMinutes() { + let activityManager = ActivityManager() + + // Test case 1: Empty array + let emptyMinutes = activityManager.getTotalActivityMinutes([]) + #expect(emptyMinutes == 0, "Total minutes should be 0 for empty array") + + // Test case 2: Single activity + let singleActivity = [ + Activity(date: Date(), steps: 3000, activeMinutes: 30, activityType: "Running") + ] + + let singleMinutes = activityManager.getTotalActivityMinutes(singleActivity) + #expect(singleMinutes == 30, "Total minutes should be 30 for single activity") + + // Test case 3: Multiple activities + let multipleActivities = [ + Activity(date: Date(), steps: 3000, activeMinutes: 30, activityType: "Running"), + Activity(date: Date(), steps: 4000, activeMinutes: 45, activityType: "Walking"), + Activity(date: Date(), steps: 5000, activeMinutes: 50, activityType: "Cycling") + ] + + let multipleMinutes = activityManager.getTotalActivityMinutes(multipleActivities) + #expect(multipleMinutes == 125, "Total minutes should be 125 for multiple activities") + + // Test case 4: Activities with zero minutes + let zeroMinuteActivities = [ + Activity(date: Date(), steps: 0, activeMinutes: 0, activityType: "Running"), + Activity(date: Date(), steps: 0, activeMinutes: 0, activityType: "Walking") + ] + + let zeroMinutes = activityManager.getTotalActivityMinutes(zeroMinuteActivities) + #expect(zeroMinutes == 0, "Total minutes should be 0 for activities with zero minutes") + } +} diff --git a/Stanford360Tests/ActivityTests/ActivityManagerStreakTests.swift b/Stanford360Tests/ActivityTests/ActivityManagerStreakTests.swift new file mode 100644 index 0000000..f0abb76 --- /dev/null +++ b/Stanford360Tests/ActivityTests/ActivityManagerStreakTests.swift @@ -0,0 +1,199 @@ +// +// ActivityManagerStreakTests.swift +// Stanford360 +// +// Created by Elsa Bismuth on 11/03/2025. +//// +// ActivityManagerStreakTests.swift +// Stanford360Tests +// +// Created on 11/03/2025. +// +// SPDX-FileCopyrightText: 2025 Stanford University +// +// SPDX-License-Identifier: MIT + +import Foundation +@testable import Stanford360 +import SwiftUICore +import Testing + +struct ActivityManagerStreakTests { + // Helper to create dates + func createDate(daysAgo: Int, hour: Int = 12, minute: Int = 0) -> Date { + let calendar = Calendar.current + guard let date = calendar.date(byAdding: .day, value: -daysAgo, to: Date()), + let result = calendar.date(bySettingHour: hour, minute: minute, second: 0, of: date) else { + fatalError("Failed to create test date") + } + return result + } + + @Test + func testStreakCalculationDirectCalls() { + // This test focuses specifically on the streak getter's implementation + let activityManager = ActivityManager() + + // Empty activities should result in zero streak + #expect(activityManager.streak == 0, "Streak should be 0 with no activities") + + // Add activities for multiple consecutive days + activityManager.activities = [ + // Today with exactly 60 minutes (threshold) + Activity(date: createDate(daysAgo: 0), steps: 6000, activeMinutes: 60, activityType: "Running") + ] + + // This should trigger the while loop in streak getter to run once + #expect(activityManager.streak == 1, "Streak should be 1 with today's activity meeting threshold") + + // Add activities for yesterday that don't meet threshold + activityManager.activities.append( + Activity(date: createDate(daysAgo: 1), steps: 5000, activeMinutes: 50, activityType: "Walking") + ) + + // This should cause the streak calculation to stop at today + #expect(activityManager.streak == 1, "Streak should still be 1 with yesterday not meeting threshold") + + // Now add more to yesterday to make it meet threshold + activityManager.activities.append( + Activity(date: createDate(daysAgo: 1), steps: 1000, activeMinutes: 10, activityType: "Walking") + ) + + // This should increase the streak + #expect(activityManager.streak == 2, "Streak should be 2 with today and yesterday meeting threshold") + } + + @Test + func testStreakMultipleDays() { + let activityManager = ActivityManager() + + // Create 5 days of activities, all meeting the threshold + for int in 0..<5 { + activityManager.activities.append( + Activity(date: createDate(daysAgo: int), steps: 6000, activeMinutes: 60, activityType: "Running") + ) + } + + // Should have a 5-day streak + #expect(activityManager.streak == 5, "Should have a 5-day streak") + + // Add a day with insufficient activity, breaking the streak + activityManager.activities.append( + Activity(date: createDate(daysAgo: 5), steps: 5000, activeMinutes: 50, activityType: "Walking") + ) + + // Still should be a 5-day streak because day 6 is insufficient + #expect(activityManager.streak == 5, "Should still have a 5-day streak") + + // Add another activity to make day 6 sufficient + activityManager.activities.append( + Activity(date: createDate(daysAgo: 5), steps: 1000, activeMinutes: 10, activityType: "Jogging") + ) + + // Now we should have a 6-day streak + #expect(activityManager.streak == 6, "Should have a 6-day streak") + } + + @Test + func testStreakWithGaps() { + let activityManager = ActivityManager() + + // Create activities with a gap in the middle + activityManager.activities = [ + // Today + Activity(date: createDate(daysAgo: 0), steps: 6000, activeMinutes: 60, activityType: "Running"), + // Yesterday + Activity(date: createDate(daysAgo: 1), steps: 6000, activeMinutes: 60, activityType: "Cycling"), + // Skip 2 days ago + // 3 days ago + Activity(date: createDate(daysAgo: 3), steps: 6000, activeMinutes: 60, activityType: "Swimming") + ] + + // Should only count today and yesterday (2 days) + #expect(activityManager.streak == 2, "Streak should be 2 due to gap at 2 days ago") + + // Add activity for 2 days ago but insufficient + activityManager.activities.append( + Activity(date: createDate(daysAgo: 2), steps: 4500, activeMinutes: 45, activityType: "Walking") + ) + + // Should still only count today and yesterday (2 days) because 2 days ago is insufficient + #expect(activityManager.streak == 2, "Streak should still be 2 with insufficient activity 2 days ago") + + // Add more activity for 2 days ago to make it sufficient + activityManager.activities.append( + Activity(date: createDate(daysAgo: 2), steps: 1500, activeMinutes: 15, activityType: "Jogging") + ) + + // Now should count today, yesterday, and 2 days ago, and 3 days ago (4 days total) + #expect(activityManager.streak == 4, "Streak should be 4 with sufficient activity all 4 days") + } + + @Test + func testStreakWithMultipleActivitiesPerDay() { + let activityManager = ActivityManager() + + // Add multiple activities on same day across multiple days + activityManager.activities = [ + // Today - multiple activities totaling over threshold + Activity(date: createDate(daysAgo: 0, hour: 9), steps: 2000, activeMinutes: 20, activityType: "Running"), + Activity(date: createDate(daysAgo: 0, hour: 12), steps: 3000, activeMinutes: 30, activityType: "Walking"), + Activity(date: createDate(daysAgo: 0, hour: 18), steps: 1000, activeMinutes: 10, activityType: "Cycling"), + + // Yesterday - multiple activities totaling exactly threshold + Activity(date: createDate(daysAgo: 1, hour: 8), steps: 2000, activeMinutes: 20, activityType: "Yoga"), + Activity(date: createDate(daysAgo: 1, hour: 17), steps: 4000, activeMinutes: 40, activityType: "Swimming"), + + // 2 days ago - multiple activities totaling under threshold + Activity(date: createDate(daysAgo: 2, hour: 10), steps: 2000, activeMinutes: 20, activityType: "Tennis"), + Activity(date: createDate(daysAgo: 2, hour: 14), steps: 3000, activeMinutes: 30, activityType: "Basketball"), + + // 3 days ago - single activity over threshold + Activity(date: createDate(daysAgo: 3, hour: 16), steps: 7000, activeMinutes: 70, activityType: "Hiking") + ] + + // Should have a 2-day streak (today and yesterday) because 2 days ago is under threshold + #expect(activityManager.streak == 2, "Streak should be 2 with today and yesterday meeting threshold") + + // Add more activity to 2 days ago to make it meet threshold + activityManager.activities.append( + Activity(date: createDate(daysAgo: 2, hour: 20), steps: 1000, activeMinutes: 10, activityType: "Stretching") + ) + + // Now should have a 3-day streak + #expect(activityManager.streak == 4, "Streak should be 4 with 2 days ago now meeting threshold") + } + + @Test + func testStreakEdgeCases() { + let activityManager = ActivityManager() + + // Edge case 1: Activities at midnight + activityManager.activities = [ + Activity(date: createDate(daysAgo: 0, hour: 0, minute: 0), steps: 6000, activeMinutes: 60, activityType: "Running"), + Activity(date: createDate(daysAgo: 1, hour: 0, minute: 0), steps: 6000, activeMinutes: 60, activityType: "Cycling") + ] + + #expect(activityManager.streak == 2, "Streak should be 2 with midnight activities") + + // Edge case 2: Activities at 23:59 + activityManager.activities = [ + Activity(date: createDate(daysAgo: 0, hour: 23, minute: 59), steps: 6000, activeMinutes: 60, activityType: "Running"), + Activity(date: createDate(daysAgo: 1, hour: 23, minute: 59), steps: 6000, activeMinutes: 60, activityType: "Cycling") + ] + + #expect(activityManager.streak == 2, "Streak should be 2 with late night activities") + + // Edge case 3: Activities spanning different days + activityManager.activities = [ + // Today morning and evening + Activity(date: createDate(daysAgo: 0, hour: 8), steps: 3000, activeMinutes: 30, activityType: "Running"), + Activity(date: createDate(daysAgo: 0, hour: 20), steps: 3000, activeMinutes: 30, activityType: "Walking"), + + // Yesterday morning only (not enough) + Activity(date: createDate(daysAgo: 1, hour: 9), steps: 5000, activeMinutes: 50, activityType: "Cycling") + ] + + #expect(activityManager.streak == 1, "Streak should be 1 with today sufficient, yesterday insufficient") + } +} diff --git a/Stanford360Tests/ActivityTests/ActivityManagerTest.swift b/Stanford360Tests/ActivityTests/ActivityManagerTest.swift index 1a37b3e..2250f0e 100644 --- a/Stanford360Tests/ActivityTests/ActivityManagerTest.swift +++ b/Stanford360Tests/ActivityTests/ActivityManagerTest.swift @@ -8,273 +8,269 @@ // // SPDX-License-Identifier: MIT -import FirebaseFirestore -// @testable import Stanford360 +import Foundation +@testable import Stanford360 import SwiftUICore -import XCTest +import Testing -final class ActivityManagerTests: XCTestCase { -// var activityManager: ActivityManager? -//// var mockFirestore: Firestore! -//// var mockUserDocRef: DocumentReference! -//// @Environment(Stanford360Standard.self) private var standard -// -// override func setUp() { -// super.setUp() -// activityManager = ActivityManager() +struct ActivityManagerTests { + /// **Test: Logging a new activity** + @Test + func testLogActivity() { + let activityManager = ActivityManager() + + let activity = Activity(date: Date(), steps: 5000, activeMinutes: 50, activityType: "Running") + activityManager.activities.append(activity) + + #expect(activityManager.activities.count == 1, "Activity should be logged in the manager.") + #expect(activityManager.activities.first?.steps == 5000, "Steps count should be correct.") + } + + /// **Test: Get Today's Total Minutes** + @Test + func testGetTodayTotalMinutes() { + let activityManager = ActivityManager() + + let today = Date() + // Use optional binding instead of force unwrapping + guard let yesterday = Calendar.current.date(byAdding: .day, value: -1, to: today) else { + #expect(Bool(false), "Failed to create yesterday date") + return + } + + let activities = [ + Activity(date: today, steps: 3000, activeMinutes: 30, activityType: "Running"), + Activity(date: today, steps: 4000, activeMinutes: 45, activityType: "Walking"), + Activity(date: yesterday, steps: 5000, activeMinutes: 50, activityType: "Cycling") + ] + + activityManager.activities = activities + let totalMinutes = activityManager.getTodayTotalMinutes() + + #expect(totalMinutes == 75, "Today's total minutes should be 75.") + } + + /// **Test: Trigger Motivation** + @Test + func testTriggerMotivation() { + let activityManager = ActivityManager() + + let activities = [ + Activity(date: Date(), steps: 3000, activeMinutes: 30, activityType: "Running"), + Activity(date: Date(), steps: 2500, activeMinutes: 25, activityType: "Walking") + ] + + activityManager.activities = activities + + // Test when total active minutes are less than 60 + let motivationMessage = activityManager.triggerMotivation() + #expect(motivationMessage.contains("Keep going!"), "Message should encourage user to complete 60 minutes.") + + // Add enough minutes to meet the goal + activityManager.activities.append(Activity(date: Date(), steps: 6000, activeMinutes: 60, activityType: "Cycling")) + let motivationMessageAfterGoal = activityManager.triggerMotivation() + #expect(motivationMessageAfterGoal.contains("Amazing!"), "Message should congratulate user for reaching the daily goal.") + } + + /// **Test: Streak Calculation** + @Test + func testCheckStreak() { + let activityManager = ActivityManager() + + let today = Date() + let calendar = Calendar.current + + // Use optional binding for date calculations + guard let yesterday = calendar.date(byAdding: .day, value: -1, to: today), + let twoDaysAgo = calendar.date(byAdding: .day, value: -2, to: today) else { + #expect(Bool(false), "Failed to create date objects") + return + } + + let activities = [ + Activity(date: today, steps: 6000, activeMinutes: 60, activityType: "Running"), + Activity(date: yesterday, steps: 7000, activeMinutes: 70, activityType: "Cycling"), + Activity(date: twoDaysAgo, steps: 9000, activeMinutes: 90, activityType: "Walking") + ] + + activityManager.activities = activities + let streak = activityManager.streak + + #expect(streak == 3, "The streak should be 3 days long.") + } + + /// **Test: Streak Calculation - Broken Streak** + @Test + func testCheckStreakWithBreak() { + let activityManager = ActivityManager() + + let today = Date() + let calendar = Calendar.current + + // Use optional binding for date calculations + guard let threeDaysAgo = calendar.date(byAdding: .day, value: -3, to: today), + let fiveDaysAgo = calendar.date(byAdding: .day, value: -5, to: today) else { + #expect(Bool(false), "Failed to create date objects") + return + } + + let activities = [ + Activity(date: today, steps: 8000, activeMinutes: 80, activityType: "Running"), + Activity(date: threeDaysAgo, steps: 7000, activeMinutes: 70, activityType: "Cycling"), + Activity(date: fiveDaysAgo, steps: 10000, activeMinutes: 100, activityType: "Swimming") + ] + + activityManager.activities = activities + let streak = activityManager.streak + + #expect(streak == 1, "The streak should reset because of the 2-day gap.") + } + + /// **Test: No Activity for Today** + @Test + func testGetNoTodayActivity() { + let activityManager = ActivityManager() + + let todayMinutes = activityManager.getTodayTotalMinutes() + + #expect(todayMinutes == 0, "There should be no activity for today.") + } + + /// **Test: Activities By Date** + @Test + func testActivitiesByDate() { + let activityManager = ActivityManager() + + let today = Date() + let calendar = Calendar.current + let todayStart = calendar.startOfDay(for: today) + + // Use optional binding instead of force unwrapping + guard let yesterday = calendar.date(byAdding: .day, value: -1, to: today) else { + #expect(Bool(false), "Failed to create yesterday date") + return + } + + let yesterdayStart = calendar.startOfDay(for: yesterday) + + let activities = [ + Activity(date: today, steps: 3000, activeMinutes: 30, activityType: "Running"), + Activity(date: today, steps: 4000, activeMinutes: 45, activityType: "Walking"), + Activity(date: yesterday, steps: 5000, activeMinutes: 50, activityType: "Cycling") + ] + + activityManager.activities = activities + let activitiesByDate = activityManager.activitiesByDate + + #expect(activitiesByDate.count == 2, "Activities should be grouped into 2 days") + #expect(activitiesByDate[todayStart]?.count == 2, "Today should have 2 activities") + #expect(activitiesByDate[yesterdayStart]?.count == 1, "Yesterday should have 1 activity") + } + +// /// **Test: Reverse Sort Activities By Date** +// @Test +// func testReverseSortActivitiesByDate() { +// let activityManager = ActivityManager() // -// // Initialize a mock Firestore instance for testing -//// mockFirestore = Firestore.firestore() -//// mockUserDocRef = mockFirestore.collection("users").document("testUser") -//// -//// // Directly override the `userDocumentReference` method using a testable extension -//// standard.configuration.userDocumentReference = { -//// return self.mockUserDocRef -//// } -// } -// -// override func tearDown() { -// activityManager = nil -//// mockFirestore = nil -//// mockUserDocRef = nil -// super.tearDown() -// } -// -// /// **Test: Logging a new activity** -// func testLogActivityToView() { -// guard let manager = activityManager else { -// XCTFail("Activity Manager not initialized") -// return -// } -// let activity = Activity( -// date: Date(), -// steps: 5000, -// activeMinutes: 50, -// activityType: "Running" -// ) -// -// manager.logActivityToView(activity) -// XCTAssertEqual(manager.activities.count, 1, "Activity should be logged in the manager.") -// XCTAssertEqual(manager.activities.first?.steps, 5000, "Steps count should be correct.") -// } -// -// /// **Test: Steps to Active Minutes Conversion** -// func testConvertStepsToMinutes() { -// let minutes = Activity.convertStepsToMinutes(steps: 3000) -// XCTAssertEqual(minutes, 30, "3000 steps should equal 30 active minutes.") -// } -// -// /// **Test: Streak Calculation** -// func testCheckStreak() { -// let today = Date() -// guard let manager = activityManager else { -// XCTFail("Activity Manager not initialized") -// return -// } -// if let yesterday = Calendar.current.date(byAdding: .day, value: -1, to: today), -// let twoDaysAgo = Calendar.current.date(byAdding: .day, value: -2, to: today) { -// let activities = [ -// Activity( -// date: today, -// steps: 6000, -// activeMinutes: 60, -// activityType: "Running" -// ), -// Activity( -// date: yesterday, -// steps: 7000, -// activeMinutes: 70, -// activityType: "Cycling" -// ), -// Activity( -// date: twoDaysAgo, -// steps: 9000, -// activeMinutes: 90, -// activityType: "Walking" -// ) -// ] -// -// manager.activities = activities -// let streak = manager.checkStreak() -// -// XCTAssertEqual(streak, 3, "The streak should be 3 days long.") -// } -// } -// -// /// **Test: Streak Calculation - Broken Streak** -// func testCheckStreakWithBreak() { -// let today = Date() -// guard let manager = activityManager else { -// XCTFail("Activity Manager not initialized") -// return -// } -// -// if let threeDaysAgo = Calendar.current.date(byAdding: .day, value: -3, to: today), -// let fiveDaysAgo = Calendar.current.date(byAdding: .day, value: -5, to: today) { -// let activities = [ -// Activity(date: today, steps: 8000, activeMinutes: 80, activityType: "Running"), -// Activity(date: threeDaysAgo, steps: 7000, activeMinutes: 70, activityType: "Cycling"), -// Activity(date: fiveDaysAgo, steps: 10000, activeMinutes: 100, activityType: "Swimming") -// ] -// -// manager.activities = activities -// let streak = manager.checkStreak() -// -// XCTAssertEqual(streak, 1, "The streak should reset because of the 2-day gap.") -// } -// } -// -// /// **Test: Weekly Summary Fetching** -// func testGetWeeklySummary() { -// let today = Date() -// guard let manager = activityManager else { -// XCTFail("Activity Manager not initialized") -// return -// } -// -// if let oneDayAgo = Calendar.current.date(byAdding: .day, value: -1, to: today), -// let threeDaysAgo = Calendar.current.date(byAdding: .day, value: -3, to: today), -// let eightDaysAgo = Calendar.current.date(byAdding: .day, value: -8, to: today) { // This should be excluded -// -// let activities = [ -// Activity(date: today, steps: 6000, activeMinutes: 60, activityType: "Running"), -// Activity(date: oneDayAgo, steps: 5000, activeMinutes: 50, activityType: "Jogging"), -// Activity(date: threeDaysAgo, steps: 4000, activeMinutes: 40, activityType: "Walking"), -// Activity(date: eightDaysAgo, steps: 3000, activeMinutes: 30, activityType: "Yoga") // Should NOT be included -// ] -// -// manager.activities = activities -// let weeklySummary = manager.getWeeklySummary() -// -// XCTAssertEqual(weeklySummary.count, 3, "Weekly summary should only include activities from the past 7 days.") -// } -// } -// -// /// **Test: Fetch Today's Activity** -// func testGetTodayActivity() { // let today = Date() -// guard let manager = activityManager else { -// XCTFail("Activity Manager not initialized") -// return -// } -// let activity = Activity( -// date: today, -// steps: 4000, -// activeMinutes: 40, -// activityType: "Jogging" -// ) -// -// manager.logActivityToView(activity) -// let todayActivity = manager.getTodayActivity() -// -// XCTAssertNotNil(todayActivity, "Today's activity should be found.") -// XCTAssertEqual(todayActivity?.steps, 4000, "Today's activity steps should be correct.") -// } -// -//// /// **Test: Motivational Message** -//// func testTriggerMotivation() { -//// guard let manager = activityManager else { -//// XCTFail("Activity Manager not initialized") -//// return -//// } -//// let activity = Activity( -//// date: Date(), -//// steps: 5000, -//// activeMinutes: 50, -//// activityType: "Running" -//// ) -//// manager.logActivityToView(activity) -//// -//// let message = manager.triggerMotivation() -//// XCTAssertTrue(message.contains("Keep going!"), "Message should encourage user to complete 60 minutes.") -//// } -// -// func testSendActivityReminder() { -// guard let manager = activityManager else { -// XCTFail("Activity Manager not initialized") +// let calendar = Calendar.current +// +// // Use optional binding for date calculations +// guard let yesterday = calendar.date(byAdding: .day, value: -1, to: today), +// let twoDaysAgo = calendar.date(byAdding: .day, value: -2, to: today) else { +// #expect(Bool(false), "Failed to create date objects") // return // } -// -// let mockActivities: [Activity] = [ -// { -// guard let date = Calendar.current.date(byAdding: .hour, value: -2, to: Date()) else { -// fatalError("Failed to create mock date") -// } -// return Activity( -// date: date, -// steps: 3000, -// activeMinutes: 30, -// activityType: "Running" -// ) -// }(), -// { -// guard let date = Calendar.current.date(byAdding: .hour, value: -1, to: Date()) else { -// fatalError("Failed to create mock date") -// } -// return Activity( -// date: date, -// steps: 2000, -// activeMinutes: 20, -// activityType: "Walking" -// ) -// }() +// +// let activities = [ +// Activity(date: yesterday, steps: 5000, activeMinutes: 50, activityType: "Cycling"), +// Activity(date: today, steps: 3000, activeMinutes: 30, activityType: "Running"), +// Activity(date: twoDaysAgo, steps: 4000, activeMinutes: 45, activityType: "Walking") // ] -// -// manager.activities = mockActivities // Assign activities to the manager -// manager.sendActivityReminder() // Call the function from manager -// -// let totalActiveMinutes = manager.activities.reduce(0) { $0 + $1.activeMinutes } -// -// UNUserNotificationCenter.current().getPendingNotificationRequests { requests in -// if totalActiveMinutes >= 60 { -// XCTAssertFalse(requests.contains { $0.identifier == "activityReminder" }, "No reminder should be scheduled if goal is met.") -// } else { -// XCTAssertTrue(requests.contains { $0.identifier == "activityReminder" }, "A reminder should be scheduled if goal is not met.") -// } -// -// // Additional Checks -// XCTAssertEqual( -// requests.filter { $0.identifier == "activityReminder" }.count, -// totalActiveMinutes < 60 ? 1 : 0, -// "There should be exactly one reminder if total minutes are below 60." -// ) -// -// if let reminder = requests.first(where: { $0.identifier == "activityReminder" }) { -// XCTAssert(reminder.content.body.contains("minutes away"), "Notif body should include remaining minutes message when goal is not met.") -// if totalActiveMinutes < 60 { -// XCTAssert(reminder.trigger is UNTimeIntervalNotificationTrigger, "Notification trigger should be time-based.")} -// } -// } +// +// let sortedActivities = activityManager.reverseSortActivitiesByDate(activities) +// +// #expect(sortedActivities.count == 3, "Should contain all activities") +// #expect(sortedActivities[0].date == today, "First activity should be today's") +// #expect(sortedActivities[1].date == yesterday, "Second activity should be yesterday's") +// #expect(sortedActivities[2].date == twoDaysAgo, "Third activity should be from two days ago") // } +// + /// **Test: Get Latest Milestone** + @Test + func testGetLatestMilestone() { + let activityManager = ActivityManager() + + let today = Date() + + // Test with different minute values + let testCases = [ + (minutes: 0, expected: 0.0), + (minutes: 30, expected: 20), + (minutes: 60, expected: 60), + (minutes: 90, expected: 80) + ] + + for testCase in testCases { + activityManager.activities = [ + Activity(date: today, steps: 3000, activeMinutes: testCase.minutes, activityType: "Running") + ] + + let milestone = activityManager.getLatestMilestone() + #expect(milestone == testCase.expected, "Milestone should be \(testCase.expected) for \(testCase.minutes) minutes") + } + } -// /// **Test: Store Activity in Firestore** -// func testStoreActivity() async throws { -// guard let manager = activityManager else { -// XCTFail("Activity Manager not initialized") -// return -// } -// -// let activity = Activity( -// date: Date(), -// steps: 6000, -// activeMinutes: 60, -// activityType: "Running" -// ) -// -// do { -// try await standard.store(activity: activity) -// -// // Fetch stored data to verify -// let snapshot = try await mockUserDocRef.collection("activities").getDocuments() -// let storedActivities = snapshot.documents.map { try? $0.data(as: Activity.self) } -// -// XCTAssertFalse(storedActivities.isEmpty, "Activity should be stored in Firestore.") -// XCTAssertEqual(storedActivities.first??.steps, 6000, "Stored activity steps should match.") -// XCTAssertEqual(storedActivities.first??.activeMinutes, 60, "Stored activity minutes should match.") -// XCTAssertEqual(storedActivities.first??.activityType, "Running", "Stored activity type should match.") -// -// } catch { -// XCTFail("Failed to store activity: \(error)") -// } -// } + /// **Test: Trigger Motivation with No Activity** + @Test + func testTriggerMotivationWithNoActivity() { + let activityManager = ActivityManager() + + // Clear any activities + activityManager.activities = [] + + let motivationMessage = activityManager.triggerMotivation() + #expect(motivationMessage.contains("Start your activity"), "Should encourage to start activity when no minutes logged") + } + + /// **Test: Custom ActivityManager Initialization** + @Test + func testCustomInitialization() { + let today = Date() + let activities = [ + Activity(date: today, steps: 3000, activeMinutes: 30, activityType: "Running"), + Activity(date: today, steps: 4000, activeMinutes: 45, activityType: "Walking") + ] + + // Initialize with custom activities + let customActivityManager = ActivityManager(activities: activities) + + #expect(customActivityManager.activities.count == 2, "Should have 2 activities") + #expect(customActivityManager.getTodayTotalMinutes() == 75, "Total minutes should be 75") + } + + /// **Test: Streak with Insufficient Activity Minutes** + @Test + func testStreakWithInsufficientActivityMinutes() { + let activityManager = ActivityManager() + + let today = Date() + let calendar = Calendar.current + + // Use optional binding for date calculation + guard let yesterday = calendar.date(byAdding: .day, value: -1, to: today) else { + #expect(Bool(false), "Failed to create yesterday date") + return + } + + let activities = [ + Activity(date: today, steps: 6000, activeMinutes: 60, activityType: "Running"), + Activity(date: yesterday, steps: 3000, activeMinutes: 30, activityType: "Walking") // Less than 60 minutes + ] + + activityManager.activities = activities + let streak = activityManager.streak + + #expect(streak == 1, "Streak should be 1 since yesterday didn't have enough minutes") + } } diff --git a/Stanford360Tests/ActivityTests/HealthKitActivityTests.swift b/Stanford360Tests/ActivityTests/HealthKitActivityTests.swift index 6b93e86..aeecec8 100644 --- a/Stanford360Tests/ActivityTests/HealthKitActivityTests.swift +++ b/Stanford360Tests/ActivityTests/HealthKitActivityTests.swift @@ -19,7 +19,7 @@ final class HealthKitActivityTests: XCTestCase { date: date, steps: 1000, activeMinutes: 30, - activityType: "HealthKit" + activityType: "Walking" ) // When @@ -29,6 +29,6 @@ final class HealthKitActivityTests: XCTestCase { XCTAssertEqual(activity.date, date) XCTAssertEqual(activity.steps, 1000) XCTAssertEqual(activity.activeMinutes, 30) - XCTAssertEqual(activity.activityType, "HealthKit") + XCTAssertEqual(activity.activityType, "Walking") } }