Optional Fuel Savings feature (off by default): vehicle MPG in UK imperial gallons, fill amount in pounds or litres, persisted through the existing keychain/app-group store. Ranks the WHOLE station pool by estimated net saving (fuel-price saving at each pump minus the extra return-trip driving cost) using MapKit road distances with a straight-line fallback marked (est.). The closest station with a valid price for the selected fuel is the comparison baseline and is labelled "Closest station"; other rows read "Save £X.XX after travel" / "£X.XX worse off after travel" / "Break even". Labels live in the shared model so they are unit-tested. The TOP badge still marks the cheapest pump price and reads "CHEAPEST" when the list is ranked by net saving. Road routing now prioritises the Worth-the-Drive candidates plus the baseline and merges cache entries instead of replacing them, so far-out stations get real routes rather than permanent straight-line estimates. Numeric fields gain a Done button above the keypad (decimalPad has no Return key); the fill-up control is hidden unless the feature is configured; Favourites shares one session fill amount with the station list. 131 tests.
70 lines
3.0 KiB
Swift
70 lines
3.0 KiB
Swift
import XCTest
|
|
@testable import FuelBoardShared
|
|
|
|
/// Shared payload storage must survive an install whose provisioning profile
|
|
/// has NO `com.apple.security.application-groups` entitlement (free/personal
|
|
/// team and SideStore installs) — the exact situation this test runner is in.
|
|
///
|
|
/// Regression guard: `appGroupFileURL` used to return nil without a group
|
|
/// container, so every save silently failed and every load returned empty. That
|
|
/// left `loadStations()` permanently empty (letting the freshness gate skip the
|
|
/// fetch for ever, i.e. a blank station list) and re-downloaded the ~2.8 MB
|
|
/// dump on every refresh. Storage now falls back to the app's own container.
|
|
final class FuelStoreFileCacheTests: XCTestCase {
|
|
|
|
func testSharedFileURLExistsWithoutAppGroupContainer() {
|
|
XCTAssertNotNil(
|
|
FuelStore.appGroupFileURL("fuelboard.test.probe.json"),
|
|
"storage must resolve to a writable location even when the App Group container is unprovisioned"
|
|
)
|
|
}
|
|
|
|
func testSharedFileRoundTripsWithoutAppGroupContainer() throws {
|
|
let name = "fuelboard.test.roundtrip.json"
|
|
let payload = Data("hello-cache".utf8)
|
|
|
|
XCTAssertTrue(
|
|
FuelStore.saveAppGroupFile(payload, name: name),
|
|
"save must succeed without an App Group container"
|
|
)
|
|
XCTAssertEqual(FuelStore.loadAppGroupFile(name), payload)
|
|
|
|
if let url = FuelStore.appGroupFileURL(name) {
|
|
try? FileManager.default.removeItem(at: url)
|
|
}
|
|
}
|
|
|
|
/// The dump cache is what stops a ~2.8 MB re-download on every refresh, so
|
|
/// it must round-trip too — not just the station list.
|
|
func testDumpCacheRoundTripsWithoutAppGroupContainer() {
|
|
let day = "2026-01-01"
|
|
let data = Data(repeating: 7, count: 4096)
|
|
|
|
MirrorFuelProvider.saveDumpCache(day: day, data: data)
|
|
let cache = MirrorFuelProvider.loadDumpCache()
|
|
|
|
XCTAssertEqual(cache?.day, day, "cached dump day must survive a round-trip")
|
|
XCTAssertEqual(cache?.data, data, "cached dump payload must survive a round-trip")
|
|
|
|
if let url = FuelStore.appGroupFileURL(MirrorFuelProvider.liveDumpCacheFileName) {
|
|
try? FileManager.default.removeItem(at: url)
|
|
}
|
|
}
|
|
|
|
/// The road-distance cache is memoized in memory because re-reading it per
|
|
/// station (it lives in the keychain) stalled the main thread past the iOS
|
|
/// launch watchdog. A save must therefore invalidate the memo — a stale
|
|
/// in-memory cache would show wrong distances after a route refresh.
|
|
func testRoadDistanceMemoRefreshesAfterSave() {
|
|
FuelStore.saveRoadDistances(sourceLat: 51.0, sourceLng: -1.0, entries: [:])
|
|
XCTAssertEqual(FuelStore.loadRoadDistances()?.sourceLat, 51.0)
|
|
|
|
FuelStore.saveRoadDistances(sourceLat: 52.0, sourceLng: -2.0, entries: [:])
|
|
|
|
XCTAssertEqual(
|
|
FuelStore.loadRoadDistances()?.sourceLat, 52.0,
|
|
"load must reflect the newest save, not a stale memoized cache"
|
|
)
|
|
}
|
|
}
|