-
Notifications
You must be signed in to change notification settings - Fork 0
Refactor UI tests to use app extension helpers and improve test stability #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,7 +31,6 @@ struct HomeScreen: View { | |
| private var highContrastMode = AccessibilitySettings.highContrastModeDefault | ||
|
|
||
| @State private var spotlightMode: SpotlightMode = .quickCards | ||
| @State private var connectivity: ConnectivityState = .offline | ||
| @State private var quickCardsState: HomeSectionState<[QuickCard]> = .loading | ||
| @State private var feedState: HomeSectionState<[HomeFeedItem]> = .loading | ||
| @State private var pinnedState: HomeSectionState<[HomePinnedItem]> = .loading | ||
|
|
@@ -42,15 +41,15 @@ struct HomeScreen: View { | |
| @State private var notesState: HomeSectionState<[NoteRecord]> = .loading | ||
| @State private var readinessSnapshot: SupplyReadinessSnapshot? | ||
| @State private var showEmergencyMode = false | ||
| @State private var connectivityNotice: ConnectivityStatusNotice? | ||
| @State private var connectivityNoticeDismissTask: Task<Void, Never>? | ||
| @State private var hasLoadedInitially = false | ||
| @State private var connectivityPresenter = ConnectivityNoticePresenter() | ||
|
|
||
| var body: some View { | ||
| ScrollView { | ||
| VStack(alignment: .leading, spacing: Spacing.lg) { | ||
| HomeHeaderView(connectivity: connectivity, onEmergencyModeTapped: openEmergencyMode) | ||
| if let connectivityNotice { | ||
| ConnectivityStatusCallout(notice: connectivityNotice) | ||
| HomeHeaderView(connectivity: connectivityPresenter.connectivity, onEmergencyModeTapped: openEmergencyMode) | ||
| if let notice = connectivityPresenter.notice { | ||
| ConnectivityStatusCallout(notice: notice) | ||
| } | ||
| HomeReadinessSectionView(readinessSnapshot: readinessSnapshot) | ||
| HomeWeeklyDrillSectionView(state: weeklyDrillState) | ||
|
|
@@ -73,24 +72,38 @@ struct HomeScreen: View { | |
| .navigationTitle("Home") | ||
| .navigationBarTitleDisplayMode(.inline) | ||
| .onAppear(perform: loadDashboard) | ||
| .task { await observeConnectivity() } | ||
| .task { | ||
| connectivityPresenter.updateReduceMotion(accessibilityReduceMotion) | ||
| guard let service = connectivityService else { return } | ||
| await connectivityPresenter.observe(service: service) { state, previous in | ||
| Self.homeConnectivityNotice(for: state, previousState: previous) | ||
| } | ||
| } | ||
| .refreshable { await refreshDashboard() } | ||
| .fullScreenCover(isPresented: $showEmergencyMode) { | ||
| EmergencyModeView() | ||
| } | ||
| .onDisappear { | ||
| connectivityNoticeDismissTask?.cancel() | ||
| connectivityPresenter.cancelDismissTask() | ||
| } | ||
| } | ||
|
|
||
| private func loadDashboard() { | ||
| reloadLocalSections() | ||
| if hasLoadedInitially { | ||
| // On tab re-entry, refresh only sections whose underlying data may | ||
| // have changed (checklists, inventory, notes, readiness). Quick cards | ||
| // keep their stable shuffle to avoid visual churn. | ||
| reloadMutableSections() | ||
| } else { | ||
| hasLoadedInitially = true | ||
| reloadAllSections() | ||
| } | ||
| if spotlightMode == .feed, case .loading = feedState { | ||
| Task { await loadFeed() } | ||
| } | ||
| } | ||
|
|
||
| private func reloadLocalSections() { | ||
| private func reloadAllSections() { | ||
| loadQuickCards() | ||
| loadWeeklyDrill() | ||
| loadPinnedContent() | ||
|
|
@@ -101,8 +114,18 @@ struct HomeScreen: View { | |
| loadRecentNotes() | ||
| } | ||
|
|
||
| /// Refreshes only the sections backed by user-mutable data. | ||
| /// Quick cards and weekly drill keep their stable shuffle. | ||
| private func reloadMutableSections() { | ||
| loadPinnedContent() | ||
| loadActiveChecklists() | ||
| loadReadinessSnapshot() | ||
| loadInventoryReminders() | ||
| loadRecentNotes() | ||
| } | ||
|
|
||
| private func refreshDashboard() async { | ||
| reloadLocalSections() | ||
| reloadAllSections() | ||
| if spotlightMode == .feed { | ||
| await loadFeed() | ||
| } | ||
|
|
@@ -496,42 +519,7 @@ struct HomeScreen: View { | |
| } | ||
| } | ||
|
|
||
| private func observeConnectivity() async { | ||
| guard let service = connectivityService else { return } | ||
| var previousState: ConnectivityState? | ||
| for await state in service.stateStream() { | ||
| handleConnectivityChange(from: previousState, to: state) | ||
| previousState = state | ||
| } | ||
| } | ||
|
|
||
| private func handleConnectivityChange(from previousState: ConnectivityState?, to newState: ConnectivityState) { | ||
| guard previousState != newState else { return } | ||
| withAnimation(connectivityAnimation) { | ||
| connectivity = newState | ||
| } | ||
| presentConnectivityNotice(homeConnectivityNotice(for: newState, previousState: previousState)) | ||
| } | ||
|
|
||
| private func presentConnectivityNotice(_ notice: ConnectivityStatusNotice?) { | ||
| connectivityNoticeDismissTask?.cancel() | ||
|
|
||
| withAnimation(connectivityAnimation) { | ||
| connectivityNotice = notice | ||
| } | ||
|
|
||
| guard let notice, notice.autoDismisses else { return } | ||
|
|
||
| connectivityNoticeDismissTask = Task { @MainActor in | ||
| try? await Task.sleep(for: .seconds(4)) | ||
| guard !Task.isCancelled, connectivityNotice == notice else { return } | ||
| withAnimation(connectivityAnimation) { | ||
| connectivityNotice = nil | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private func homeConnectivityNotice( | ||
| private static func homeConnectivityNotice( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 MEDIUM RISK The method homeConnectivityNotice has a cyclomatic complexity of 9 (threshold 8). Given HomeScreen.swift is flagged as an uncovered complex file, this logic should be simplified to reduce maintenance risk. Refactor to use a switch statement or a declarative mapping for connectivity state transitions. |
||
| for state: ConnectivityState, | ||
| previousState: ConnectivityState? | ||
| ) -> ConnectivityStatusNotice? { | ||
|
|
@@ -570,11 +558,6 @@ struct HomeScreen: View { | |
| } | ||
| } | ||
|
|
||
| private var connectivityAnimation: Animation { | ||
| accessibilityReduceMotion | ||
| ? .easeOut(duration: 0.12) | ||
| : .easeInOut(duration: 0.2) | ||
| } | ||
| } | ||
|
|
||
| #Preview { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,16 +33,14 @@ struct SettingsScreen: View { | |
| private var isRSSDiscoveryEnabled = DiscoverySettings.isRSSDiscoveryEnabledDefault | ||
| @AppStorage(DiscoverySettings.lastDiscoveryDateKey) | ||
| private var lastDiscoveryTimestamp = 0.0 | ||
| @State private var connectivity: ConnectivityState = .offline | ||
| @State private var braveSearchAPIKey: String | ||
| @State private var isDiscovering = false | ||
| @State private var lastDiscoveryMessage: String? | ||
| @State private var lastDiscoveryMessageColor: Color = .secondary | ||
| @State private var credentialErrorMessage: String? | ||
| @State private var contacts: [EmergencyContact] = [] | ||
| @State private var contactEditor: EmergencyContactEditorState? | ||
| @State private var connectivityNotice: ConnectivityStatusNotice? | ||
| @State private var connectivityNoticeDismissTask: Task<Void, Never>? | ||
| @State private var connectivityPresenter = ConnectivityNoticePresenter() | ||
| @State private var inventoryAlertAuthorizationStatus: InventoryNotificationAuthorizationStatus = .notDetermined | ||
| @State private var isUpdatingInventoryAlerts = false | ||
| private let braveSearchCredentialStore: BraveSearchCredentialStore | ||
|
|
@@ -74,7 +72,11 @@ struct SettingsScreen: View { | |
| .task { | ||
| loadContacts() | ||
| await refreshInventoryAlertAuthorizationStatus() | ||
| await observeConnectivity() | ||
| connectivityPresenter.updateReduceMotion(accessibilityReduceMotion) | ||
| guard let service = connectivityService else { return } | ||
| await connectivityPresenter.observe(service: service) { state, previous in | ||
| Self.settingsConnectivityNotice(for: state, previousState: previous) | ||
| } | ||
| } | ||
| .onChange(of: braveSearchAPIKey) { _, newValue in | ||
| persistBraveSearchAPIKey(newValue) | ||
|
|
@@ -108,7 +110,7 @@ struct SettingsScreen: View { | |
| } | ||
| } | ||
| .onDisappear { | ||
| connectivityNoticeDismissTask?.cancel() | ||
| connectivityPresenter.cancelDismissTask() | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -279,12 +281,12 @@ struct SettingsScreen: View { | |
| @ViewBuilder | ||
| private var connectivitySection: some View { | ||
| Section("Connectivity") { | ||
| if let connectivityNotice { | ||
| ConnectivityStatusCallout(notice: connectivityNotice) | ||
| if let notice = connectivityPresenter.notice { | ||
| ConnectivityStatusCallout(notice: notice) | ||
| } | ||
|
|
||
| LabeledContent("Status") { | ||
| ConnectivityBadge(state: connectivity) | ||
| ConnectivityBadge(state: connectivityPresenter.connectivity) | ||
| } | ||
|
|
||
| LabeledContent("Last Discovery Run") { | ||
|
|
@@ -414,7 +416,7 @@ struct SettingsScreen: View { | |
| lastDiscoveryMessageColor = .osaCritical | ||
| return | ||
| } | ||
| guard connectivity == .onlineUsable else { | ||
| guard connectivityPresenter.connectivity == .onlineUsable else { | ||
| lastDiscoveryMessage = "Connect to the internet to discover new content." | ||
| lastDiscoveryMessageColor = .secondary | ||
| hapticFeedbackService?.play(.warning) | ||
|
|
@@ -455,22 +457,6 @@ struct SettingsScreen: View { | |
| } | ||
| } | ||
|
|
||
| private func observeConnectivity() async { | ||
| guard let service = connectivityService else { return } | ||
| var previousState: ConnectivityState? | ||
| for await state in service.stateStream() { | ||
| handleConnectivityChange(from: previousState, to: state) | ||
| previousState = state | ||
| } | ||
| } | ||
|
|
||
| private func handleConnectivityChange(from previousState: ConnectivityState?, to newState: ConnectivityState) { | ||
| guard previousState != newState else { return } | ||
| withAnimation(connectivityAnimation) { | ||
| connectivity = newState | ||
| } | ||
| presentConnectivityNotice(settingsConnectivityNotice(for: newState, previousState: previousState)) | ||
| } | ||
|
|
||
| private func refreshInventoryAlertAuthorizationStatus() async { | ||
| guard let inventoryExpiryNotificationService else { | ||
|
|
@@ -539,25 +525,7 @@ struct SettingsScreen: View { | |
| } | ||
| } | ||
|
|
||
| private func presentConnectivityNotice(_ notice: ConnectivityStatusNotice?) { | ||
| connectivityNoticeDismissTask?.cancel() | ||
|
|
||
| withAnimation(connectivityAnimation) { | ||
| connectivityNotice = notice | ||
| } | ||
|
|
||
| guard let notice, notice.autoDismisses else { return } | ||
|
|
||
| connectivityNoticeDismissTask = Task { @MainActor in | ||
| try? await Task.sleep(for: .seconds(4)) | ||
| guard !Task.isCancelled, connectivityNotice == notice else { return } | ||
| withAnimation(connectivityAnimation) { | ||
| connectivityNotice = nil | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private func settingsConnectivityNotice( | ||
| private static func settingsConnectivityNotice( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 MEDIUM RISK Similar to the home screen, the settingsConnectivityNotice method is overly complex at a CCN of 9. Extract the logic for determining the ConnectivityStatusNotice into a dedicated mapper or use a switch statement on the state transitions to improve maintainability. |
||
| for state: ConnectivityState, | ||
| previousState: ConnectivityState? | ||
| ) -> ConnectivityStatusNotice? { | ||
|
|
@@ -635,7 +603,7 @@ struct SettingsScreen: View { | |
| } | ||
|
|
||
| private var connectivitySupportText: String { | ||
| switch connectivity { | ||
| switch connectivityPresenter.connectivity { | ||
| case .offline: | ||
| "OSA remains fully usable offline. Connectivity only affects optional discovery and refresh." | ||
| case .onlineConstrained: | ||
|
|
@@ -648,7 +616,7 @@ struct SettingsScreen: View { | |
| } | ||
|
|
||
| private var discoveryAvailabilityText: String { | ||
| switch connectivity { | ||
| switch connectivityPresenter.connectivity { | ||
| case .offline: | ||
| "Discovery is unavailable offline. Your local handbook, quick cards, and notes still work." | ||
| case .onlineConstrained: | ||
|
|
@@ -661,7 +629,7 @@ struct SettingsScreen: View { | |
| } | ||
|
|
||
| private var discoveryAvailabilityIcon: String { | ||
| switch connectivity { | ||
| switch connectivityPresenter.connectivity { | ||
| case .offline: | ||
| "wifi.slash" | ||
| case .onlineConstrained: | ||
|
|
@@ -674,7 +642,7 @@ struct SettingsScreen: View { | |
| } | ||
|
|
||
| private var discoveryAvailabilityTint: Color { | ||
| switch connectivity { | ||
| switch connectivityPresenter.connectivity { | ||
| case .offline: | ||
| .osaBoundary | ||
| case .onlineConstrained: | ||
|
|
@@ -687,11 +655,11 @@ struct SettingsScreen: View { | |
| } | ||
|
|
||
| private var discoveryActionDisabled: Bool { | ||
| isDiscovering || discoveryCoordinator == nil || connectivity != .onlineUsable | ||
| isDiscovering || discoveryCoordinator == nil || connectivityPresenter.connectivity != .onlineUsable | ||
| } | ||
|
|
||
| private var discoveryAccessibilityHint: String { | ||
| switch connectivity { | ||
| switch connectivityPresenter.connectivity { | ||
| case .offline: | ||
| "Unavailable offline. Reconnect to check approved sources for new content." | ||
| case .onlineConstrained: | ||
|
|
@@ -746,12 +714,6 @@ struct SettingsScreen: View { | |
| } | ||
| } | ||
|
|
||
| private var connectivityAnimation: Animation { | ||
| accessibilityReduceMotion | ||
| ? .easeOut(duration: 0.12) | ||
| : .easeInOut(duration: 0.2) | ||
| } | ||
|
|
||
| private var selectedHazards: Set<HazardScenario> { | ||
| Set(UserProfileSettings.hazards(from: hazardsRawValue)) | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
⚪ LOW RISK
Suggestion: Use the
idparameter on the.taskmodifier to keep the presenter's motion settings in sync with system changes (e.g., .task(id: accessibilityReduceMotion)).