diff --git a/Yattee/Services/SubscriptionFeedCache.swift b/Yattee/Services/SubscriptionFeedCache.swift index 84f24c95..7ba5e3d1 100644 --- a/Yattee/Services/SubscriptionFeedCache.swift +++ b/Yattee/Services/SubscriptionFeedCache.swift @@ -22,11 +22,44 @@ enum FeedLoadState: Equatable { case partiallyLoaded(readyCount: Int, pendingCount: Int, errorCount: Int) case error(FeedLoadError) + /// The feed error worth showing in this state, if any. + /// + /// While the context banner is up, only the errors it already answers for + /// stand down — the banner names their cause, so it is the one that stays. + /// Everything else keeps its own message and its retry: a local refresh + /// that failed on the network is news the banner does not carry, and + /// swallowing it leaves a stale feed with nothing to say and nothing to + /// press. + func displayedError(contextBannerVisible: Bool) -> FeedLoadError? { + guard case let .error(error) = self else { return nil } + guard !contextBannerVisible || !error.isAnsweredByContextBanner else { return nil } + + return error + } + /// Specific error types for feed loading. enum FeedLoadError: Equatable { case yatteeServerRequired case notAuthenticated case networkError(String) + + /// Whether a context banner, when one is up, already answers for this. + /// + /// A selected account that cannot be used reads local data, and the + /// local feed then asks for the Yattee Server it has no reason to have + /// — "add a subscriptions account in Settings" under a banner saying + /// *this* account needs signing in again is two answers to one + /// question. These two are the account the banner is already talking + /// about; a refresh that failed on the way out is not, and has a retry + /// worth offering. + var isAnsweredByContextBanner: Bool { + switch self { + case .yatteeServerRequired, .notAuthenticated: + return true + case .networkError: + return false + } + } } } diff --git a/Yattee/Views/Subscriptions/SubscriptionsView.swift b/Yattee/Views/Subscriptions/SubscriptionsView.swift index 515e9c8e..856cd4eb 100644 --- a/Yattee/Views/Subscriptions/SubscriptionsView.swift +++ b/Yattee/Views/Subscriptions/SubscriptionsView.swift @@ -165,20 +165,10 @@ struct SubscriptionsView: View { } #endif - /// The feed error worth showing, if any. - /// - /// Nothing while the context banner is up: a selected account that cannot be - /// used reads local data, and the local feed then reports the Yattee Server - /// it has no reason to have — "add a subscriptions account in Settings" - /// under a banner saying *this* account needs signing in again is two - /// answers to one question. The banner names the cause, so it is the one - /// that stays. + /// The feed error worth showing, if any — the banner takes over only for + /// the errors it is already the answer to. private var displayedFeedError: FeedLoadState.FeedLoadError? { - guard contextUnavailability == nil, - case let .error(error) = feedCache.feedLoadState - else { return nil } - - return error + feedCache.feedLoadState.displayedError(contextBannerVisible: contextUnavailability != nil) } private var isShowingFullScreenError: Bool { diff --git a/YatteeTests/SubscriptionFeedErrorTests.swift b/YatteeTests/SubscriptionFeedErrorTests.swift new file mode 100644 index 00000000..3456c3a2 --- /dev/null +++ b/YatteeTests/SubscriptionFeedErrorTests.swift @@ -0,0 +1,73 @@ +// +// SubscriptionFeedErrorTests.swift +// YatteeTests +// +// Tests for which feed errors the context banner speaks for. +// + +import Testing +import Foundation +@testable import Yattee + +@Suite("Subscription Feed Error Display") +struct SubscriptionFeedErrorTests { + + // MARK: - Without a banner + + @Test("Every feed error shows when no banner is up") + func everyErrorShowsWithoutBanner() { + let errors: [FeedLoadState.FeedLoadError] = [ + .yatteeServerRequired, + .notAuthenticated, + .networkError("The Internet connection appears to be offline.") + ] + + for error in errors { + #expect(FeedLoadState.error(error).displayedError(contextBannerVisible: false) == error) + } + } + + // MARK: - Under the banner + + @Test("The Yattee Server the fallback asks for stands down under the banner") + func yatteeServerRequiredStandsDownUnderBanner() { + let state = FeedLoadState.error(.yatteeServerRequired) + + #expect(state.displayedError(contextBannerVisible: true) == nil) + } + + @Test("An account error stands down under the banner") + func notAuthenticatedStandsDownUnderBanner() { + let state = FeedLoadState.error(.notAuthenticated) + + #expect(state.displayedError(contextBannerVisible: true) == nil) + } + + @Test("A failed refresh keeps its error under the banner") + func networkErrorSurvivesTheBanner() { + let state = FeedLoadState.error(.networkError("The Internet connection appears to be offline.")) + + #expect( + state.displayedError(contextBannerVisible: true) + == .networkError("The Internet connection appears to be offline.") + ) + } + + // MARK: - Other states + + @Test("States that are not errors have nothing to show") + func nonErrorStatesShowNothing() { + let states: [FeedLoadState] = [ + .idle, + .loading, + .loadingMore, + .ready, + .partiallyLoaded(readyCount: 1, pendingCount: 2, errorCount: 0) + ] + + for state in states { + #expect(state.displayedError(contextBannerVisible: false) == nil) + #expect(state.displayedError(contextBannerVisible: true) == nil) + } + } +}