Redesign the station listing and move the Best Value controls
Two columns split by a 1pt hairline: identity on the left at a fixed 52% share so rows line up when scrolling, price and savings on the right, leading-aligned on one axis with the price dominant. The favourites star sits under the station logo, freeing the width the pounds-and-pence price needs to render in full rather than scaled or truncated. Badges stay tinted rather than filled — BEST VALUE green, CHEAPEST blue, none for nearest — and the verdict capsule is one shared view across the listing, the detail sheet and the compact summary, so wording and colour cannot drift between them. Best Value's minimum saving moves from Alerts to Settings, Fuel Savings, beside the fill size and MPG that produce the saving; the Alerts tab reads it back instead of offering it twice. Its options are now 50p steps to £5 (was 50p/£1/£2/£5), so £2.50 and £3 are reachable, with a test pinning the step, the ceiling and the store's string round-trip. The verdict capsule scales to 0.7 before truncating. Measured on a 393pt screen its copy is handed ~69pt while "Closest station" needs ~90pt, so the old 0.8 floor could not reach it and rows showed "Closest stati..." instead. Verified: Debug build clean, swift test 249/249.
This commit is contained in:
@@ -159,3 +159,101 @@ final class FuelSavingsSettingsTests: XCTestCase {
|
||||
XCTAssertTrue(FuelSavingsSettings(enabled: true, mpg: 45, preferredAmount: .init(value: 40, unit: .pounds)).isConfigured)
|
||||
}
|
||||
}
|
||||
|
||||
/// The row's single recommendation badge. Presentation only, but the rules are
|
||||
/// worth pinning: they decide which of two true statements a row makes, and
|
||||
/// "no badge" is itself an answer.
|
||||
final class StationRecommendationBadgeTests: XCTestCase {
|
||||
private let threshold = 1.0
|
||||
|
||||
func testSavingAtTheThresholdEarnsBestValue() {
|
||||
XCTAssertEqual(
|
||||
StationRecommendationBadge.badge(netSavingGBP: 1.0, isCheapest: false,
|
||||
bestValueThresholdGBP: threshold),
|
||||
.bestValue
|
||||
)
|
||||
}
|
||||
|
||||
func testSavingBelowTheThresholdDoesNot() {
|
||||
XCTAssertNil(
|
||||
StationRecommendationBadge.badge(netSavingGBP: 0.42, isCheapest: false,
|
||||
bestValueThresholdGBP: threshold)
|
||||
)
|
||||
}
|
||||
|
||||
func testCheapestWithoutAWorthwhileSavingStillSaysCheapest() {
|
||||
XCTAssertEqual(
|
||||
StationRecommendationBadge.badge(netSavingGBP: 0.42, isCheapest: true,
|
||||
bestValueThresholdGBP: threshold),
|
||||
.cheapest
|
||||
)
|
||||
}
|
||||
|
||||
/// The cheapest station keeps its badge even when the drive costs more than
|
||||
/// the saving buys — it IS the cheapest price; the capsule underneath is
|
||||
/// what says the trip is not worth it.
|
||||
func testCheapestKeepsItsBadgeWhenTheDriveCostsMoreThanItSaves() {
|
||||
XCTAssertEqual(
|
||||
StationRecommendationBadge.badge(netSavingGBP: -14.30, isCheapest: true,
|
||||
bestValueThresholdGBP: threshold),
|
||||
.cheapest
|
||||
)
|
||||
}
|
||||
|
||||
/// One badge per row: the stronger statement wins.
|
||||
func testBestValueOutranksCheapestWhenBothApply() {
|
||||
XCTAssertEqual(
|
||||
StationRecommendationBadge.badge(netSavingGBP: 3.15, isCheapest: true,
|
||||
bestValueThresholdGBP: threshold),
|
||||
.bestValue
|
||||
)
|
||||
}
|
||||
|
||||
/// A near-by station with no saving to speak of is context, not a
|
||||
/// recommendation — the mockup gives NEAREST no capsule, and nothing else
|
||||
/// here may invent one.
|
||||
func testNoSavingAndNotCheapestMeansNoBadge() {
|
||||
XCTAssertNil(
|
||||
StationRecommendationBadge.badge(netSavingGBP: 0.0, isCheapest: false,
|
||||
bestValueThresholdGBP: threshold)
|
||||
)
|
||||
XCTAssertNil(
|
||||
StationRecommendationBadge.badge(netSavingGBP: -2.50, isCheapest: false,
|
||||
bestValueThresholdGBP: threshold)
|
||||
)
|
||||
}
|
||||
|
||||
func testUnavailableSavingsLeaveOnlyTheCheapestBadge() {
|
||||
XCTAssertEqual(
|
||||
StationRecommendationBadge.badge(netSavingGBP: nil, isCheapest: true,
|
||||
bestValueThresholdGBP: threshold),
|
||||
.cheapest
|
||||
)
|
||||
XCTAssertNil(
|
||||
StationRecommendationBadge.badge(netSavingGBP: nil, isCheapest: false,
|
||||
bestValueThresholdGBP: threshold)
|
||||
)
|
||||
}
|
||||
|
||||
/// A rubbish figure must not become a recommendation.
|
||||
func testNonFiniteFiguresProduceNoBadge() {
|
||||
XCTAssertNil(
|
||||
StationRecommendationBadge.badge(netSavingGBP: .nan, isCheapest: false,
|
||||
bestValueThresholdGBP: threshold)
|
||||
)
|
||||
XCTAssertNil(
|
||||
StationRecommendationBadge.badge(netSavingGBP: 5.0, isCheapest: false,
|
||||
bestValueThresholdGBP: .nan)
|
||||
)
|
||||
}
|
||||
|
||||
/// The threshold is the user's alert setting: set it to zero and any saving
|
||||
/// that is not a loss counts.
|
||||
func testZeroThresholdAwardsBestValueToAnyNonNegativeSaving() {
|
||||
XCTAssertEqual(
|
||||
StationRecommendationBadge.badge(netSavingGBP: 0, isCheapest: false,
|
||||
bestValueThresholdGBP: 0),
|
||||
.bestValue
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import XCTest
|
||||
#if SWIFT_PACKAGE
|
||||
@testable import FuelBoardShared
|
||||
#endif
|
||||
|
||||
/// The Best Value gate's option list.
|
||||
///
|
||||
/// This one array is the figure behind two user-facing things — the BEST VALUE
|
||||
/// badge on a station row and the proximity alert — so its granularity is a
|
||||
/// setting, not an implementation detail. It was four coarse values (50p, £1,
|
||||
/// £2, £5); these tests pin the 50p step and the £5 ceiling so a later edit
|
||||
/// cannot quietly coarsen it back.
|
||||
final class FuelStoreBestValueThresholdTests: XCTestCase {
|
||||
func testOptionsStepByFiftyPenceUpToFivePounds() {
|
||||
let options = FuelStore.bestValueAlertThresholdOptionsGBP
|
||||
XCTAssertEqual(options, [0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0])
|
||||
XCTAssertEqual(options.count, 10)
|
||||
XCTAssertEqual(options.first, 0.5, "the floor is the cheapest worth-the-drive saving offered")
|
||||
XCTAssertEqual(options.last, 5.0, "£5 is the ceiling")
|
||||
}
|
||||
|
||||
/// The store writes the threshold as a string and, on load, rejects any
|
||||
/// value that is not one of these options — falling back to the £1 default.
|
||||
/// So an option that does not survive `String` → `Double` exactly would
|
||||
/// silently discard the user's choice. Multiples of 0.5 are exactly
|
||||
/// representable in binary, and this keeps them that way.
|
||||
func testEveryOptionSurvivesTheStoresStringRoundTrip() {
|
||||
for option in FuelStore.bestValueAlertThresholdOptionsGBP {
|
||||
XCTAssertEqual(Double(String(option)), option,
|
||||
"£\(option) does not round-trip through the store's string encoding")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user