From 12aa4e42fa9250058ec4c1542e9f06f9627cbb13 Mon Sep 17 00:00:00 2001 From: Emma Simon Date: Wed, 11 Dec 2024 12:33:15 -0500 Subject: [PATCH 1/3] feat: Add special behavior when vehicle at target --- .../ComponentViews/UpcomingTripView.swift | 2 +- iosApp/iosApp/Localizable.xcstrings | 41 +++++++ .../Pages/StopDetails/TripDetailsView.swift | 72 ++++++++----- .../Pages/StopDetails/TripVehicleCard.swift | 101 +++++++++++++----- .../tid/mbta_app/model/TripDetailsStopList.kt | 42 +++++--- 5 files changed, 187 insertions(+), 71 deletions(-) diff --git a/iosApp/iosApp/ComponentViews/UpcomingTripView.swift b/iosApp/iosApp/ComponentViews/UpcomingTripView.swift index 634c9d264..4435e847c 100644 --- a/iosApp/iosApp/ComponentViews/UpcomingTripView.swift +++ b/iosApp/iosApp/ComponentViews/UpcomingTripView.swift @@ -50,7 +50,7 @@ struct UpcomingTripView: View { case let .some(prediction): switch onEnum(of: prediction) { case let .overridden(overridden): - Text(overridden.textWithLocale()).realtime() + Text(overridden.textWithLocale()).realtime(hideIndicator: hideRealtimeIndicators) case .hidden, .skipped: // should have been filtered out already Text(verbatim: "") diff --git a/iosApp/iosApp/Localizable.xcstrings b/iosApp/iosApp/Localizable.xcstrings index 83aaca6e3..9332215eb 100644 --- a/iosApp/iosApp/Localizable.xcstrings +++ b/iosApp/iosApp/Localizable.xcstrings @@ -8721,6 +8721,47 @@ } } }, + "Waiting to depart" : { + "comment" : "Label for a vehicle stopped at a terminal station waiting to start a trip. For example: Waiting to depart Alewife", + "localizations" : { + "es" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Esperando para partir" + } + }, + "ht" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Ap tann pou yo pati" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Esperando para partir" + } + }, + "vi" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Đang chờ khởi hành" + } + }, + "zh-Hans-CN" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "等待出发" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "等待離開" + } + } + } + }, "We use your location to show you nearby transit options." : { "localizations" : { "es" : { diff --git a/iosApp/iosApp/Pages/StopDetails/TripDetailsView.swift b/iosApp/iosApp/Pages/StopDetails/TripDetailsView.swift index 4e797fff5..5b647e41b 100644 --- a/iosApp/iosApp/Pages/StopDetails/TripDetailsView.swift +++ b/iosApp/iosApp/Pages/StopDetails/TripDetailsView.swift @@ -47,7 +47,35 @@ struct TripDetailsView: View { self.analytics = analytics } + func getParentFor(_ stopId: String?, stops: [String: Stop]) -> Stop? { + if let stopId { stops[stopId]?.resolveParent(stops: stops) } else { nil } + } + var body: some View { + content + .task { stopDetailsVM.handleTripFilterChange(tripFilter) } + .onDisappear { + if stopDetailsVM.tripData?.tripFilter == tripFilter { + stopDetailsVM.clearTripDetails() + } + clearMapVehicle() + } + .onChange(of: tripFilter) { nextTripFilter in stopDetailsVM.handleTripFilterChange(nextTripFilter) } + .onChange(of: stopDetailsVM.tripData?.vehicle) { vehicle in mapVM.selectedVehicle = vehicle } + .onReceive(inspection.notice) { inspection.visit(self, $0) } + .withScenePhaseHandlers( + onActive: { + stopDetailsVM.returnFromBackground() + if let tripFilter { + stopDetailsVM.joinTripChannels(tripFilter: tripFilter) + } + }, + onInactive: stopDetailsVM.leaveTripChannels, + onBackground: stopDetailsVM.leaveTripChannels + ) + } + + @ViewBuilder private var content: some View { VStack(spacing: 16) { if nearbyVM.showDebugMessages { DebugView { @@ -71,38 +99,20 @@ struct TripDetailsView: View { alertsData: nearbyVM.alerts, globalData: global ) { - let vehicleStop: Stop? = if let stopId = vehicle.stopId, let allStops = stopDetailsVM.global?.stops { - allStops[stopId]?.resolveParent(stops: allStops) - } else { - nil - } + let vehicleStop: Stop? = getParentFor(vehicle.stopId, stops: global.stops) + + let terminalStop: Stop? = getParentFor(tripData.trip.stopIds?.first, stops: global.stops) + let atTerminal = terminalStop != nil && terminalStop?.id == vehicleStop?.id + && vehicle.currentStatus == .stoppedAt + let terminalEntry = atTerminal ? stops.terminalStop : nil + let routeAccents = stopDetailsVM.getTripRouteAccents() - tripDetails(tripFilter.tripId, stops, vehicle, vehicleStop, routeAccents) + tripDetails(tripFilter.tripId, stops, vehicle, vehicleStop, terminalEntry, routeAccents) } else { loadingBody() } } .padding(.horizontal, 6) - .task { stopDetailsVM.handleTripFilterChange(tripFilter) } - .onDisappear { - if stopDetailsVM.tripData?.tripFilter == tripFilter { - stopDetailsVM.clearTripDetails() - } - clearMapVehicle() - } - .onChange(of: tripFilter) { nextTripFilter in stopDetailsVM.handleTripFilterChange(nextTripFilter) } - .onChange(of: stopDetailsVM.tripData?.vehicle) { vehicle in mapVM.selectedVehicle = vehicle } - .onReceive(inspection.notice) { inspection.visit(self, $0) } - .withScenePhaseHandlers( - onActive: { - stopDetailsVM.returnFromBackground() - if let tripFilter { - stopDetailsVM.joinTripChannels(tripFilter: tripFilter) - } - }, - onInactive: stopDetailsVM.leaveTripChannels, - onBackground: stopDetailsVM.leaveTripChannels - ) } @ViewBuilder private func tripDetails( @@ -110,11 +120,12 @@ struct TripDetailsView: View { _ stops: TripDetailsStopList, _ vehicle: Vehicle?, _ vehicleStop: Stop?, + _ terminalEntry: TripDetailsStopList.Entry?, _ routeAccents: TripRouteAccents ) -> some View { let vehicleShown = vehicle != nil && vehicleStop != nil VStack(spacing: 0) { - vehicleCardView(vehicle, vehicleStop, tripId, routeAccents).zIndex(1) + vehicleCardView(vehicle, vehicleStop, tripId, terminalEntry, routeAccents).zIndex(1) TripStops( targetId: stopId, stops: stops, @@ -134,6 +145,7 @@ struct TripDetailsView: View { _ vehicle: Vehicle?, _ vehicleStop: Stop?, _ tripId: String, + _ terminalEntry: TripDetailsStopList.Entry?, _ routeAccents: TripRouteAccents ) -> some View { if let vehicle, let vehicleStop { @@ -141,7 +153,10 @@ struct TripDetailsView: View { vehicle: vehicle, stop: vehicleStop, tripId: tripId, - routeAccents: routeAccents + targetId: stopId, + terminalEntry: terminalEntry, + routeAccents: routeAccents, + now: now ) } } @@ -153,6 +168,7 @@ struct TripDetailsView: View { placeholderInfo.stops, placeholderInfo.vehicle, placeholderInfo.vehicleStop, + nil, TripRouteAccents() ).loadingPlaceholder() } diff --git a/iosApp/iosApp/Pages/StopDetails/TripVehicleCard.swift b/iosApp/iosApp/Pages/StopDetails/TripVehicleCard.swift index 0381b2249..39c2b7f24 100644 --- a/iosApp/iosApp/Pages/StopDetails/TripVehicleCard.swift +++ b/iosApp/iosApp/Pages/StopDetails/TripVehicleCard.swift @@ -14,7 +14,10 @@ struct TripVehicleCard: View { let vehicle: Vehicle let stop: Stop let tripId: String + let targetId: String + let terminalEntry: TripDetailsStopList.Entry? let routeAccents: TripRouteAccents + let now: Date var body: some View { ZStack(alignment: .leading) { @@ -31,7 +34,7 @@ struct TripVehicleCard: View { liveIndicator } .frame(maxWidth: .infinity, minHeight: 56, alignment: .leading) - .padding(.vertical, 12) + .padding(.vertical, 8) .padding(.leading, 30) .padding(.trailing, 16) } @@ -92,7 +95,10 @@ struct TripVehicleCard: View { "Next stop", comment: "Label for a vehicle's next stop. For example: Next stop Alewife" ) - case .stoppedAt: NSLocalizedString( + case .stoppedAt: terminalEntry != nil ? NSLocalizedString( + "Waiting to depart", + comment: "Label for a vehicle stopped at a terminal station waiting to start a trip. For example: Waiting to depart Alewife" + ) : NSLocalizedString( "Now at", comment: "Label for a where a vehicle is currently stopped. For example: Now at Alewife" ) @@ -114,28 +120,59 @@ struct TripVehicleCard: View { .rotationEffect(.degrees(225)) routeIcon(routeAccents.type) .resizable() - .frame(width: 27, height: 27) + .frame(width: 27.5, height: 27.5) .foregroundColor(routeAccents.textColor) + .overlay { + if targetId == stop.id, vehicle.currentStatus == .stoppedAt { + Image(.stopPinIndicator) + .resizable() + .scaledToFit() + .frame(width: 20, height: 26) + .padding(.bottom, 36) + } + } } .accessibilityHidden(true) .padding([.bottom], 6) } var liveIndicator: some View { - HStack { - Image(.liveData) - .resizable() - .frame(width: 16, height: 16) - Text("Live", comment: "Indicates that data is being updated in real-time") - .font(Typography.footnote) + VStack { + HStack { + Image(.liveData) + .resizable() + .frame(width: 16, height: 16) + Text("Live", comment: "Indicates that data is being updated in real-time") + .font(Typography.footnote) + } + .opacity(0.6) + .accessibilityElement() + .accessibilityAddTraits(.isHeader) + .accessibilityLabel(Text( + "Real-time arrivals updating live", + comment: "VoiceOver label for real-time indicator icon" + )) + if let upcomingTripViewState { + UpcomingTripView( + prediction: upcomingTripViewState, + routeType: routeAccents.type, + hideRealtimeIndicators: true + ).foregroundStyle(Color.text).opacity(0.6) + } + } + } + + var upcomingTripViewState: UpcomingTripView.State? { + guard let terminalEntry else { return nil } + if let alert = terminalEntry.alert { + return .noService(alert.effect) + } else { + let formatted = terminalEntry.format(now: now.toKotlinInstant(), routeType: routeAccents.type) + return switch onEnum(of: formatted) { + case .hidden, .skipped: nil + default: .some(formatted) + } } - .opacity(0.6) - .accessibilityElement() - .accessibilityAddTraits(.isHeader) - .accessibilityLabel(Text( - "Real-time arrivals updating live", - comment: "VoiceOver label for real-time indicator icon" - )) } } @@ -153,23 +190,33 @@ struct TripVehicleCard_Previews: PreviewProvider { trip.id = "1234" trip.headsign = "Alewife" } - let vehicle = Vehicle(id: "y1234", bearing: nil, - currentStatus: __Bridge__Vehicle_CurrentStatus.inTransitTo, - currentStopSequence: 30, - directionId: 1, - latitude: 0.0, - longitude: 0.0, - updatedAt: Date.now.addingTimeInterval(-10).toKotlinInstant(), - routeId: "66", - stopId: "place-davis", - tripId: trip.id) + let vehicle = Vehicle( + id: "y1234", bearing: nil, + currentStatus: __Bridge__Vehicle_CurrentStatus.inTransitTo, + currentStopSequence: 30, + directionId: 1, + latitude: 0.0, + longitude: 0.0, + updatedAt: Date.now.addingTimeInterval(-10).toKotlinInstant(), + routeId: "66", + stopId: "place-davis", + tripId: trip.id + ) let stop = objects.stop { stop in stop.name = "Davis" } List { - TripVehicleCard(vehicle: vehicle, stop: stop, tripId: trip.id, routeAccents: TripRouteAccents(route: red)) + TripVehicleCard( + vehicle: vehicle, + stop: stop, + tripId: trip.id, + targetId: "", + terminalEntry: nil, + routeAccents: TripRouteAccents(route: red), + now: Date.now + ) } .previewDisplayName("VehicleCard") } diff --git a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/model/TripDetailsStopList.kt b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/model/TripDetailsStopList.kt index ae87c4c3a..bdb318ca7 100644 --- a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/model/TripDetailsStopList.kt +++ b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/model/TripDetailsStopList.kt @@ -1,12 +1,15 @@ package com.mbta.tid.mbta_app.model +import co.touchlab.skie.configuration.annotations.DefaultArgumentInterop import com.mbta.tid.mbta_app.model.response.AlertsStreamDataResponse import com.mbta.tid.mbta_app.model.response.GlobalResponse import com.mbta.tid.mbta_app.model.response.PredictionsStreamDataResponse import com.mbta.tid.mbta_app.model.response.TripSchedulesResponse import kotlinx.datetime.Instant -data class TripDetailsStopList(val stops: List) { +data class TripDetailsStopList +@DefaultArgumentInterop.Enabled +constructor(val stops: List, val terminalStop: Entry? = null) { data class Entry( val stop: Stop, val stopSequence: Int, @@ -170,9 +173,25 @@ data class TripDetailsStopList(val stops: List) { if (entries.isEmpty()) { return TripDetailsStopList(emptyList()) } + + val sortedEntries = entries.entries.sortedBy { it.key } + + fun getEntry(optionalWorking: WorkingEntry?): Entry? { + val working = optionalWorking ?: return null + val stop = globalData.stops[working.stopId] ?: return null + return Entry( + stop, + working.stopSequence, + getAlert(working, alertsData, globalData, tripId, directionId), + working.schedule, + working.prediction, + working.vehicle, + getTransferRoutes(working, globalData) + ) + } + return TripDetailsStopList( - entries.entries - .sortedBy { it.key } + sortedEntries .dropWhile { if ( vehicle == null || @@ -181,20 +200,13 @@ data class TripDetailsStopList(val stops: List) { ) { false } else { - it.value.stopSequence < vehicle.currentStopSequence + it.value.stopSequence < vehicle.currentStopSequence || + (it.value.stopSequence == vehicle.currentStopSequence && + vehicle.currentStatus == Vehicle.CurrentStatus.StoppedAt) } } - .mapNotNull { - Entry( - globalData.stops[it.value.stopId] ?: return@mapNotNull null, - it.value.stopSequence, - getAlert(it.value, alertsData, globalData, tripId, directionId), - it.value.schedule, - it.value.prediction, - it.value.vehicle, - getTransferRoutes(it.value, globalData) - ) - } + .mapNotNull { getEntry(it.value) }, + getEntry(sortedEntries.firstOrNull()?.value) ) } From 3a81524e212b02788b73d9883389914a1b9c1081 Mon Sep 17 00:00:00 2001 From: Emma Simon Date: Wed, 11 Dec 2024 14:34:14 -0500 Subject: [PATCH 2/3] test: Add tests for updated target stop behavior --- iosApp/iosApp.xcodeproj/project.pbxproj | 4 + .../Pages/StopDetails/TripVehicleCard.swift | 5 +- .../StopDetails/TripVehicleCardTests.swift | 192 ++++++++++++++++++ .../mbta_app/model/TripDetailsStopListTest.kt | 42 +++- 4 files changed, 237 insertions(+), 6 deletions(-) create mode 100644 iosApp/iosAppTests/Pages/StopDetails/TripVehicleCardTests.swift diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj index 2226c8fc2..2ef217213 100644 --- a/iosApp/iosApp.xcodeproj/project.pbxproj +++ b/iosApp/iosApp.xcodeproj/project.pbxproj @@ -176,6 +176,7 @@ 9A7F12132CCB185D0042B0F1 /* TabLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A7F12122CCB185D0042B0F1 /* TabLabel.swift */; }; 9A7F12172CCFEFAA0042B0F1 /* MoreLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A7F12162CCFEFAA0042B0F1 /* MoreLink.swift */; }; 9A7F12192CCFF2D20042B0F1 /* MorePhone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A7F12182CCFF2D20042B0F1 /* MorePhone.swift */; }; + 9A8375EE2D0A14DD00E3694F /* TripVehicleCardTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A8375ED2D0A14DD00E3694F /* TripVehicleCardTests.swift */; }; 9A84DB102D03A6BF00A78C64 /* TripStopsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A84DB0F2D03A6BF00A78C64 /* TripStopsTests.swift */; }; 9A887D572B683103006F5B80 /* SearchResultsContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A887D562B683103006F5B80 /* SearchResultsContainer.swift */; }; 9A887D592B698EF1006F5B80 /* SearchResultViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A887D582B698EF1006F5B80 /* SearchResultViewTests.swift */; }; @@ -473,6 +474,7 @@ 9A7F12122CCB185D0042B0F1 /* TabLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabLabel.swift; sourceTree = ""; }; 9A7F12162CCFEFAA0042B0F1 /* MoreLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoreLink.swift; sourceTree = ""; }; 9A7F12182CCFF2D20042B0F1 /* MorePhone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MorePhone.swift; sourceTree = ""; }; + 9A8375ED2D0A14DD00E3694F /* TripVehicleCardTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TripVehicleCardTests.swift; sourceTree = ""; }; 9A84DB0F2D03A6BF00A78C64 /* TripStopsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TripStopsTests.swift; sourceTree = ""; }; 9A887D562B683103006F5B80 /* SearchResultsContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsContainer.swift; sourceTree = ""; }; 9A887D582B698EF1006F5B80 /* SearchResultViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultViewTests.swift; sourceTree = ""; }; @@ -961,6 +963,7 @@ 9A9B9FFF2D03565800BCB2BD /* TripDetailsViewTests.swift */, 9A4092EA2D0258A20026EB01 /* TripStopRowTests.swift */, 9A84DB0F2D03A6BF00A78C64 /* TripStopsTests.swift */, + 9A8375ED2D0A14DD00E3694F /* TripVehicleCardTests.swift */, ); path = StopDetails; sourceTree = ""; @@ -1479,6 +1482,7 @@ ED5C93F62C4A1AD70086D017 /* TripDetailsHeaderTests.swift in Sources */, 9A9BA0002D03565800BCB2BD /* TripDetailsViewTests.swift in Sources */, 9A60E8E72B8501BD008A8D5C /* RoutePillTests.swift in Sources */, + 9A8375EE2D0A14DD00E3694F /* TripVehicleCardTests.swift in Sources */, 6E35D4D32B72CD3900A2BF95 /* HomeMapViewTests.swift in Sources */, 9AF0937A2BD962FF001DF39F /* DirectionPickerTests.swift in Sources */, 9A6FA0282BC72F110067769C /* LegacyStopDetailsPageTests.swift in Sources */, diff --git a/iosApp/iosApp/Pages/StopDetails/TripVehicleCard.swift b/iosApp/iosApp/Pages/StopDetails/TripVehicleCard.swift index 39c2b7f24..3122a6339 100644 --- a/iosApp/iosApp/Pages/StopDetails/TripVehicleCard.swift +++ b/iosApp/iosApp/Pages/StopDetails/TripVehicleCard.swift @@ -97,7 +97,10 @@ struct TripVehicleCard: View { ) case .stoppedAt: terminalEntry != nil ? NSLocalizedString( "Waiting to depart", - comment: "Label for a vehicle stopped at a terminal station waiting to start a trip. For example: Waiting to depart Alewife" + comment: """ + Label for a vehicle stopped at a terminal station waiting to start a trip. + For example: Waiting to depart Alewife + """ ) : NSLocalizedString( "Now at", comment: "Label for a where a vehicle is currently stopped. For example: Now at Alewife" diff --git a/iosApp/iosAppTests/Pages/StopDetails/TripVehicleCardTests.swift b/iosApp/iosAppTests/Pages/StopDetails/TripVehicleCardTests.swift new file mode 100644 index 000000000..9bad09194 --- /dev/null +++ b/iosApp/iosAppTests/Pages/StopDetails/TripVehicleCardTests.swift @@ -0,0 +1,192 @@ +// +// TripVehicleCardTests.swift +// iosAppTests +// +// Created by esimon on 12/11/24. +// Copyright © 2024 MBTA. All rights reserved. +// + +@testable import iosApp +import shared +import SwiftUI +import ViewInspector +import XCTest + +final class TripVehicleCardTests: XCTestCase { + override func setUp() { + executionTimeAllowance = 60 + } + + func testDisplaysStopName() throws { + let now = Date.now + let objects = ObjectCollectionBuilder() + let stop = objects.stop { _ in } + let vehicle = objects.vehicle { vehicle in + vehicle.currentStatus = .inTransitTo + vehicle.tripId = "" + } + let sut = TripVehicleCard( + vehicle: vehicle, + stop: stop, + tripId: "", + targetId: "", + terminalEntry: nil, + routeAccents: .init(), + now: now + ) + + try XCTAssertNotNil(sut.inspect().find(text: stop.name)) + } + + func testDisplaysStatusDescription() throws { + let now = Date.now + let objects = ObjectCollectionBuilder() + let stop = objects.stop { _ in } + + let inTransitVehicle = objects.vehicle { vehicle in + vehicle.currentStatus = .inTransitTo + vehicle.tripId = "" + } + let inTransitSut = TripVehicleCard( + vehicle: inTransitVehicle, + stop: stop, + tripId: "", + targetId: "", + terminalEntry: nil, + routeAccents: .init(), + now: now + ) + try XCTAssertNotNil(inTransitSut.inspect().find(text: "Next stop")) + + let incomingVehicle = objects.vehicle { vehicle in + vehicle.currentStatus = .incomingAt + vehicle.tripId = "" + } + let incomingSut = TripVehicleCard( + vehicle: incomingVehicle, + stop: stop, + tripId: "", + targetId: "", + terminalEntry: nil, + routeAccents: .init(), + now: now + ) + try XCTAssertNotNil(incomingSut.inspect().find(text: "Approaching")) + + let stoppedVehicle = objects.vehicle { vehicle in + vehicle.currentStatus = .stoppedAt + vehicle.tripId = "" + } + let stoppedSut = TripVehicleCard( + vehicle: stoppedVehicle, + stop: stop, + tripId: "", + targetId: "", + terminalEntry: nil, + routeAccents: .init(), + now: now + ) + try XCTAssertNotNil(stoppedSut.inspect().find(text: "Now at")) + } + + func testDifferentTrip() throws { + let now = Date.now + let objects = ObjectCollectionBuilder() + let stop = objects.stop { _ in } + + let vehicle = objects.vehicle { vehicle in + vehicle.currentStatus = .inTransitTo + vehicle.tripId = "different" + } + let sut = TripVehicleCard( + vehicle: vehicle, + stop: stop, + tripId: "selected", + targetId: "", + terminalEntry: nil, + routeAccents: .init(), + now: now + ) + try XCTAssertNotNil(sut.inspect().find(text: "This vehicle is completing another trip")) + try XCTAssertThrowsError(sut.inspect().find(text: stop.name)) + } + + func testAtTarget() throws { + let now = Date.now + let objects = ObjectCollectionBuilder() + let stop = objects.stop { _ in } + + let vehicle = objects.vehicle { vehicle in + vehicle.currentStatus = .stoppedAt + vehicle.tripId = "" + vehicle.stopId = stop.id + } + let targeted = TripVehicleCard( + vehicle: vehicle, + stop: stop, + tripId: "", + targetId: stop.id, + terminalEntry: nil, + routeAccents: .init(), + now: now + ) + + XCTAssertNotNil(try targeted.inspect().find(ViewType.Image.self, where: { image in + try image.actualImage().name() == "stop-pin-indicator" + })) + + let notTargeted = TripVehicleCard( + vehicle: vehicle, + stop: stop, + tripId: "", + targetId: "", + terminalEntry: nil, + routeAccents: .init(), + now: now + ) + + XCTAssertThrowsError(try notTargeted.inspect().find(ViewType.Image.self, where: { image in + try image.actualImage().name() == "stop-pin-indicator" + })) + } + + func testAtTerminal() throws { + let now = Date.now + let objects = ObjectCollectionBuilder() + let stop = objects.stop { _ in } + + let vehicle = objects.vehicle { vehicle in + vehicle.currentStatus = .stoppedAt + vehicle.tripId = "" + vehicle.stopId = stop.id + vehicle.currentStopSequence = 0 + } + let prediction = objects.prediction { prediction in + prediction.departureTime = now.addingTimeInterval(5 * 60).toKotlinInstant() + } + + let sut = TripVehicleCard( + vehicle: vehicle, + stop: stop, + tripId: "", + targetId: stop.id, + terminalEntry: .init( + stop: stop, + stopSequence: 0, + alert: nil, + schedule: nil, + prediction: prediction, + vehicle: vehicle, + routes: [] + ), + routeAccents: .init(), + now: now + ) + + try XCTAssertNotNil(sut.inspect().find(text: "Waiting to depart")) + try XCTAssertNotNil(sut.inspect().find(ViewType.Image.self, where: { image in + try image.actualImage().name() == "stop-pin-indicator" + })) + try XCTAssertNotNil(sut.inspect().find(UpcomingTripView.self)) + } +} diff --git a/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/model/TripDetailsStopListTest.kt b/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/model/TripDetailsStopListTest.kt index a5218df6e..ee512bff6 100644 --- a/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/model/TripDetailsStopListTest.kt +++ b/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/model/TripDetailsStopListTest.kt @@ -86,8 +86,8 @@ class TripDetailsStopListTest { block() } - fun stopListOf(vararg stops: TripDetailsStopList.Entry) = - TripDetailsStopList(stops.asList()) + fun stopListOf(vararg stops: TripDetailsStopList.Entry, terminalStop: TripDetailsStopList.Entry? = null) = + TripDetailsStopList(stops.asList(), terminalStop ?: stops.firstOrNull()) fun entry( stopId: String, @@ -408,7 +408,8 @@ class TripDetailsStopListTest { null, listOf() ), - ) + ), + TripDetailsStopList.Entry(boylston, 590, null, null, null, null, listOf()) ), list ) @@ -492,6 +493,15 @@ class TripDetailsStopListTest { vehicle, listOf() ) + ), + TripDetailsStopList.Entry( + stop1, + 1, + null, + schedule1, + prediction1, + vehicle, + listOf() ) ), list @@ -643,7 +653,7 @@ class TripDetailsStopListTest { @Test fun `fromPieces discards stops vehicle has passed`() = test { - prediction("A", 10) + val pred1 = prediction("A", 10) val pred2 = prediction("B", 20) val pred3 = prediction("C", 30) val trip = objects.trip {} @@ -656,7 +666,29 @@ class TripDetailsStopListTest { assertEquals( stopListOf( entry("B", 20, prediction = pred2, vehicle = vehicle), - entry("C", 30, prediction = pred3, vehicle = vehicle) + entry("C", 30, prediction = pred3, vehicle = vehicle), + terminalStop = entry("A", 10, prediction = pred1, vehicle = vehicle), + ), + fromPieces(null, predictions(), vehicle, trip = trip) + ) + } + + @Test + fun `fromPieces discards stops vehicle is currently at`() = test { + val pred1 = prediction("A", 10) + prediction("B", 20) + val pred3 = prediction("C", 30) + val trip = objects.trip {} + val vehicle = + objects.vehicle { + currentStatus = Vehicle.CurrentStatus.StoppedAt + currentStopSequence = 20 + tripId = trip.id + } + assertEquals( + stopListOf( + entry("C", 30, prediction = pred3, vehicle = vehicle), + terminalStop = entry("A", 10, prediction = pred1, vehicle = vehicle), ), fromPieces(null, predictions(), vehicle, trip = trip) ) From c13086e24e55d3cba7b03eaacfbe2a2275023c63 Mon Sep 17 00:00:00 2001 From: Emma Simon Date: Wed, 11 Dec 2024 17:22:09 -0500 Subject: [PATCH 3/3] fix: Add some missed MainActor annotations --- iosApp/iosApp/ViewModels/StopDetailsViewModel.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/iosApp/iosApp/ViewModels/StopDetailsViewModel.swift b/iosApp/iosApp/ViewModels/StopDetailsViewModel.swift index 662f68209..f20f61202 100644 --- a/iosApp/iosApp/ViewModels/StopDetailsViewModel.swift +++ b/iosApp/iosApp/ViewModels/StopDetailsViewModel.swift @@ -236,7 +236,7 @@ class StopDetailsViewModel: ObservableObject { leaveVehicle() guard let vehicleId = tripFilter.vehicleId else { // If the filter has a null vehicle ID, we can't join anything, clear the vehicle and return - tripData?.vehicle = nil + Task { @MainActor in tripData?.vehicle = nil } return } let errorKey = "TripDetailsPage.joinVehicle" @@ -347,6 +347,7 @@ class StopDetailsViewModel: ObservableObject { } } + @MainActor func returnFromBackground() { if let predictionsByStop, predictionsRepository