The Closest label was drawing location.north.fill. A north arrow reads as a bearing — it implies direction of travel toward something, which is the wrong idea for a label that means "this is the station nearest you". The plain location pointer says where a place is without implying a heading, and it is already the glyph the app's other location affordances draw in Settings and on the map. One constant, as before: StationLabelKind.closest.symbol. The badge, the Key legend and both verdict symbols reference it, so nothing else needed touching. The test that pins each label's glyph now expects "location"; its north-arrow expectation was the only failure in the suite. The comment above the verdict symbol still described a north arrow and now describes the pointer. Also bumps CURRENT_PROJECT_VERSION 30 -> 31 for this device install round. Tests: 278 pass.
536 lines
26 KiB
Swift
536 lines
26 KiB
Swift
import XCTest
|
||
#if SWIFT_PACKAGE
|
||
@testable import FuelBoardShared
|
||
#endif
|
||
|
||
final class FuelSavingsCalculatorTests: XCTestCase {
|
||
func testUKMPGConversionUsesImperialGallons() {
|
||
XCTAssertEqual(FuelSavingsCalculator.fuelUsedLitres(distanceMiles: 10, mpg: 45), 1.0102, accuracy: 0.0001)
|
||
}
|
||
|
||
func testPoundsBudgetConvertsToLitresUsingGivenPrice() throws {
|
||
let amount = FuelSavingsFillAmount(value: 40, unit: .pounds)
|
||
let litres = try FuelSavingsCalculator.fillLitres(amount: amount, pricePencePerLitre: 160)
|
||
XCTAssertEqual(litres, 25.0, accuracy: 0.0001)
|
||
}
|
||
|
||
func testLitresAmountPassesThrough() throws {
|
||
let amount = FuelSavingsFillAmount(value: 35, unit: .litres)
|
||
let litres = try FuelSavingsCalculator.fillLitres(amount: amount, pricePencePerLitre: 160)
|
||
XCTAssertEqual(litres, 35, accuracy: 0.0001)
|
||
}
|
||
|
||
func testNetSavingSubtractsAdditionalDrivingFuelCost() throws {
|
||
let result = try FuelSavingsCalculator.calculate(
|
||
mpg: 45,
|
||
amount: FuelSavingsFillAmount(value: 40, unit: .pounds),
|
||
baselinePricePencePerLitre: 160,
|
||
candidatePricePencePerLitre: 150,
|
||
baselineJourneyMiles: 2,
|
||
candidateJourneyMiles: 8
|
||
)
|
||
XCTAssertEqual(result.fillLitres, 26.6667, accuracy: 0.0001)
|
||
XCTAssertEqual(result.grossSavingGBP, 2.6667, accuracy: 0.0001)
|
||
XCTAssertEqual(result.extraDistanceMiles, 6, accuracy: 0.0001)
|
||
XCTAssertEqual(result.extraDrivingCostGBP, 0.9092, accuracy: 0.0001)
|
||
XCTAssertEqual(result.netSavingGBP, 1.7574, accuracy: 0.0001)
|
||
XCTAssertEqual(result.status, .worthTheDrive)
|
||
}
|
||
|
||
func testNegativeAndBreakEvenOutcomes() throws {
|
||
let negative = try FuelSavingsCalculator.calculate(
|
||
mpg: 45,
|
||
amount: FuelSavingsFillAmount(value: 10, unit: .litres),
|
||
baselinePricePencePerLitre: 150,
|
||
candidatePricePencePerLitre: 149,
|
||
baselineJourneyMiles: 1,
|
||
candidateJourneyMiles: 20
|
||
)
|
||
XCTAssertEqual(negative.status, .notWorthTheDrive)
|
||
XCTAssertLessThan(negative.netSavingGBP, 0)
|
||
|
||
let even = try FuelSavingsCalculator.calculate(
|
||
mpg: 45,
|
||
amount: FuelSavingsFillAmount(value: 10, unit: .litres),
|
||
baselinePricePencePerLitre: 150,
|
||
candidatePricePencePerLitre: 150,
|
||
baselineJourneyMiles: 2,
|
||
candidateJourneyMiles: 2
|
||
)
|
||
XCTAssertEqual(even.status, .breakEven)
|
||
}
|
||
|
||
func testInvalidInputsRejected() {
|
||
XCTAssertThrowsError(try FuelSavingsCalculator.fillLitres(amount: FuelSavingsFillAmount(value: 40, unit: .pounds), pricePencePerLitre: 0))
|
||
XCTAssertThrowsError(try FuelSavingsCalculator.calculate(mpg: 0, amount: FuelSavingsFillAmount(value: 40, unit: .pounds), baselinePricePencePerLitre: 160, candidatePricePencePerLitre: 150, baselineJourneyMiles: 1, candidateJourneyMiles: 2))
|
||
XCTAssertThrowsError(try FuelSavingsCalculator.calculate(mpg: 500, amount: FuelSavingsFillAmount(value: 40, unit: .pounds), baselinePricePencePerLitre: 160, candidatePricePencePerLitre: 150, baselineJourneyMiles: 1, candidateJourneyMiles: 2))
|
||
}
|
||
|
||
func testSwitchingPoundsToLitresPreservesPhysicalQuantity() throws {
|
||
let pounds = FuelSavingsFillAmount(value: 40, unit: .pounds)
|
||
let litres = try FuelSavingsCalculator.convert(amount: pounds, to: .litres, referencePricePencePerLitre: 160)
|
||
XCTAssertEqual(litres.unit, .litres)
|
||
XCTAssertEqual(litres.value, 25, accuracy: 0.0001)
|
||
let back = try FuelSavingsCalculator.convert(amount: litres, to: .pounds, referencePricePencePerLitre: 160)
|
||
XCTAssertEqual(back.value, 40, accuracy: 0.0001)
|
||
}
|
||
|
||
func testSortingPlacesUnavailableAfterSavingsAndBreaksTies() {
|
||
let a = FuelSavingsRankedStation(stationID: "a", result: .available(.init(fillLitres: 25, grossSavingGBP: 2, extraDrivingCostGBP: 1, extraDistanceMiles: 3, netSavingGBP: 1, status: .worthTheDrive)), distanceMiles: 5, pricePencePerLitre: 150)
|
||
let b = FuelSavingsRankedStation(stationID: "b", result: .available(.init(fillLitres: 25, grossSavingGBP: 3, extraDrivingCostGBP: 1, extraDistanceMiles: 2, netSavingGBP: 2, status: .worthTheDrive)), distanceMiles: 8, pricePencePerLitre: 149)
|
||
let c = FuelSavingsRankedStation(stationID: "c", result: .unavailable("No route"), distanceMiles: 1, pricePencePerLitre: 140)
|
||
XCTAssertEqual(FuelSavingsCalculator.sortedForWorthTheDrive([a, c, b]).map(\.stationID), ["b", "a", "c"])
|
||
}
|
||
|
||
func testBestValueAlertCandidateRequiresConfiguredSavingsAndThreshold() {
|
||
let stations = [
|
||
station("near", lat: 0, lng: 0, price: 160),
|
||
station("cheap", lat: 0, lng: 0, price: 150),
|
||
]
|
||
let journeys = ["near": 2.0, "cheap": 4.0]
|
||
let off = FuelSavingsSettings(enabled: false, mpg: 45, preferredAmount: .init(value: 40, unit: .pounds))
|
||
XCTAssertNil(FuelSavingsCalculator.bestValueAlertCandidate(
|
||
stations: stations,
|
||
fuel: .e10,
|
||
settings: off,
|
||
amount: off.preferredAmount,
|
||
minimumNetSavingGBP: 1.0,
|
||
journeyMiles: { journeys[$0.id].map { FuelSavingsJourney(miles: $0, basis: .roadRoute) } }
|
||
))
|
||
|
||
let on = FuelSavingsSettings(enabled: true, mpg: 45, preferredAmount: .init(value: 40, unit: .pounds))
|
||
XCTAssertNil(FuelSavingsCalculator.bestValueAlertCandidate(
|
||
stations: stations,
|
||
fuel: .e10,
|
||
settings: on,
|
||
amount: on.preferredAmount,
|
||
minimumNetSavingGBP: 5.0,
|
||
journeyMiles: { journeys[$0.id].map { FuelSavingsJourney(miles: $0, basis: .roadRoute) } }
|
||
))
|
||
}
|
||
|
||
func testBestValueAlertCandidatePicksHighestNetSavingAboveThreshold() throws {
|
||
let stations = [
|
||
station("baseline", lat: 0, lng: 0, price: 160),
|
||
station("small", lat: 0, lng: 0, price: 154),
|
||
station("best", lat: 0, lng: 0, price: 148),
|
||
]
|
||
let journeys = ["baseline": 2.0, "small": 4.0, "best": 6.0]
|
||
let settings = FuelSavingsSettings(enabled: true, mpg: 45, preferredAmount: .init(value: 40, unit: .pounds))
|
||
let candidate = FuelSavingsCalculator.bestValueAlertCandidate(
|
||
stations: stations,
|
||
fuel: .e10,
|
||
settings: settings,
|
||
amount: settings.preferredAmount,
|
||
minimumNetSavingGBP: 1.0,
|
||
journeyMiles: { journeys[$0.id].map { FuelSavingsJourney(miles: $0, basis: .roadRoute) } }
|
||
)
|
||
XCTAssertEqual(candidate?.station.id, "best")
|
||
XCTAssertEqual(candidate?.result.status, .worthTheDrive)
|
||
XCTAssertGreaterThan(candidate?.result.netSavingGBP ?? 0, 1.0)
|
||
}
|
||
|
||
func testBestValueAlertCandidateBreaksTiesByDistanceThenPrice() throws {
|
||
let first = FuelSavingsResult(fillLitres: 25, grossSavingGBP: 3, extraDrivingCostGBP: 1, extraDistanceMiles: 2, netSavingGBP: 2, status: .worthTheDrive)
|
||
let nearer = FuelSavingsRankedStation(stationID: "nearer", result: .available(first), distanceMiles: 4, pricePencePerLitre: 149)
|
||
let farther = FuelSavingsRankedStation(stationID: "farther", result: .available(first), distanceMiles: 8, pricePencePerLitre: 145)
|
||
XCTAssertEqual(FuelSavingsCalculator.sortedForWorthTheDrive([farther, nearer]).first?.stationID, "nearer")
|
||
}
|
||
|
||
private func station(_ id: String, lat: Double, lng: Double, price: Double) -> FuelStation {
|
||
FuelStation(id: id, name: id, brand: id, address: "", postcode: "", lat: lat, lng: lng, prices: [.e10: price], priceUpdated: nil)
|
||
}
|
||
}
|
||
|
||
final class FuelSavingsSettingsTests: XCTestCase {
|
||
func testSwitchingFillUnitStartsAnExplicitNewComparison() {
|
||
let budget = FuelSavingsFillAmount(value: 60, unit: .pounds)
|
||
let litres = FuelSavingsCalculator.newComparisonAmount(afterSwitching: budget, to: .litres)
|
||
XCTAssertEqual(litres, FuelSavingsFillAmount(value: 30, unit: .litres))
|
||
XCTAssertEqual(FuelSavingsCalculator.newComparisonAmount(afterSwitching: litres, to: .pounds),
|
||
FuelSavingsFillAmount(value: 40, unit: .pounds))
|
||
XCTAssertEqual(FuelSavingsCalculator.newComparisonAmount(afterSwitching: budget, to: .pounds), budget)
|
||
}
|
||
|
||
func testSettingsValidationRequiresEnabledMPGAndFillAmount() {
|
||
XCTAssertFalse(FuelSavingsSettings(enabled: false, mpg: 45, preferredAmount: .init(value: 40, unit: .pounds)).isConfigured)
|
||
XCTAssertFalse(FuelSavingsSettings(enabled: true, mpg: 0, preferredAmount: .init(value: 40, unit: .pounds)).isConfigured)
|
||
XCTAssertFalse(FuelSavingsSettings(enabled: true, mpg: 45, preferredAmount: .init(value: 0, unit: .pounds)).isConfigured)
|
||
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 station with no saving, not the cheapest and not the closest states
|
||
/// nothing a badge could carry.
|
||
func testNoSavingNotCheapestAndNotClosestMeansNoBadge() {
|
||
XCTAssertNil(
|
||
StationRecommendationBadge.badge(netSavingGBP: 0.0, isCheapest: false,
|
||
isClosest: false,
|
||
bestValueThresholdGBP: threshold)
|
||
)
|
||
XCTAssertNil(
|
||
StationRecommendationBadge.badge(netSavingGBP: -2.50, isCheapest: false,
|
||
isClosest: false,
|
||
bestValueThresholdGBP: threshold)
|
||
)
|
||
}
|
||
|
||
func testTheClosestStationWearsClosestWhenNothingElseApplies() {
|
||
XCTAssertEqual(
|
||
StationRecommendationBadge.badge(netSavingGBP: 0.0, isCheapest: false,
|
||
isClosest: true,
|
||
bestValueThresholdGBP: threshold),
|
||
.closest
|
||
)
|
||
// Closest is a position, so it does not need a savings figure to hold.
|
||
XCTAssertEqual(
|
||
StationRecommendationBadge.badge(netSavingGBP: nil, isCheapest: false,
|
||
isClosest: true,
|
||
bestValueThresholdGBP: threshold),
|
||
.closest
|
||
)
|
||
}
|
||
|
||
/// The nearest forecourt is usually also the cheapest, so without the
|
||
/// ordering the Closest label would win by accident and hide the price news.
|
||
func testCheapestOutranksClosestWhenBothApply() {
|
||
XCTAssertEqual(
|
||
StationRecommendationBadge.badge(netSavingGBP: 0.42, isCheapest: true,
|
||
isClosest: true,
|
||
bestValueThresholdGBP: threshold),
|
||
.cheapest
|
||
)
|
||
}
|
||
|
||
func testBestValueOutranksBothCheapestAndClosest() {
|
||
XCTAssertEqual(
|
||
StationRecommendationBadge.badge(netSavingGBP: 3.15, isCheapest: true,
|
||
isClosest: true,
|
||
bestValueThresholdGBP: threshold),
|
||
.bestValue
|
||
)
|
||
}
|
||
|
||
/// The whole ladder in one place, so a future case cannot slide in between
|
||
/// two rungs unnoticed: Best Value, then Cheapest, then Closest, then none.
|
||
func testLabelPrecedenceIsTotalAcrossEveryCombination() {
|
||
let cases: [(saving: Double, cheapest: Bool, closest: Bool,
|
||
badge: StationRecommendationBadge?)] = [
|
||
(3.15, true, true, .bestValue),
|
||
(3.15, false, true, .bestValue),
|
||
(3.15, false, false, .bestValue),
|
||
(0.42, true, true, .cheapest),
|
||
(0.42, true, false, .cheapest),
|
||
(0.42, false, true, .closest),
|
||
(0.42, false, false, nil)
|
||
]
|
||
for c in cases {
|
||
XCTAssertEqual(
|
||
StationRecommendationBadge.badge(
|
||
netSavingGBP: c.saving, isCheapest: c.cheapest, isClosest: c.closest,
|
||
bestValueThresholdGBP: threshold
|
||
),
|
||
c.badge,
|
||
"saving \(c.saving), cheapest \(c.cheapest), closest \(c.closest)"
|
||
)
|
||
}
|
||
}
|
||
|
||
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
|
||
)
|
||
}
|
||
}
|
||
|
||
final class KeySheetContentTests: XCTestCase {
|
||
func testPriceRatingsExplainTheImplementedRAGBoundaries() {
|
||
XCTAssertEqual(KeySheetContent.priceRatings, [
|
||
.init(label: "Good price", description: "Within 1.5p/L of the cheapest."),
|
||
.init(label: "Okay", description: "More than 1.5p/L, up to 4p/L above the cheapest."),
|
||
.init(label: "Pricey", description: "More than 4p/L above the cheapest.")
|
||
])
|
||
}
|
||
|
||
func testSavingsExplainVerdictsWithoutImplyingMinimumSavingQualification() {
|
||
XCTAssertEqual(KeySheetContent.savings, [
|
||
.init(label: "Saves", description: "You’ll save money compared with your closest station, including travel costs."),
|
||
.init(label: "Break even", description: "The saving or extra cost is negligible."),
|
||
// "Costs" — the listing capsule's own headline, not "Costs more".
|
||
.init(label: "Costs", description: "Filling up here costs more than at your closest station, including travel.")
|
||
])
|
||
XCTAssertFalse(KeySheetContent.savings.contains { $0.label == "Small saving" })
|
||
}
|
||
|
||
/// Every Key label is sentence case: the capsules are read as phrases, and an
|
||
/// all-caps set reads as shouting beside the sentence-case copy around it.
|
||
func testEveryKeyLabelIsSentenceCase() {
|
||
let labels = (KeySheetContent.priceRatings
|
||
+ KeySheetContent.savings
|
||
+ KeySheetContent.stationLabels).map(\.label)
|
||
for label in labels {
|
||
XCTAssertNotEqual(label, label.uppercased(),
|
||
"\(label) must be sentence case, not all caps")
|
||
}
|
||
XCTAssertEqual(KeySheetContent.stationLabels.map(\.label),
|
||
["Best value", "Cheapest", "Closest"])
|
||
}
|
||
|
||
/// The Key dispatches its indicators on `StationLabelKind`, so every kind must
|
||
/// have exactly one entry in the legend — and each entry's label must be the
|
||
/// kind's own, or the row would describe one label and draw another.
|
||
func testEveryStationLabelKindHasOneMatchingLegendEntry() {
|
||
XCTAssertEqual(StationLabelKind.allCases.count, KeySheetContent.stationLabels.count)
|
||
for kind in StationLabelKind.allCases {
|
||
let matches = KeySheetContent.stationLabels.filter { $0.label == kind.label }
|
||
XCTAssertEqual(matches.count, 1, "\(kind) must own exactly one legend entry")
|
||
}
|
||
}
|
||
|
||
/// Recommended symbols, pinned so a rename cannot silently blank a badge.
|
||
func testStationLabelKindsUseTheirRecommendedSymbols() {
|
||
XCTAssertEqual(StationLabelKind.bestValue.symbol, "medal")
|
||
XCTAssertEqual(StationLabelKind.cheapest.symbol, "tag")
|
||
XCTAssertEqual(StationLabelKind.closest.symbol, "location")
|
||
// No trophy: "Top" is gone. One state, one label.
|
||
XCTAssertFalse(StationLabelKind.allCases.map(\.symbol).contains("trophy"))
|
||
}
|
||
|
||
func testStationLabelsDescribeTheCurrentQualificationAndRangeRules() {
|
||
XCTAssertEqual(KeySheetContent.stationLabels, [
|
||
.init(label: "Best value", description: "Meets your minimum saving after travel costs."),
|
||
.init(label: "Cheapest", description: "The lowest fuel price per litre within your selected range."),
|
||
.init(label: "Closest", description: "The closest station by journey distance.")
|
||
])
|
||
}
|
||
|
||
/// "Top" is retired. It and "Cheapest" were two words for the same flag, and the
|
||
/// app drifted between them by device: the phone's rows said Cheapest while the
|
||
/// watch said Top for the identical state. Nothing may reintroduce the synonym.
|
||
func testTopLabelIsRetired() {
|
||
XCTAssertFalse(StationLabelKind.allCases.contains { $0.label == "Top" })
|
||
XCTAssertFalse(KeySheetContent.stationLabels.contains { $0.label == "Top" })
|
||
// The concept is not lost — it is called Cheapest, which is also the word
|
||
// every piece of favourites copy uses.
|
||
XCTAssertTrue(StationLabelKind.allCases.contains { $0.label == "Cheapest" })
|
||
}
|
||
|
||
/// The closest label is one word everywhere: the badge, the verdict capsule the
|
||
/// baseline station wears, and the Key all read it from `baselineHeadline`.
|
||
func testClosestLabelIsTheSameWordEverywhere() {
|
||
XCTAssertEqual(FuelSavingsResult.baselineHeadline, "Closest")
|
||
XCTAssertEqual(StationLabelKind.closest.label, "Closest")
|
||
let baseline = FuelSavingsResult(
|
||
fillLitres: 30,
|
||
grossSavingGBP: 0,
|
||
extraDrivingCostGBP: 0,
|
||
extraDistanceMiles: 0,
|
||
netSavingGBP: 0,
|
||
status: .baseline,
|
||
journeyConfidence: .confirmed
|
||
)
|
||
XCTAssertEqual(baseline.headline(), "Closest")
|
||
XCTAssertEqual(baseline.headline(), StationLabelKind.closest.label)
|
||
}
|
||
|
||
func testFavouriteAndHelpCopyRemainPinned() {
|
||
XCTAssertEqual(
|
||
KeySheetContent.favourite,
|
||
.init(label: "Favourites", description: "Star a station to save it to your Favourites.")
|
||
)
|
||
// The link names the calculation, not the feature: the sheet explains the
|
||
// fill-up, the return trip and the total, of which the Best Value gate is
|
||
// only the last step.
|
||
XCTAssertEqual(KeySheetContent.helpTitle, "How does FuelBoard calculate value?")
|
||
// The sheet title is a short form of the question, NOT the question itself:
|
||
// a nav title clamps to the bar, and the full question truncated to
|
||
// "How does FuelBoard…" at both display modes.
|
||
XCTAssertEqual(KeySheetContent.helpSheetTitle, "How value is calculated")
|
||
XCTAssertLessThan(KeySheetContent.helpSheetTitle.count, KeySheetContent.helpTitle.count)
|
||
}
|
||
}
|
||
|
||
/// The More info sheet's ranking capsules. Every rule must come from the shared
|
||
/// badge logic — the sheet may show more labels than a row (which shows one), but
|
||
/// it must not invent a qualification of its own.
|
||
final class StationRankingTests: XCTestCase {
|
||
private let threshold = 2.50
|
||
|
||
func testNoQualificationProducesNoCapsules() {
|
||
XCTAssertEqual(
|
||
StationRanking.kinds(netSavingGBP: 0.40, isCheapestInRange: false,
|
||
bestValueThresholdGBP: threshold),
|
||
[]
|
||
)
|
||
}
|
||
|
||
func testBestValueUsesTheSameGateAsTheListingBadge() {
|
||
// Exactly at the threshold qualifies, matching StationRecommendationBadge.
|
||
XCTAssertEqual(
|
||
StationRanking.kinds(netSavingGBP: threshold, isCheapestInRange: false,
|
||
bestValueThresholdGBP: threshold),
|
||
[.bestValue]
|
||
)
|
||
// A hair under does not.
|
||
XCTAssertEqual(
|
||
StationRanking.kinds(netSavingGBP: threshold - 0.01, isCheapestInRange: false,
|
||
bestValueThresholdGBP: threshold),
|
||
[]
|
||
)
|
||
}
|
||
|
||
/// Cheapest is the only ranking label besides Best value. Whichever tab opened
|
||
/// the sheet — Stations or Favourites — it means the lowest price among the
|
||
/// stations on that screen, so there is no second cheapest-flavoured capsule.
|
||
func testCheapestIsTheOnlyCheapestFlavouredLabel() {
|
||
XCTAssertEqual(
|
||
StationRanking.kinds(netSavingGBP: nil, isCheapestInRange: true,
|
||
bestValueThresholdGBP: threshold),
|
||
[.cheapest]
|
||
)
|
||
XCTAssertEqual(
|
||
StationRanking.kinds(netSavingGBP: nil, isCheapestInRange: false,
|
||
bestValueThresholdGBP: threshold),
|
||
[]
|
||
)
|
||
}
|
||
|
||
func testCapsulesAreOrderedBestValueThenCheapest() {
|
||
XCTAssertEqual(
|
||
StationRanking.kinds(netSavingGBP: 9.99, isCheapestInRange: true,
|
||
bestValueThresholdGBP: threshold),
|
||
[.bestValue, .cheapest]
|
||
)
|
||
}
|
||
|
||
/// The sheet has room for every qualification, so a station holding all three
|
||
/// shows all three — in the Key's order, which is also the row's precedence,
|
||
/// so the sheet reads left-to-right the same way a row resolves top-to-bottom.
|
||
func testCapsulesAreOrderedBestValueCheapestClosest() {
|
||
XCTAssertEqual(
|
||
StationRanking.kinds(netSavingGBP: 9.99, isCheapestInRange: true,
|
||
isClosestInRange: true, bestValueThresholdGBP: threshold),
|
||
[.bestValue, .cheapest, .closest]
|
||
)
|
||
}
|
||
|
||
/// The closest station with no saving and no price advantage still earns its
|
||
/// label on the sheet — the one case a listing row cannot show, because there
|
||
/// Best Value and Cheapest are both unavailable and Closest wins the slot.
|
||
func testClosestAloneEarnsOneCapsule() {
|
||
XCTAssertEqual(
|
||
StationRanking.kinds(netSavingGBP: nil, isCheapestInRange: false,
|
||
isClosestInRange: true, bestValueThresholdGBP: threshold),
|
||
[.closest]
|
||
)
|
||
XCTAssertEqual(
|
||
StationRanking.kinds(netSavingGBP: 0.0, isCheapestInRange: false,
|
||
isClosestInRange: true, bestValueThresholdGBP: threshold),
|
||
[.closest]
|
||
)
|
||
}
|
||
|
||
/// Closest must not sneak in when the host did not flag it — the sheet never
|
||
/// invents a rule of its own.
|
||
func testNoClosestFlagMeansNoClosestCapsule() {
|
||
XCTAssertEqual(
|
||
StationRanking.kinds(netSavingGBP: nil, isCheapestInRange: true,
|
||
isClosestInRange: false, bestValueThresholdGBP: threshold),
|
||
[.cheapest]
|
||
)
|
||
}
|
||
|
||
/// A row shows ONE badge; the sheet may show several. Both must agree on the
|
||
/// Best Value decision, or the sheet would claim a label the list behind it
|
||
/// withheld.
|
||
func testSheetAgreesWithTheRowOnBestValue() {
|
||
for netSaving in [-3.0, 0.0, 2.49, 2.50, 12.0, .nan] as [Double] {
|
||
let rowSaysBestValue = StationRecommendationBadge.badge(
|
||
netSavingGBP: netSaving, isCheapest: false, bestValueThresholdGBP: threshold
|
||
) == .bestValue
|
||
let sheetSaysBestValue = StationRanking.kinds(
|
||
netSavingGBP: netSaving, isCheapestInRange: false,
|
||
bestValueThresholdGBP: threshold
|
||
).contains(.bestValue)
|
||
XCTAssertEqual(rowSaysBestValue, sheetSaysBestValue, "disagreed on net saving \(netSaving)")
|
||
}
|
||
}
|
||
}
|