mirror of
https://github.com/yattee/yattee.git
synced 2026-10-24 05:32:53 +00:00
Let the banner speak only for what it answers
Standing the feed error down under the context banner was aimed at one sentence and hit every one. The fallback to local reads asks for a Yattee Server nobody configured, and that question the banner already answers — but a local refresh that failed on the way out answers nothing, and it was being swallowed too. With the banner up, an offline refresh left the last feed on screen with no explanation and no retry: the state said error, the view said nothing. The decision now names the errors the banner covers. `yatteeServerRequired` and `notAuthenticated` are the account the banner is already talking about and stand down; a `networkError` is news the banner does not carry and keeps its own message and its retry button. It lives on `FeedLoadState` as `displayedError(contextBannerVisible:)` so the rule is one function rather than a guard in a view property, and so it can be tested.
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
73
YatteeTests/SubscriptionFeedErrorTests.swift
Normal file
73
YatteeTests/SubscriptionFeedErrorTests.swift
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user