Skip to content

Commit

Permalink
test: Update tests for new stop page and view
Browse files Browse the repository at this point in the history
  • Loading branch information
EmmaSimon committed Nov 21, 2024
1 parent 4a60e67 commit 02bc84d
Show file tree
Hide file tree
Showing 8 changed files with 493 additions and 445 deletions.
4 changes: 4 additions & 0 deletions iosApp/iosApp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@
9A2005CB2B97B68700F562E1 /* UpcomingTripView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A2005CA2B97B68700F562E1 /* UpcomingTripView.swift */; };
9A2BCBDE2CED365200FB2913 /* StopDetailsPageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A2BCBDD2CED365200FB2913 /* StopDetailsPageTests.swift */; };
9A2BCBE02CED366300FB2913 /* StopDetailsViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A2BCBDF2CED366300FB2913 /* StopDetailsViewTests.swift */; };
9A2BCBE22CEE8A9F00FB2913 /* StopDetailsPageHandlerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A2BCBE12CEE8A9F00FB2913 /* StopDetailsPageHandlerExtension.swift */; };
9A320F962CD3E4CF0096D7B1 /* UpcomingTripAccessibilityFormatters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A320F952CD3E4CF0096D7B1 /* UpcomingTripAccessibilityFormatters.swift */; };
9A37F3052BACCC40001714FE /* DoubleRoundedExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A37F3042BACCC40001714FE /* DoubleRoundedExtension.swift */; };
9A37F3072BACCCA5001714FE /* CoordinateExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A37F3062BACCCA5001714FE /* CoordinateExtension.swift */; };
Expand Down Expand Up @@ -397,6 +398,7 @@
9A2005CA2B97B68700F562E1 /* UpcomingTripView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpcomingTripView.swift; sourceTree = "<group>"; };
9A2BCBDD2CED365200FB2913 /* StopDetailsPageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StopDetailsPageTests.swift; sourceTree = "<group>"; };
9A2BCBDF2CED366300FB2913 /* StopDetailsViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StopDetailsViewTests.swift; sourceTree = "<group>"; };
9A2BCBE12CEE8A9F00FB2913 /* StopDetailsPageHandlerExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StopDetailsPageHandlerExtension.swift; sourceTree = "<group>"; };
9A320F952CD3E4CF0096D7B1 /* UpcomingTripAccessibilityFormatters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpcomingTripAccessibilityFormatters.swift; sourceTree = "<group>"; };
9A37F3042BACCC40001714FE /* DoubleRoundedExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DoubleRoundedExtension.swift; sourceTree = "<group>"; };
9A37F3062BACCCA5001714FE /* CoordinateExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoordinateExtension.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1063,6 +1065,7 @@
isa = PBXGroup;
children = (
9ACE4FCF2CE6707900FEB006 /* StopDetailsPage.swift */,
9A2BCBE12CEE8A9F00FB2913 /* StopDetailsPageHandlerExtension.swift */,
9ACE4FD12CE6709B00FEB006 /* StopDetailsView.swift */,
9ACE4FD32CE6D17D00FEB006 /* TripDetailsView.swift */,
);
Expand Down Expand Up @@ -1491,6 +1494,7 @@
6E2027902BD989AC0037554F /* ProductionAppView.swift in Sources */,
6E04D4302C1A17340055FD99 /* StopDeparturesSummaryList.swift in Sources */,
9A74A2112BE2D71400E57102 /* AnnotationLabel.swift in Sources */,
9A2BCBE22CEE8A9F00FB2913 /* StopDetailsPageHandlerExtension.swift in Sources */,
6EEF219E2BF2927E0023A3E9 /* VehicleCardView.swift in Sources */,
9A9E7DCF2C2200C9000DA1FD /* LineHeader.swift in Sources */,
8C05C5812CD568DE000381E8 /* MoreNavLink.swift in Sources */,
Expand Down
5 changes: 2 additions & 3 deletions iosApp/iosApp/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -264,15 +264,14 @@ struct ContentView: View {
// Otherwise only the header animates
TabView {
StopDetailsPage(
viewportProvider: viewportProvider,
stopId: stopId,
stopFilter: stopFilter,
tripFilter: tripFilter,
errorBannerVM: errorBannerVM,
nearbyVM: nearbyVM,
mapVM: mapVM
mapVM: mapVM,
viewportProvider: viewportProvider
)

.toolbar(.hidden, for: .tabBar)
.onAppear {
let filtered = stopFilter != nil
Expand Down
300 changes: 66 additions & 234 deletions iosApp/iosApp/Pages/StopDetails/StopDetailsPage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,7 @@ import SwiftPhoenixClient
import SwiftUI

struct StopDetailsPage: View {
var analytics: StopDetailsAnalytics = AnalyticsProvider.shared
let globalRepository: IGlobalRepository
@State var globalResponse: GlobalResponse?
@ObservedObject var viewportProvider: ViewportProvider
let schedulesRepository: ISchedulesRepository
@State var schedulesResponse: ScheduleResponse?
var pinnedRouteRepository = RepositoryDI().pinnedRoutes
var togglePinnedUsecase = UsecaseDI().toggledPinnedRouteUsecase

@State var predictionsRepository: IPredictionsRepository
var stopId: String

var stopFilter: StopDetailsFilter?
var tripFilter: TripDetailsFilter?

Expand All @@ -34,88 +23,92 @@ struct StopDetailsPage: View {
@ObservedObject var errorBannerVM: ErrorBannerViewModel
@ObservedObject var nearbyVM: NearbyViewModel
@ObservedObject var mapVM: MapViewModel
@ObservedObject var viewportProvider: ViewportProvider

let pinnedRouteRepository = RepositoryDI().pinnedRoutes
let globalRepository: IGlobalRepository
let predictionsRepository: IPredictionsRepository
let schedulesRepository: ISchedulesRepository
let togglePinnedUsecase = UsecaseDI().toggledPinnedRouteUsecase
let tripPredictionsRepository: ITripPredictionsRepository
let tripRepository: ITripRepository
let vehicleRepository: IVehicleRepository

@State var globalResponse: GlobalResponse?
@State var pinnedRoutes: Set<String> = []
@State var predictionsByStop: PredictionsByStopJoinResponse?
@State var schedulesResponse: ScheduleResponse?

var analytics: StopDetailsAnalytics = AnalyticsProvider.shared
let inspection = Inspection<Self>()

var didAppear: ((Self) -> Void)?

init(
globalRepository: IGlobalRepository = RepositoryDI().global,
schedulesRepository: ISchedulesRepository = RepositoryDI().schedules,
predictionsRepository: IPredictionsRepository = RepositoryDI().predictions,
viewportProvider: ViewportProvider,
stopId: String,
stopFilter: StopDetailsFilter?,
tripFilter: TripDetailsFilter?,
internalDepartures: StopDetailsDepartures? = nil,
internalDepartures _: StopDetailsDepartures? = nil,

errorBannerVM: ErrorBannerViewModel,
nearbyVM: NearbyViewModel,
mapVM: MapViewModel
mapVM: MapViewModel,
viewportProvider: ViewportProvider,

globalRepository: IGlobalRepository = RepositoryDI().global,
predictionsRepository: IPredictionsRepository = RepositoryDI().predictions,
schedulesRepository: ISchedulesRepository = RepositoryDI().schedules,
tripPredictionsRepository: ITripPredictionsRepository = RepositoryDI().tripPredictions,
tripRepository: ITripRepository = RepositoryDI().trip,
vehicleRepository: IVehicleRepository = RepositoryDI().vehicle
) {
self.globalRepository = globalRepository
self.schedulesRepository = schedulesRepository
self.predictionsRepository = predictionsRepository
self.viewportProvider = viewportProvider
self.stopId = stopId
self.stopFilter = stopFilter
self.tripFilter = tripFilter
self.internalDepartures = internalDepartures // only for testing

self.errorBannerVM = errorBannerVM
self.nearbyVM = nearbyVM
self.mapVM = mapVM
self.viewportProvider = viewportProvider

self.globalRepository = globalRepository
self.schedulesRepository = schedulesRepository
self.predictionsRepository = predictionsRepository
self.tripPredictionsRepository = tripPredictionsRepository
self.tripRepository = tripRepository
self.vehicleRepository = vehicleRepository
}

@ViewBuilder
var stopDetails: some View {
StopDetailsView(
stopId: stopId,
stopFilter: stopFilter,
tripFilter: tripFilter,
setStopFilter: { filter in nearbyVM.setLastStopDetailsFilter(stopId, filter) },
setTripFilter: { filter in nearbyVM.setLastTripDetailsFilter(stopId, filter) },
departures: internalDepartures,
global: globalResponse,
pinnedRoutes: pinnedRoutes,
togglePinnedRoute: togglePinnedRoute,
now: now,
errorBannerVM: errorBannerVM,
nearbyVM: nearbyVM,
mapVM: mapVM,
tripPredictionsRepository: tripPredictionsRepository,
tripRepository: tripRepository,
vehicleRepository: vehicleRepository
)
}

var body: some View {
VStack {
StopDetailsView(
stopId: stopId,
stopFilter: stopFilter,
tripFilter: tripFilter,
setStopFilter: { filter in nearbyVM.setLastStopDetailsFilter(stopId, filter) },
setTripFilter: { filter in nearbyVM.setLastTripDetailsFilter(stopId, filter) },
departures: internalDepartures,
errorBannerVM: errorBannerVM,
nearbyVM: nearbyVM,
mapVM: mapVM,
now: now,
pinnedRoutes: pinnedRoutes,
togglePinnedRoute: togglePinnedRoute
)
.onAppear {
loadEverything()
didAppear?(self)
}
.onChange(of: stopId) { nextStopId in
changeStop(nextStopId)
}
.onChange(of: globalResponse) { _ in
updateDepartures(stopId)
}
.onChange(of: pinnedRoutes) { _ in
updateDepartures(stopId)
}
.onChange(of: predictionsByStop) { newPredictionsByStop in
updateDepartures(stopId, newPredictionsByStop)
}
.onChange(of: schedulesResponse) { _ in
updateDepartures(stopId)
}
.onChange(of: stopFilter) { nextStopFilter in
nearbyVM.setLastTripDetailsFilter(stopId, internalDepartures?.autoTripFilter(
stopFilter: nextStopFilter,
currentTripFilter: tripFilter,
filterAtTime: now.toKotlinInstant()
))
}
.onChange(of: internalDepartures) { nextDepartures in
nearbyVM.setLastTripDetailsFilter(stopId, nextDepartures?.autoTripFilter(
stopFilter: stopFilter,
currentTripFilter: tripFilter,
filterAtTime: now.toKotlinInstant()
))
}
stopDetails
.onChange(of: stopId) { nextStopId in changeStop(nextStopId) }
.onChange(of: globalResponse) { nextGlobal in updateDepartures(globalResponse: nextGlobal) }
.onChange(of: pinnedRoutes) { nextPinned in updateDepartures(pinnedRoutes: nextPinned) }
.onChange(of: predictionsByStop) { nextPredictions in updateDepartures(predictionsByStop: nextPredictions) }
.onChange(of: schedulesResponse) { nextSchedules in updateDepartures(schedulesResponse: nextSchedules) }
.onChange(of: stopFilter) { nextStopFilter in setTripFilter(stopFilter: nextStopFilter) }
.onChange(of: internalDepartures) { nextDepartures in setTripFilter(departures: nextDepartures) }
.onAppear { loadEverything() }
.onReceive(inspection.notice) { inspection.visit(self, $0) }
.task(id: stopId) {
while !Task.isCancelled {
Expand All @@ -125,9 +118,7 @@ struct StopDetailsPage: View {
try? await Task.sleep(for: .seconds(5))
}
}
.onDisappear {
leavePredictions()
}
.onDisappear { leavePredictions() }
.withScenePhaseHandlers(
onActive: {
if let predictionsByStop,
Expand All @@ -143,164 +134,5 @@ struct StopDetailsPage: View {
errorBannerVM.loadingWhenPredictionsStale = true
}
)
}
}

func loadEverything() {
loadGlobalData()
fetchStopData(stopId)
loadPinnedRoutes()
}

@MainActor
func activateGlobalListener() async {
for await globalData in globalRepository.state {
globalResponse = globalData
}
}

func loadGlobalData() {
Task(priority: .high) {
await activateGlobalListener()
}
Task {
await fetchApi(
errorBannerVM.errorRepository,
errorKey: "StopDetailsPage.loadGlobalData",
getData: { try await globalRepository.getGlobalData() },
onRefreshAfterError: loadEverything
)
}
}

func loadPinnedRoutes() {
Task {
do {
pinnedRoutes = try await pinnedRouteRepository.getPinnedRoutes()
} catch is CancellationError {
// do nothing on cancellation
} catch {
// getPinnedRoutes shouldn't actually fail
debugPrint(error)
}
}
}

func togglePinnedRoute(_ routeId: String) {
Task {
do {
_ = try await togglePinnedUsecase.execute(route: routeId)
loadPinnedRoutes()
} catch is CancellationError {
// do nothing on cancellation
} catch {
// execute shouldn't actually fail
debugPrint(error)
}
}
}

func changeStop(_ stopId: String) {
leavePredictions()
fetchStopData(stopId)
}

func fetchStopData(_ stopId: String) {
getSchedule(stopId)
joinPredictions(stopId)
updateDepartures(stopId)
}

func getSchedule(_ stopId: String) {
Task {
schedulesResponse = nil
await fetchApi(
errorBannerVM.errorRepository,
errorKey: "StopDetailsPage.getSchedule",
getData: { try await schedulesRepository.getSchedule(stopIds: [stopId]) },
onSuccess: { schedulesResponse = $0 },
onRefreshAfterError: loadEverything
)
}
}

func joinPredictions(_ stopId: String) {
// no error handling since persistent errors cause stale predictions
predictionsRepository.connectV2(stopIds: [stopId], onJoin: { outcome in
DispatchQueue.main.async {
if case let .ok(result) = onEnum(of: outcome) {
predictionsByStop = result.data
checkPredictionsStale()
}
errorBannerVM.loadingWhenPredictionsStale = false
}
}, onMessage: { outcome in
DispatchQueue.main.async {
if case let .ok(result) = onEnum(of: outcome) {
if let existingPredictionsByStop = predictionsByStop {
predictionsByStop = existingPredictionsByStop.mergePredictions(updatedPredictions: result.data)
} else {
predictionsByStop = PredictionsByStopJoinResponse(
predictionsByStop: [result.data.stopId: result.data.predictions],
trips: result.data.trips,
vehicles: result.data.vehicles
)
}
checkPredictionsStale()
}
errorBannerVM.loadingWhenPredictionsStale = false
}

})
}

func leavePredictions() {
predictionsRepository.disconnect()
}

private func checkPredictionsStale() {
if let lastPredictions = predictionsRepository.lastUpdated {
errorBannerVM.errorRepository.checkPredictionsStale(
predictionsLastUpdated: lastPredictions,
predictionQuantity: Int32(
predictionsByStop?.predictionQuantity() ?? 0
),
action: {
leavePredictions()
joinPredictions(stopId)
}
)
}
}

func updateDepartures(
_ stopId: String? = nil,
_ predictionsByStop: PredictionsByStopJoinResponse? = nil
) {
let stopId = stopId ?? self.stopId
let predictionsByStop = predictionsByStop ?? self.predictionsByStop

let targetPredictions = predictionsByStop?.toPredictionsStreamDataResponse()

let newDepartures: StopDetailsDepartures? = if let globalResponse {
StopDetailsDepartures.companion.fromData(
stopId: stopId,
global: globalResponse,
schedules: schedulesResponse,
predictions: targetPredictions,
alerts: nearbyVM.alerts,
pinnedRoutes: pinnedRoutes,
filterAtTime: now.toKotlinInstant()
)
} else {
nil
}
let nextStopFilter = stopFilter ?? newDepartures?.autoStopFilter()
if stopFilter != nextStopFilter {
nearbyVM.setLastStopDetailsFilter(stopId, nextStopFilter)
}

internalDepartures = newDepartures
nearbyVM.setDepartures(stopId, newDepartures)
}
}
Loading

0 comments on commit 02bc84d

Please sign in to comment.