remindwall / RWPureSwift / Tests / DashboardTests / TrafficAlertsTests.swift
Back to remindwall — Current RemindWall
import AppTypes
import ComposableArchitecture
import Dao
import DependenciesTestSupport
import Foundation
import Testing
import TrafficAPI
@testable import Dashboard
@MainActor
@Suite("TrafficAlerts Feature Tests", .dependencies {
$0.defaultDatabase = try! $0.appDatabase()
$0.uuid = .incrementing
// .syncedSetting stamps lastModified when tests set window/origin.
$0.date = .constant(Date(timeIntervalSince1970: 0))
})
struct TrafficAlertsTests {
private static let utc: Calendar = {
var c = Calendar(identifier: .gregorian)
c.timeZone = TimeZone(identifier: "UTC")!
return c
}()
/// April 6, 2026 is a Monday — inside the default Mon–Fri window.
private static let monday0700 = utc.date(from: DateComponents(
year: 2026, month: 4, day: 6, hour: 7, minute: 0
))!
/// April 5, 2026 is a Sunday — outside the default window.
private static let sunday0700 = utc.date(from: DateComponents(
year: 2026, month: 4, day: 5, hour: 7, minute: 0
))!
private static let home = HomeOrigin(latitude: 47.52, longitude: -122.39, name: "Home")
@discardableResult
private func seedRoute(
label: String = "School run",
latitude: Double = 47.5423,
longitude: Double = -122.3866,
normalMinutes: Int = 12,
sortOrder: Int = 0
) async -> MonitoredRoute.ID {
@Dependency(\.defaultDatabase) var database
@Dependency(\.uuid) var uuid
let id = MonitoredRoute.ID(uuid())
try! await database.write { db in
try MonitoredRoute.insert {
MonitoredRoute(
id: id,
label: label,
destinationLatitude: latitude,
destinationLongitude: longitude,
destinationName: label,
normalMinutes: normalMinutes,
sortOrder: sortOrder
)
}.execute(db)
}
return id
}
@Test("out-of-window tick clears the rail without touching the API")
func outOfWindow() async {
await seedRoute()
let store = TestStore(initialState: TrafficAlertsFeature.State()) {
TrafficAlertsFeature()
} withDependencies: {
$0.date = .constant(Self.sunday0700)
$0.calendar = Self.utc
// trafficAPI deliberately unstubbed: a call would fail the test.
}
store.state.$homeOrigin.withLock { $0 = Self.home }
await store.send(.tick)
// The seeded route's default window excludes Sunday; the exact
// receive proves no API call happened.
await store.receive(._routesLoaded([], error: nil))
}
@Test("in-window tick builds one display route per monitored route with late math")
func inWindowFetch() async {
await seedRoute(label: "School run", normalMinutes: 12, sortOrder: 0)
await seedRoute(label: "Ferry run", latitude: 47.6026, longitude: -122.3393, normalMinutes: 25, sortOrder: 1)
let store = TestStore(initialState: TrafficAlertsFeature.State()) {
TrafficAlertsFeature()
} withDependencies: {
$0.date = .constant(Self.monday0700)
$0.calendar = Self.utc
$0.trafficAPI.calculateETA = { _, destination in
// School run gets 18 min (late vs norm 12+5); ferry 20 (fine vs 25).
destination.latitude == 47.5423
? DriveETA(expectedTravelTime: 18 * 60, distanceMeters: 9000)
: DriveETA(expectedTravelTime: 20 * 60, distanceMeters: 12000)
}
}
store.exhaustivity = .off
store.state.$homeOrigin.withLock { $0 = Self.home }
await store.send(.tick)
await store.receive(\._routesLoaded)
let routes = store.state.displayRoutes
#expect(routes.map(\.label) == ["School run", "Ferry run"])
#expect(routes.map(\.etaMinutes) == [18, 20])
#expect(routes.map(\.isLate) == [true, false])
#expect(store.state.lastError == nil)
}
@Test("routes without a home origin surface the fix, not a blank rail")
func missingOrigin() async {
await seedRoute()
let store = TestStore(initialState: TrafficAlertsFeature.State()) {
TrafficAlertsFeature()
} withDependencies: {
$0.date = .constant(Self.monday0700)
$0.calendar = Self.utc
}
await store.send(.tick)
await store.receive(._routesLoaded([], error: "home origin not set")) {
$0.lastError = "home origin not set"
}
#expect(store.state.railCards.map(\.priority) == [.errorChip])
}
@Test("an API failure collapses to a single error chip")
func apiFailure() async {
await seedRoute()
let store = TestStore(initialState: TrafficAlertsFeature.State()) {
TrafficAlertsFeature()
} withDependencies: {
$0.date = .constant(Self.monday0700)
$0.calendar = Self.utc
$0.trafficAPI.calculateETA = { _, _ in throw TrafficAPIError.noRoute }
}
store.exhaustivity = .off
store.state.$homeOrigin.withLock { $0 = Self.home }
await store.send(.tick)
await store.receive(\._routesLoaded)
#expect(store.state.displayRoutes.isEmpty)
#expect(store.state.lastError != nil)
#expect(store.state.railCards.count == 1)
#expect(store.state.railCards.first?.priority == .errorChip)
}
@Test("per-watch gating: a disabled route never drives to the API")
func disabledRouteSkipped() async {
await seedRoute(label: "Active", normalMinutes: 12, sortOrder: 0)
@Dependency(\.defaultDatabase) var database
@Dependency(\.uuid) var uuid
try! await database.write { db in
try MonitoredRoute.insert {
MonitoredRoute(
id: MonitoredRoute.ID(uuid()),
label: "Switched off",
destinationLatitude: 47.61, destinationLongitude: -122.20,
destinationName: "Bellevue",
normalMinutes: 30, sortOrder: 1,
enabled: false
)
}.execute(db)
}
let fetched = LockIsolated<[Double]>([])
let store = TestStore(initialState: TrafficAlertsFeature.State()) {
TrafficAlertsFeature()
} withDependencies: {
$0.date = .constant(Self.monday0700)
$0.calendar = Self.utc
$0.trafficAPI.calculateETA = { _, destination in
fetched.withValue { $0.append(destination.latitude) }
return DriveETA(expectedTravelTime: 10 * 60, distanceMeters: 5000)
}
}
store.exhaustivity = .off
store.state.$homeOrigin.withLock { $0 = Self.home }
await store.send(.tick)
await store.receive(\._routesLoaded)
#expect(fetched.value == [47.5423])
}
@Test("late math: over normal-plus-threshold is late, at the boundary is not")
func lateBoundary() {
let route = MonitoredRoute(
id: MonitoredRoute.ID(UUID()),
label: "School run",
destinationLatitude: 0, destinationLongitude: 0,
destinationName: "School",
normalMinutes: 12,
sortOrder: 0
)
// Threshold is 5: 17 min is the boundary (not late), 18 is late.
#expect(!makeDisplayRoute(route: route, eta: DriveETA(expectedTravelTime: 17 * 60, distanceMeters: 0)).isLate)
#expect(makeDisplayRoute(route: route, eta: DriveETA(expectedTravelTime: 18 * 60, distanceMeters: 0)).isLate)
// Round-up feeds the comparison: 17:10 driving is 18 min, late.
#expect(makeDisplayRoute(route: route, eta: DriveETA(expectedTravelTime: 17 * 60 + 10, distanceMeters: 0)).isLate)
}
@Test("rail cards map late to slowRoute and on-time to onTimeRoute")
func railCardPriorities() {
var state = TrafficAlertsFeature.State()
state.displayRoutes = [
DisplayRoute(
id: MonitoredRoute.ID(UUID()), label: "School run",
etaMinutes: 18, normalMinutes: 12, etaSeconds: 18 * 60, isLate: true
),
DisplayRoute(
id: MonitoredRoute.ID(UUID()), label: "Ferry run",
etaMinutes: 20, normalMinutes: 25, etaSeconds: 20 * 60, isLate: false
),
]
#expect(state.railCards.map(\.priority) == [.slowRoute, .onTimeRoute])
#expect(state.railCards.first?.content == .drive(label: "School run", etaText: "18 min", isLate: true))
// Error chip only appears when there is nothing to show.
state.lastError = "timeout"
#expect(state.railCards.count == 2)
}
}