Accurate distances toggle, Approx. distance markers, and Closest-mode speedup
Distance confidence - Every distance surface takes the figure AND its "Approx." marker from one FuelStore.stationDistanceDisplay call (station rows, detail sheet, widget, Live Activity, watch, Siri), so a surface can never mark one figure approximate while showing a routed one. Radius filters and Closest ordering test that same displayed distance, so no row can read a distance outside the radius that filtered it (the reported "14.6 mi inside a 10-mile list"). Accurate distances (Settings -> Distances, default off) - Off: the browse layer serves the straight line, marked Approx., for every row, so two installs agree to the metre and the app makes no route requests at all. - On: routed distances for the whole search area, unmarked, served only from a COMPLETE set anchored within 600 m — a partial fill reverts every row at once rather than mixing road and straight-line bases. - Best Value maths sits deliberately outside the toggle: it keeps using measured road routes and its own confidence marking, so money figures never move with the switch. - Fills run only while the toggle is on and the app is frontmost; capacity raised to 600 stations per pass; completeness tolerates pins Apple Maps has no route for, which otherwise left an area permanently incomplete and the whole list stuck on estimates. Performance (the reported slowdown switching to Closest / launching with it) - FuelStore.accurateDistancesEnabled was a UserDefaults(suiteName:) read per call: a synchronous cfprefsd round-trip measured at ~1.9 us, ~200k calls per Closest sort. Memoised, with the setter keeping it in step. - Both keyed sorts decorate their keys — one distance per station, carried as an index instead of copying whole stations into the decorated array — and the comparison baseline plus per-station journeys now come from ONE pass instead of two full scans per tab. The Stations tab also reuses its single sorted list for the count and the TOP badge instead of sorting the country three times. - Measured on iPhone SE (7,914-station Closest pool): sort 36.6 ms -> 17.1 ms per list, Cheapest 6 ms, Best Value 11 ms. - DEBUG probes: FB_PERF list timing, FB_PERF_BENCH toggle-read benchmark, and FB_DIST_FOCUS now reports the served value, marker and mode (and re-prints when a fill lands). FB_ACCURATE_DISTANCES / FB_SORT_MODE env forms for devices, where devicectl eats a leading-dash argument. Tests: 198, 0 failures. Device and simulator Debug builds green.
This commit is contained in:
@@ -632,6 +632,9 @@ final class RoadDistanceCacheTests: XCTestCase {
|
||||
// test or run would pollute these. Overwrite with an empty, far-away
|
||||
// cache (source at (0,0)) so every test starts from a clean slate.
|
||||
FuelStore.saveRoadDistances(sourceLat: 0, sourceLng: 0, entries: [:])
|
||||
// "Accurate distances" is a persisted setting, so a previous run (or a
|
||||
// test that turned it on) would leak into these. Off is the default.
|
||||
FuelStore.setAccurateDistancesEnabled(false)
|
||||
}
|
||||
|
||||
private func station(_ id: String, _ lat: Double, _ lng: Double) -> FuelStation {
|
||||
@@ -647,6 +650,7 @@ final class RoadDistanceCacheTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testRoadDistanceUsedWhenCachedNear() {
|
||||
FuelStore.setAccurateDistancesEnabled(true)
|
||||
let s = station("a", 51.5074, -0.1278)
|
||||
// Cache a road distance of 3.2 km for this station from the user's fix.
|
||||
FuelStore.saveRoadDistances(sourceLat: 51.6, sourceLng: -0.1,
|
||||
@@ -656,6 +660,7 @@ final class RoadDistanceCacheTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testRoadDistanceNilWhenOriginFar() {
|
||||
FuelStore.setAccurateDistancesEnabled(true)
|
||||
let s = station("a", 51.5074, -0.1278)
|
||||
// Cache built in London, but the user is now ~200 km away -> stale.
|
||||
FuelStore.saveRoadDistances(sourceLat: 51.5074, sourceLng: -0.1278,
|
||||
@@ -668,6 +673,7 @@ final class RoadDistanceCacheTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testRoadDistanceUsedForOtherStationNotFound() {
|
||||
FuelStore.setAccurateDistancesEnabled(true)
|
||||
FuelStore.saveRoadDistances(sourceLat: 51.6, sourceLng: -0.1,
|
||||
entries: ["a": .init(meters: 3200, lat: 51.5074, lng: -0.1278)])
|
||||
// A station that isn't in the cache falls back to straight-line.
|
||||
@@ -677,6 +683,7 @@ final class RoadDistanceCacheTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testRoadDistanceNotServedWhenStationPinDiffers() {
|
||||
FuelStore.setAccurateDistancesEnabled(true)
|
||||
// Route a road distance to station "a" at pin P1.
|
||||
FuelStore.saveRoadDistances(sourceLat: 51.6, sourceLng: -0.1,
|
||||
entries: ["a": .init(meters: 3200, lat: 51.5074, lng: -0.1278)])
|
||||
@@ -689,7 +696,12 @@ final class RoadDistanceCacheTests: XCTestCase {
|
||||
"road value routed to the old pin leaked onto a different coordinate")
|
||||
}
|
||||
|
||||
/// A partial cache legitimately mixes road and straight-line distances for
|
||||
/// the SAVINGS maths (Best Value marks the estimated ones itself), but the
|
||||
/// browse layer only ever serves a COMPLETE set — see the accurate-distances
|
||||
/// tests below.
|
||||
func testPartialRoadCacheCanMixRoadAndStraightLineDistances() {
|
||||
FuelStore.setAccurateDistancesEnabled(true)
|
||||
let routed = station("routed", 51.5074, -0.1278)
|
||||
let estimated = station("estimated", 51.5100, -0.1300)
|
||||
FuelStore.saveRoadDistances(sourceLat: 51.6, sourceLng: -0.1,
|
||||
@@ -707,6 +719,39 @@ final class RoadDistanceCacheTests: XCTestCase {
|
||||
XCTAssertFalse(FuelStore.hasMeasuredReturnRoadDistance(for: estimated, userLat: 51.6, userLng: -0.1))
|
||||
}
|
||||
|
||||
/// The reported bug: "10 miles cheapest" listed Jet Chidswell at 14.6 mi.
|
||||
/// Its straight line was inside 10 miles, its routed distance was not — the
|
||||
/// filter tested the former while the row displayed the latter.
|
||||
func testRadiusFilterUsesTheDisplayedDistanceNotTheStraightLine() {
|
||||
FuelStore.setAccurateDistancesEnabled(true)
|
||||
let sourceLat = 51.6, sourceLng = -0.1
|
||||
let far = station("far", 51.5074, -0.1278) // in by straight line…
|
||||
let near = station("near", 51.5400, -0.1000)
|
||||
FuelStore.saveRoadDistances(
|
||||
sourceLat: sourceLat, sourceLng: sourceLng,
|
||||
entries: ["far": .init(meters: 23_500, lat: 51.5074, lng: -0.1278, returnMeters: 23_500)])
|
||||
|
||||
let tenMilesKM = 10 * 1.60934
|
||||
XCTAssertLessThan(far.distanceKM(to: sourceLat, lng2: sourceLng), tenMilesKM,
|
||||
"fixture: the straight line must sit INSIDE the 10-mile radius")
|
||||
XCTAssertGreaterThan(FuelStore.displayDistanceKM(station: far, userLat: sourceLat, userLng: sourceLng),
|
||||
tenMilesKM, "fixture: the routed distance must sit OUTSIDE it")
|
||||
|
||||
XCTAssertTrue(FuelStore.isWithinDisplayRadius(station: near, radiusKM: tenMilesKM,
|
||||
userLat: sourceLat, userLng: sourceLng))
|
||||
XCTAssertFalse(FuelStore.isWithinDisplayRadius(station: far, radiusKM: tenMilesKM,
|
||||
userLat: sourceLat, userLng: sourceLng),
|
||||
"a station whose row reads 14.6 mi must not appear in a 10-mile list")
|
||||
// The same station is back once the radius actually covers its distance.
|
||||
XCTAssertTrue(FuelStore.isWithinDisplayRadius(station: far, radiusKM: 15 * 1.60934,
|
||||
userLat: sourceLat, userLng: sourceLng))
|
||||
// …and an unknown station (no cached route) still tests its displayed
|
||||
// straight line, so nothing changes for the bulk of the list.
|
||||
let unrouted = station("unrouted", 51.5400, -0.1000)
|
||||
XCTAssertTrue(FuelStore.isWithinDisplayRadius(station: unrouted, radiusKM: tenMilesKM,
|
||||
userLat: sourceLat, userLng: sourceLng))
|
||||
}
|
||||
|
||||
func testLegacyOneWayRoadCacheIsUsableButNotConfirmed() {
|
||||
let legacy = station("legacy", 51.5074, -0.1278)
|
||||
FuelStore.saveRoadDistances(sourceLat: 51.6, sourceLng: -0.1,
|
||||
@@ -723,6 +768,144 @@ final class RoadDistanceCacheTests: XCTestCase {
|
||||
XCTAssertFalse(diagnostics.hasMeasuredReturn)
|
||||
XCTAssertTrue(diagnostics.hasValidCacheEntry)
|
||||
}
|
||||
|
||||
/// A station distance the user can see is marked "Approx." until a routed road
|
||||
/// distance exists for it. Figure and marker come out of ONE cache read, so a
|
||||
/// surface can never show a routed value labelled approximate (or the reverse).
|
||||
func testStationDistanceIsMarkedApproximateUntilRoutingLands() {
|
||||
FuelStore.setAccurateDistancesEnabled(true)
|
||||
let routed = station("routed", 51.5074, -0.1278)
|
||||
let unrouted = station("unrouted", 51.5200, -0.1400)
|
||||
FuelStore.saveRoadDistances(sourceLat: 51.6, sourceLng: -0.1,
|
||||
entries: ["routed": .init(meters: 3200, lat: 51.5074, lng: -0.1278, returnMeters: 3600)])
|
||||
|
||||
let confirmed = FuelStore.stationDistanceDisplay(station: routed, userLat: 51.6, userLng: -0.1)
|
||||
XCTAssertFalse(confirmed.isApproximate, "a cached road distance is not an approximation")
|
||||
XCTAssertEqual(confirmed.km, 3.2, accuracy: 0.0001)
|
||||
XCTAssertEqual(confirmed.label(unit: .miles), "2.0 mi")
|
||||
XCTAssertEqual(confirmed.spokenLabel(unit: .miles), "2.0 miles")
|
||||
|
||||
let approximate = FuelStore.stationDistanceDisplay(station: unrouted, userLat: 51.6, userLng: -0.1)
|
||||
XCTAssertTrue(approximate.isApproximate, "the straight-line fallback must be identified as one")
|
||||
XCTAssertEqual(approximate.km, unrouted.distanceKM(to: 51.6, lng2: -0.1), accuracy: 0.0001)
|
||||
XCTAssertEqual(approximate.label(unit: .miles),
|
||||
"Approx. " + DistanceUnit.miles.format(approximate.km))
|
||||
XCTAssertEqual(approximate.spokenLabel(unit: .kilometers),
|
||||
"Approximately " + DistanceUnit.kilometers.spoken(approximate.km))
|
||||
}
|
||||
|
||||
/// The marker tracks the cache on its own: same station, bare figure once its
|
||||
/// route lands, "Approx." again once the origin moves out of tolerance.
|
||||
func testStationDistanceLabelFollowsTheRoadCache() {
|
||||
FuelStore.setAccurateDistancesEnabled(true)
|
||||
let moved = station("moved", 51.5074, -0.1278)
|
||||
XCTAssertTrue(FuelStore.stationDistanceDisplay(station: moved, userLat: 51.6, userLng: -0.1).isApproximate,
|
||||
"nothing has been routed yet")
|
||||
FuelStore.saveRoadDistances(sourceLat: 51.6, sourceLng: -0.1,
|
||||
entries: ["moved": .init(meters: 3200, lat: 51.5074, lng: -0.1278, returnMeters: 3600)])
|
||||
XCTAssertFalse(FuelStore.stationDistanceDisplay(station: moved, userLat: 51.6, userLng: -0.1).isApproximate,
|
||||
"route data landed, so the marker must drop without any other change")
|
||||
XCTAssertTrue(FuelStore.stationDistanceDisplay(station: moved, userLat: 59.0, userLng: -0.1).isApproximate,
|
||||
"a cache built from another origin must not confirm this one")
|
||||
}
|
||||
|
||||
// MARK: - Accurate distances toggle (browse layer only)
|
||||
|
||||
/// The toggle must be opt-in: unset means off, so a fresh install makes no
|
||||
/// route requests and every device shows the same straight-line figures.
|
||||
func testAccurateDistancesDefaultsToOffWhenUnset() {
|
||||
UserDefaults(suiteName: FuelStore.appGroupSuite)?.removeObject(forKey: FuelStore.accurateDistancesKey)
|
||||
XCTAssertFalse(FuelStore.accurateDistancesEnabled)
|
||||
}
|
||||
|
||||
/// Mode OFF: a COMPLETE routed cache exists and the browse layer must still
|
||||
/// show the straight line, marked Approx., for every row — while the savings
|
||||
/// maths keeps using the measured routes. That boundary is the feature.
|
||||
func testAccurateDistancesOffIgnoresCachedRoutes() {
|
||||
let routed = station("routed", 51.5074, -0.1278)
|
||||
FuelStore.saveRoadDistances(sourceLat: 51.6, sourceLng: -0.1,
|
||||
entries: ["routed": .init(meters: 23_500, lat: 51.5074,
|
||||
lng: -0.1278, returnMeters: 23_500)],
|
||||
complete: true)
|
||||
|
||||
let display = FuelStore.stationDistanceDisplay(station: routed, userLat: 51.6, userLng: -0.1)
|
||||
XCTAssertTrue(display.isApproximate, "the browse layer must never serve routes with the mode off")
|
||||
XCTAssertEqual(display.km, routed.distanceKM(to: 51.6, lng2: -0.1), accuracy: 0.0001)
|
||||
XCTAssertTrue(FuelStore.isWithinDisplayRadius(station: routed, radiusKM: 10 * 1.60934,
|
||||
userLat: 51.6, userLng: -0.1),
|
||||
"with the mode off the radius is a straight-line radius, agreeing with the rows")
|
||||
|
||||
XCTAssertEqual(FuelStore.roadJourneyMeters(for: routed, userLat: 51.6, userLng: -0.1), 47_000,
|
||||
"Best Value keeps its measured routes whichever way the toggle is set")
|
||||
XCTAssertTrue(FuelStore.hasMeasuredReturnRoadDistance(for: routed, userLat: 51.6, userLng: -0.1))
|
||||
}
|
||||
|
||||
/// An incomplete fill must never be half-served: the row that HAS a route
|
||||
/// falls back too, so a list can't read "14.6 mi" beside rows that its radius
|
||||
/// filter measured as straight lines.
|
||||
func testAccurateDistancesOnWithIncompleteSetServesStraightLineForEveryRow() {
|
||||
FuelStore.setAccurateDistancesEnabled(true)
|
||||
let routed = station("routed", 51.5074, -0.1278)
|
||||
FuelStore.saveRoadDistances(sourceLat: 51.6, sourceLng: -0.1,
|
||||
entries: ["routed": .init(meters: 23_500, lat: 51.5074,
|
||||
lng: -0.1278, returnMeters: 23_500)],
|
||||
complete: false)
|
||||
|
||||
let display = FuelStore.stationDistanceDisplay(station: routed, userLat: 51.6, userLng: -0.1)
|
||||
XCTAssertTrue(display.isApproximate)
|
||||
XCTAssertEqual(display.km, routed.distanceKM(to: 51.6, lng2: -0.1), accuracy: 0.0001,
|
||||
"a partial set must not be served, even for a station that has a route")
|
||||
}
|
||||
|
||||
/// Mode ON with a complete set from a location we are standing at: routed
|
||||
/// values, unmarked, and the radius means miles of driving.
|
||||
func testAccurateDistancesOnServesRoutedValuesWhenCompleteAndNear() {
|
||||
FuelStore.setAccurateDistancesEnabled(true)
|
||||
let routed = station("routed", 51.5074, -0.1278)
|
||||
FuelStore.saveRoadDistances(sourceLat: 51.6, sourceLng: -0.1,
|
||||
entries: ["routed": .init(meters: 23_500, lat: 51.5074,
|
||||
lng: -0.1278, returnMeters: 23_500)],
|
||||
complete: true)
|
||||
|
||||
let display = FuelStore.stationDistanceDisplay(station: routed, userLat: 51.6, userLng: -0.1)
|
||||
XCTAssertFalse(display.isApproximate)
|
||||
XCTAssertEqual(display.km, 23.5, accuracy: 0.0001)
|
||||
XCTAssertFalse(FuelStore.isWithinDisplayRadius(station: routed, radiusKM: 10 * 1.60934,
|
||||
userLat: 51.6, userLng: -0.1),
|
||||
"in this mode a 10-mile radius is 10 miles of driving, matching the routed rows")
|
||||
}
|
||||
|
||||
/// Same complete cache, but the user has since driven away: nothing is served
|
||||
/// (every row falls back together) rather than showing routes measured from a
|
||||
/// place the user has left. Walking back inside tolerance serves it again
|
||||
/// with no refill.
|
||||
func testAccurateDistancesOnFallsBackTogetherWhenTheAnchorMoves() {
|
||||
FuelStore.setAccurateDistancesEnabled(true)
|
||||
let routed = station("routed", 51.5074, -0.1278)
|
||||
FuelStore.saveRoadDistances(sourceLat: 51.6, sourceLng: -0.1,
|
||||
entries: ["routed": .init(meters: 23_500, lat: 51.5074,
|
||||
lng: -0.1278, returnMeters: 23_500)],
|
||||
complete: true)
|
||||
let movedLat = 51.6 + 0.018 // ~2 km north, outside the 600 m tolerance
|
||||
XCTAssertTrue(FuelStore.stationDistanceDisplay(station: routed, userLat: movedLat, userLng: -0.1).isApproximate)
|
||||
XCTAssertFalse(FuelStore.stationDistanceDisplay(station: routed, userLat: 51.6, userLng: -0.1).isApproximate)
|
||||
}
|
||||
|
||||
/// The toggle is a browse-layer switch, not a data switch: Best Value's
|
||||
/// journey metres and its measured-return confidence must be identical in
|
||||
/// both positions.
|
||||
func testAccurateDistancesSavingsMathsIsUnaffectedByTheToggle() {
|
||||
let routed = station("routed", 51.5074, -0.1278)
|
||||
FuelStore.saveRoadDistances(sourceLat: 51.6, sourceLng: -0.1,
|
||||
entries: ["routed": .init(meters: 23_500, lat: 51.5074,
|
||||
lng: -0.1278, returnMeters: 23_600)],
|
||||
complete: true)
|
||||
for mode in [false, true] {
|
||||
FuelStore.setAccurateDistancesEnabled(mode)
|
||||
XCTAssertEqual(FuelStore.roadJourneyMeters(for: routed, userLat: 51.6, userLng: -0.1), 47_100)
|
||||
XCTAssertTrue(FuelStore.hasMeasuredReturnRoadDistance(for: routed, userLat: 51.6, userLng: -0.1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Install identity
|
||||
|
||||
@@ -110,9 +110,9 @@ final class FuelSavingsCalculationAuditTests: XCTestCase {
|
||||
|
||||
// 2.05 mi shows as "2.0 mi" at one decimal place — the same caption.
|
||||
XCTAssertEqual(FuelSavingsFormat.extraTravelLabel(extraDistanceMiles: 2.05, unit: .miles),
|
||||
"2.0 mi more driving than closest")
|
||||
"2.0 mi extra return travel")
|
||||
XCTAssertEqual(FuelSavingsFormat.extraTravelLabel(extraDistanceMiles: 2.00, unit: .miles),
|
||||
"2.0 mi more driving than closest")
|
||||
"2.0 mi extra return travel")
|
||||
}
|
||||
|
||||
func testBriefCaseMFGHareAndHoundsIsWorseOff() throws {
|
||||
@@ -242,7 +242,7 @@ final class FuelSavingsCalculationAuditTests: XCTestCase {
|
||||
XCTAssertEqual(result.extraDrivingCostGBP, 0.701613, accuracy: 1e-6)
|
||||
XCTAssertEqual(result.netSavingGBP, 1.217509, accuracy: 1e-6)
|
||||
XCTAssertEqual(result.status, .worthTheDrive)
|
||||
XCTAssertEqual(result.headline(), "Save £1.22 after travel")
|
||||
XCTAssertEqual(result.headline(), "£1.22 better off after travel")
|
||||
}
|
||||
|
||||
func testNearerCandidateIsNeverChargedExtraTravel() throws {
|
||||
@@ -393,7 +393,7 @@ final class FuelSavingsCalculationAuditTests: XCTestCase {
|
||||
XCTAssertEqual(mixed?.result?.distanceBasis, .straightLine,
|
||||
"one unmeasured leg makes the whole estimate an estimate")
|
||||
XCTAssertEqual(mixed?.result?.journeyConfidence, .estimated)
|
||||
XCTAssertEqual(mixed?.result?.headline(compact: true), "~£0.17 worse off after travel")
|
||||
XCTAssertEqual(mixed?.result?.headline(compact: true), "Approx. £0.17 worse off after travel")
|
||||
}
|
||||
|
||||
func testBaselineStationIsLabelledClosestStationNotBreakEven() {
|
||||
|
||||
@@ -313,7 +313,7 @@ final class FuelSavingsEstimateTests: XCTestCase {
|
||||
XCTAssertEqual(result.netSavingGBP, -0.334, accuracy: 0.005)
|
||||
XCTAssertEqual(result.status, .notWorthTheDrive)
|
||||
XCTAssertEqual(result.journeyConfidence, .estimated)
|
||||
XCTAssertEqual(result.headline(compact: true), "~£0.33 worse off after travel")
|
||||
XCTAssertEqual(result.headline(compact: true), "Approx. £0.33 worse off after travel")
|
||||
}
|
||||
|
||||
func testMeasuredRoadRoutesDropTheEstimateMarker() {
|
||||
@@ -378,7 +378,7 @@ final class FuelSavingsEstimateTests: XCTestCase {
|
||||
// £60 buys 37.5 L at 160p; 10p/litre cheaper saves £3.75; 0.4 extra
|
||||
// return miles cost ~£0.09. Net ≈ £3.66.
|
||||
XCTAssertEqual(result.netSavingGBP, 3.659, accuracy: 0.005)
|
||||
XCTAssertEqual(result.headline(compact: true), "~Save £3.66 after travel")
|
||||
XCTAssertEqual(result.headline(compact: true), "Approx. £3.66 better off after travel")
|
||||
}
|
||||
|
||||
func testNeutralResultHeadlineIsBreakEven() {
|
||||
@@ -413,7 +413,7 @@ final class FuelSavingsEstimateTests: XCTestCase {
|
||||
netSavingGBP: 0.52,
|
||||
status: .worthTheDrive,
|
||||
distanceBasis: .roadRoute)
|
||||
XCTAssertEqual(saving.headline(), "Save £0.52 after travel")
|
||||
XCTAssertEqual(saving.headline(), "£0.52 better off after travel")
|
||||
|
||||
let loss = FuelSavingsResult(fillLitres: 34.9,
|
||||
grossSavingGBP: 1.40,
|
||||
@@ -460,11 +460,11 @@ final class FuelSavingsEstimateTests: XCTestCase {
|
||||
func testExtraTravelLabelFollowsTheDisplayUnit() {
|
||||
XCTAssertEqual(
|
||||
FuelSavingsFormat.extraTravelLabel(extraDistanceMiles: 19.9, unit: .miles),
|
||||
"19.9 mi more driving than closest"
|
||||
"19.9 mi extra return travel"
|
||||
)
|
||||
XCTAssertEqual(
|
||||
FuelSavingsFormat.extraTravelLabel(extraDistanceMiles: 19.9, unit: .kilometers),
|
||||
"32.0 km more driving than closest"
|
||||
"32.0 km extra return travel"
|
||||
)
|
||||
XCTAssertNil(FuelSavingsFormat.extraTravelLabel(extraDistanceMiles: 0, unit: .miles),
|
||||
"the baseline row has no extra travel to report")
|
||||
@@ -497,7 +497,7 @@ final class FuelSavingsEstimateTests: XCTestCase {
|
||||
status: .worthTheDrive,
|
||||
distanceBasis: .straightLine
|
||||
)
|
||||
XCTAssertEqual(saving.capsuleHeadline(), "~Save £1.00 after travel")
|
||||
XCTAssertEqual(saving.capsuleHeadline(), "Approx. £1.00 better off after travel")
|
||||
|
||||
let worse = FuelSavingsResult(
|
||||
fillLitres: 30,
|
||||
@@ -510,6 +510,118 @@ final class FuelSavingsEstimateTests: XCTestCase {
|
||||
XCTAssertEqual(worse.capsuleHeadline(), "£0.31 worse off after travel")
|
||||
}
|
||||
|
||||
// MARK: - Estimate wording (Approx. labels)
|
||||
|
||||
/// The estimate marker is the WORD "Approx.", never a "~", and it is applied to
|
||||
/// both directions of the money result.
|
||||
func testEstimatedHeadlinesUseTheWordApproxForBothDirections() {
|
||||
let saving = FuelSavingsResult(
|
||||
fillLitres: 30,
|
||||
grossSavingGBP: 4.00,
|
||||
extraDrivingCostGBP: 2.75,
|
||||
extraDistanceMiles: 6.4,
|
||||
netSavingGBP: 1.25,
|
||||
status: .worthTheDrive,
|
||||
distanceBasis: .straightLine
|
||||
)
|
||||
XCTAssertEqual(saving.headline(), "Approx. £1.25 better off after travel")
|
||||
|
||||
let loss = FuelSavingsResult(
|
||||
fillLitres: 30,
|
||||
grossSavingGBP: 1.40,
|
||||
extraDrivingCostGBP: 2.17,
|
||||
extraDistanceMiles: 6.4,
|
||||
netSavingGBP: -0.77,
|
||||
status: .notWorthTheDrive,
|
||||
distanceBasis: .straightLine
|
||||
)
|
||||
XCTAssertEqual(loss.headline(), "Approx. £0.77 worse off after travel")
|
||||
}
|
||||
|
||||
/// A confirmed road-route comparison must never carry the marker — in either
|
||||
/// direction, and not as a stray tilde either.
|
||||
func testConfirmedHeadlinesCarryNoApproxMarker() {
|
||||
let saving = FuelSavingsResult(
|
||||
fillLitres: 30,
|
||||
grossSavingGBP: 4.00,
|
||||
extraDrivingCostGBP: 2.75,
|
||||
extraDistanceMiles: 6.4,
|
||||
netSavingGBP: 1.25,
|
||||
status: .worthTheDrive,
|
||||
distanceBasis: .roadRoute,
|
||||
journeyConfidence: .confirmed
|
||||
)
|
||||
XCTAssertEqual(saving.headline(), "£1.25 better off after travel")
|
||||
|
||||
let loss = FuelSavingsResult(
|
||||
fillLitres: 30,
|
||||
grossSavingGBP: 1.40,
|
||||
extraDrivingCostGBP: 2.17,
|
||||
extraDistanceMiles: 6.4,
|
||||
netSavingGBP: -0.77,
|
||||
status: .notWorthTheDrive,
|
||||
distanceBasis: .roadRoute,
|
||||
journeyConfidence: .confirmed
|
||||
)
|
||||
XCTAssertEqual(loss.headline(), "£0.77 worse off after travel")
|
||||
|
||||
for headline in [saving.headline(), loss.headline(), saving.capsuleHeadline()] {
|
||||
XCTAssertFalse(headline.contains("Approx"), "\"\(headline)\" must not claim to be an estimate")
|
||||
XCTAssertFalse(headline.contains("~"), "\"\(headline)\" must use a word, not a symbol")
|
||||
}
|
||||
}
|
||||
|
||||
/// The extra-travel caption names the RETURN journey in both states. The
|
||||
/// estimated caption repeats "Approx." deliberately: it describes a different
|
||||
/// value from the verdict above it.
|
||||
func testExtraTravelNamesTheReturnJourney() {
|
||||
XCTAssertEqual(
|
||||
FuelSavingsFormat.extraTravelLabel(extraDistanceMiles: 6.4, unit: .miles),
|
||||
"6.4 mi extra return travel"
|
||||
)
|
||||
XCTAssertEqual(
|
||||
FuelSavingsFormat.extraTravelLabel(extraDistanceMiles: 6.4, unit: .miles, confidence: .estimated),
|
||||
"Approx. 6.4 mi extra return travel"
|
||||
)
|
||||
}
|
||||
|
||||
/// VoiceOver gets words, not abbreviations: "Approx." → "Approximately" and
|
||||
/// "mi"/"km" → "miles"/"kilometres".
|
||||
func testSpokenLabelsSpellOutTheAbbreviations() {
|
||||
let estimated = FuelSavingsResult(
|
||||
fillLitres: 30,
|
||||
grossSavingGBP: 1.40,
|
||||
extraDrivingCostGBP: 2.17,
|
||||
extraDistanceMiles: 6.4,
|
||||
netSavingGBP: -0.77,
|
||||
status: .notWorthTheDrive,
|
||||
distanceBasis: .straightLine
|
||||
)
|
||||
XCTAssertEqual(estimated.spokenHeadline(), "Approximately £0.77 worse off after travel")
|
||||
|
||||
XCTAssertEqual(
|
||||
FuelSavingsFormat.extraTravelSpokenLabel(extraDistanceMiles: 6.4, unit: .miles, confidence: .estimated),
|
||||
"Approximately 6.4 miles extra return travel"
|
||||
)
|
||||
XCTAssertEqual(
|
||||
FuelSavingsFormat.extraTravelSpokenLabel(extraDistanceMiles: 19.9, unit: .kilometers),
|
||||
"32.0 kilometres extra return travel"
|
||||
)
|
||||
|
||||
let confirmed = FuelSavingsResult(
|
||||
fillLitres: 30,
|
||||
grossSavingGBP: 1.40,
|
||||
extraDrivingCostGBP: 2.17,
|
||||
extraDistanceMiles: 6.4,
|
||||
netSavingGBP: -0.77,
|
||||
status: .notWorthTheDrive,
|
||||
distanceBasis: .roadRoute,
|
||||
journeyConfidence: .confirmed
|
||||
)
|
||||
XCTAssertEqual(confirmed.spokenHeadline(), confirmed.headline(),
|
||||
"a confirmed comparison has no abbreviation to spell out")
|
||||
}
|
||||
|
||||
// MARK: - Slider bounds (Settings and the in-list control share these)
|
||||
|
||||
/// Both fill-up controls are slider-only now, so every position the slider
|
||||
|
||||
Reference in New Issue
Block a user