add new tests to LocationSharingViewModel

This commit is contained in:
Mauro Romito
2026-04-02 21:54:29 +02:00
committed by Mauro
parent 208c21c2a0
commit 93a6917d8a
3 changed files with 89 additions and 26 deletions
@@ -21,5 +21,6 @@ extension LiveLocationManagerMock {
underlyingAuthorizationStatus = .init(authorizationStatusSubject) underlyingAuthorizationStatus = .init(authorizationStatusSubject)
requestAlwaysAuthorizationIfPossibleReturnValue = configuration.requestAlwaysAuthorizationIfPossibleReturnValue requestAlwaysAuthorizationIfPossibleReturnValue = configuration.requestAlwaysAuthorizationIfPossibleReturnValue
startLiveLocationRoomIDDurationReturnValue = .success(())
} }
} }
@@ -59,7 +59,7 @@ class LocationSharingScreenViewModel: LocationSharingScreenViewModelType, Locati
case .close: case .close:
actionsSubject.send(.close) actionsSubject.send(.close)
case .startLiveLocation: case .startLiveLocation:
startLiveLocationSharing() checkAlwaysShareLocationPermission()
case .selectLocation: case .selectLocation:
guard let coordinate = state.bindings.mapCenterLocation else { return } guard let coordinate = state.bindings.mapCenterLocation else { return }
let uncertainty = state.isSharingUserLocation ? context.geolocationUncertainty : nil let uncertainty = state.isSharingUserLocation ? context.geolocationUncertainty : nil
@@ -118,12 +118,8 @@ class LocationSharingScreenViewModel: LocationSharingScreenViewModelType, Locati
formatter.allowedUnits = [.hour, .minute] formatter.allowedUnits = [.hour, .minute]
return formatter return formatter
}() }()
private func startLiveLocationSharing() { private func checkAlwaysShareLocationPermission() {
requestAlwaysLocationPermission()
}
private func requestAlwaysLocationPermission() {
authorizationStatusSubscription = nil authorizationStatusSubscription = nil
let authorizationStatus = liveLocationManager.authorizationStatus.value let authorizationStatus = liveLocationManager.authorizationStatus.value
switch authorizationStatus { switch authorizationStatus {
@@ -137,7 +133,7 @@ class LocationSharingScreenViewModel: LocationSharingScreenViewModelType, Locati
.first() // this publisher only fires when there is an actual change, and if the user is done with permissions .first() // this publisher only fires when there is an actual change, and if the user is done with permissions
.sink { [weak self] newValue in .sink { [weak self] newValue in
guard newValue == .authorizedWhenInUse else { return } guard newValue == .authorizedWhenInUse else { return }
self?.requestAlwaysLocationPermission() self?.checkAlwaysShareLocationPermission()
} }
case .authorizedWhenInUse: case .authorizedWhenInUse:
guard liveLocationManager.requestAlwaysAuthorizationIfPossible() else { guard liveLocationManager.requestAlwaysAuthorizationIfPossible() else {
@@ -171,14 +167,14 @@ class LocationSharingScreenViewModel: LocationSharingScreenViewModelType, Locati
} }
private func showLiveLocationDurationPicker() { private func showLiveLocationDurationPicker() {
let durations: [TimeInterval] = [15 * 60, // 15 minutes let durations: [Duration] = [.seconds(15 * 60), // 15 minutes
60 * 60, // 1 hour .seconds(60 * 60), // 1 hour
8 * 60 * 60] // 8 hours .seconds(60 * 60 * 8)] // 8 hours
let durationButtons: [AlertInfo<LocationSharingViewAlert>.AlertButton] = durations.compactMap { duration in let durationButtons: [AlertInfo<LocationSharingViewAlert>.AlertButton] = durations.compactMap { duration in
guard let title = Self.durationFormatter.string(from: duration) else { return nil } guard let title = Self.durationFormatter.string(from: duration.seconds) else { return nil }
return .init(title: title) { [weak self] in return .init(title: title) { [weak self] in
Task { [weak self] in await self?.startLiveLocationSharingInRoom(durationMillis: UInt64(duration * 1000)) } Task { [weak self] in await self?.startLiveLocationSharingInRoom(duration: duration) }
} }
} }
@@ -187,9 +183,9 @@ class LocationSharingScreenViewModel: LocationSharingScreenViewModelType, Locati
verticalButtons: durationButtons) verticalButtons: durationButtons)
} }
private func startLiveLocationSharingInRoom(durationMillis: UInt64) async { private func startLiveLocationSharingInRoom(duration: Duration) async {
let result = await liveLocationManager.startLiveLocation(roomID: roomProxy.id, let result = await liveLocationManager.startLiveLocation(roomID: roomProxy.id,
durationMillis: durationMillis) duration: duration)
switch result { switch result {
case .success: case .success:
@@ -71,11 +71,11 @@ final class LocationSharingScreenViewModelTests {
@Test @Test
func errorMapping() { func errorMapping() {
setupViewModel() setupViewModel()
let mapError = AlertInfo(locationSharingViewError: .mapError(.failedLoadingMap)) let mapError = AlertInfo(alertID: .mapError(.failedLoadingMap))
#expect(mapError.title == L10n.errorFailedLoadingMap(InfoPlistReader.main.bundleDisplayName)) #expect(mapError.title == L10n.errorFailedLoadingMap(InfoPlistReader.main.bundleDisplayName))
let locationError = AlertInfo(locationSharingViewError: .mapError(.failedLocatingUser)) let locationError = AlertInfo(alertID: .mapError(.failedLocatingUser))
#expect(locationError.title == L10n.errorFailedLocatingUser(InfoPlistReader.main.bundleDisplayName)) #expect(locationError.title == L10n.errorFailedLocatingUser(InfoPlistReader.main.bundleDisplayName))
let AuthorizationError = AlertInfo(locationSharingViewError: .missingAuthorization) let AuthorizationError = AlertInfo(alertID: .missingAuthorization)
#expect(AuthorizationError.message == L10n.dialogPermissionLocationDescriptionIos(InfoPlistReader.main.bundleDisplayName)) #expect(AuthorizationError.message == L10n.dialogPermissionLocationDescriptionIos(InfoPlistReader.main.bundleDisplayName))
} }
@@ -140,13 +140,6 @@ final class LocationSharingScreenViewModelTests {
#expect(context.alertInfo?.id == .missingAlwaysAuthorization) #expect(context.alertInfo?.id == .missingAlwaysAuthorization)
} }
@Test
func startLiveLocationWithAlwaysAuthorization() {
setupViewModel(liveLocationManagerConfiguration: .init(authorizationStatus: .authorizedAlways))
context.send(viewAction: .startLiveLocation)
#expect(context.alertInfo == nil)
}
@Test @Test
func startLiveLocationWithWhenInUseAuthorizationAlreadyRequested() { func startLiveLocationWithWhenInUseAuthorizationAlreadyRequested() {
setupViewModel(liveLocationManagerConfiguration: .init(authorizationStatus: .authorizedWhenInUse, setupViewModel(liveLocationManagerConfiguration: .init(authorizationStatus: .authorizedWhenInUse,
@@ -191,8 +184,81 @@ final class LocationSharingScreenViewModelTests {
#expect(context.alertInfo == nil) #expect(context.alertInfo == nil)
} }
// MARK: - Live Location Start Flow Tests
@Test
func startLiveLocationShowsDisclaimer() {
setupViewModel(liveLocationManagerConfiguration: .init(authorizationStatus: .authorizedAlways))
context.send(viewAction: .startLiveLocation)
#expect(context.alertInfo?.id == .liveLocationDisclaimer)
}
@Test
func startLiveLocationDisclaimerDeclineSkipsStart() {
let liveLocationManagerMock = LiveLocationManagerMock(.init(authorizationStatus: .authorizedAlways))
setupViewModel(liveLocationManagerMock: liveLocationManagerMock)
context.send(viewAction: .startLiveLocation)
context.alertInfo?.primaryButton.action?()
#expect(!liveLocationManagerMock.startLiveLocationRoomIDDurationCalled)
}
@Test
func startLiveLocationDisclaimerAcceptShowsDurationPicker() async throws {
setupViewModel(liveLocationManagerConfiguration: .init(authorizationStatus: .authorizedAlways))
context.send(viewAction: .startLiveLocation)
#expect(context.alertInfo?.id == .liveLocationDisclaimer)
let deferred = deferFulfillment(context.observe(\.alertInfo)) { $0?.id == .liveLocationDurationSelection }
context.alertInfo?.secondaryButton?.action?()
try await deferred.fulfill()
}
@Test
func startLiveLocationDurationPickerCancelSkipsStart() async throws {
let liveLocationManagerMock = LiveLocationManagerMock(.init(authorizationStatus: .authorizedAlways))
setupViewModel(liveLocationManagerMock: liveLocationManagerMock)
context.send(viewAction: .startLiveLocation)
let deferred = deferFulfillment(context.observe(\.alertInfo)) { $0?.id == .liveLocationDurationSelection }
context.alertInfo?.secondaryButton?.action?()
try await deferred.fulfill()
context.alertInfo?.primaryButton.action?()
#expect(!liveLocationManagerMock.startLiveLocationRoomIDDurationCalled)
}
@Test
func startLiveLocationSuccess() async throws {
let liveLocationManagerMock = LiveLocationManagerMock(.init(authorizationStatus: .authorizedAlways))
setupViewModel(liveLocationManagerMock: liveLocationManagerMock)
context.send(viewAction: .startLiveLocation)
let durationPicker = deferFulfillment(context.observe(\.alertInfo)) { $0?.id == .liveLocationDurationSelection }
context.alertInfo?.secondaryButton?.action?()
try await durationPicker.fulfill()
let deferred = deferFulfillment(viewModel.actions) { $0 == .close }
context.alertInfo?.verticalButtons?.first?.action?()
try await deferred.fulfill()
#expect(liveLocationManagerMock.startLiveLocationRoomIDDurationCalled)
let arguments = try #require(liveLocationManagerMock.startLiveLocationRoomIDDurationReceivedArguments)
#expect(arguments.duration == .seconds(60 * 15))
}
@Test
func startLiveLocationFailureDoesNotClose() async throws {
let liveLocationManagerMock = LiveLocationManagerMock(.init(authorizationStatus: .authorizedAlways))
liveLocationManagerMock.startLiveLocationRoomIDDurationReturnValue = .failure(.startFailed)
setupViewModel(liveLocationManagerMock: liveLocationManagerMock)
context.send(viewAction: .startLiveLocation)
let durationPicker = deferFulfillment(context.observe(\.alertInfo)) { $0?.id == .liveLocationDurationSelection }
context.alertInfo?.secondaryButton?.action?()
try await durationPicker.fulfill()
let deferredFailure = deferFailure(viewModel.actions, timeout: .seconds(1)) { $0 == .close }
context.alertInfo?.verticalButtons?.first?.action?()
try await deferredFailure.fulfill()
}
// MARK: - Private // MARK: - Private
private func setupViewModel(liveLocationManagerConfiguration: LiveLocationManagerMock.Configuration = .init()) { private func setupViewModel(liveLocationManagerConfiguration: LiveLocationManagerMock.Configuration = .init()) {
timelineProxy = TimelineProxyMock(.init()) timelineProxy = TimelineProxyMock(.init())
viewModel = LocationSharingScreenViewModel(interactionMode: .picker, viewModel = LocationSharingScreenViewModel(interactionMode: .picker,