From 524bef6d6037851484ab4d003ba0647320f972b4 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Thu, 2 Apr 2026 21:54:29 +0200 Subject: [PATCH] add new tests to LocationSharingViewModel --- .../Mocks/LiveLocationManagerMock.swift | 1 + .../LocationSharingScreenViewModel.swift | 26 +++--- .../LocationSharingScreenViewModelTests.swift | 88 ++++++++++++++++--- 3 files changed, 89 insertions(+), 26 deletions(-) diff --git a/ElementX/Sources/Mocks/LiveLocationManagerMock.swift b/ElementX/Sources/Mocks/LiveLocationManagerMock.swift index dc3100e7d..2126a8699 100644 --- a/ElementX/Sources/Mocks/LiveLocationManagerMock.swift +++ b/ElementX/Sources/Mocks/LiveLocationManagerMock.swift @@ -21,5 +21,6 @@ extension LiveLocationManagerMock { underlyingAuthorizationStatus = .init(authorizationStatusSubject) requestAlwaysAuthorizationIfPossibleReturnValue = configuration.requestAlwaysAuthorizationIfPossibleReturnValue + startLiveLocationRoomIDDurationReturnValue = .success(()) } } diff --git a/ElementX/Sources/Screens/LocationSharing/LocationSharingScreenViewModel.swift b/ElementX/Sources/Screens/LocationSharing/LocationSharingScreenViewModel.swift index d3734eddc..bf78ed980 100644 --- a/ElementX/Sources/Screens/LocationSharing/LocationSharingScreenViewModel.swift +++ b/ElementX/Sources/Screens/LocationSharing/LocationSharingScreenViewModel.swift @@ -59,7 +59,7 @@ class LocationSharingScreenViewModel: LocationSharingScreenViewModelType, Locati case .close: actionsSubject.send(.close) case .startLiveLocation: - startLiveLocationSharing() + checkAlwaysShareLocationPermission() case .selectLocation: guard let coordinate = state.bindings.mapCenterLocation else { return } let uncertainty = state.isSharingUserLocation ? context.geolocationUncertainty : nil @@ -118,12 +118,8 @@ class LocationSharingScreenViewModel: LocationSharingScreenViewModelType, Locati formatter.allowedUnits = [.hour, .minute] return formatter }() - - private func startLiveLocationSharing() { - requestAlwaysLocationPermission() - } - - private func requestAlwaysLocationPermission() { + + private func checkAlwaysShareLocationPermission() { authorizationStatusSubscription = nil let authorizationStatus = liveLocationManager.authorizationStatus.value 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 .sink { [weak self] newValue in guard newValue == .authorizedWhenInUse else { return } - self?.requestAlwaysLocationPermission() + self?.checkAlwaysShareLocationPermission() } case .authorizedWhenInUse: guard liveLocationManager.requestAlwaysAuthorizationIfPossible() else { @@ -171,14 +167,14 @@ class LocationSharingScreenViewModel: LocationSharingScreenViewModelType, Locati } private func showLiveLocationDurationPicker() { - let durations: [TimeInterval] = [15 * 60, // 15 minutes - 60 * 60, // 1 hour - 8 * 60 * 60] // 8 hours + let durations: [Duration] = [.seconds(15 * 60), // 15 minutes + .seconds(60 * 60), // 1 hour + .seconds(60 * 60 * 8)] // 8 hours let durationButtons: [AlertInfo.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 - 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) } - private func startLiveLocationSharingInRoom(durationMillis: UInt64) async { + private func startLiveLocationSharingInRoom(duration: Duration) async { let result = await liveLocationManager.startLiveLocation(roomID: roomProxy.id, - durationMillis: durationMillis) + duration: duration) switch result { case .success: diff --git a/UnitTests/Sources/LocationSharingScreenViewModelTests.swift b/UnitTests/Sources/LocationSharingScreenViewModelTests.swift index 0c9541d48..b1dc97acb 100644 --- a/UnitTests/Sources/LocationSharingScreenViewModelTests.swift +++ b/UnitTests/Sources/LocationSharingScreenViewModelTests.swift @@ -71,11 +71,11 @@ final class LocationSharingScreenViewModelTests { @Test func errorMapping() { setupViewModel() - let mapError = AlertInfo(locationSharingViewError: .mapError(.failedLoadingMap)) + let mapError = AlertInfo(alertID: .mapError(.failedLoadingMap)) #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)) - let AuthorizationError = AlertInfo(locationSharingViewError: .missingAuthorization) + let AuthorizationError = AlertInfo(alertID: .missingAuthorization) #expect(AuthorizationError.message == L10n.dialogPermissionLocationDescriptionIos(InfoPlistReader.main.bundleDisplayName)) } @@ -140,13 +140,6 @@ final class LocationSharingScreenViewModelTests { #expect(context.alertInfo?.id == .missingAlwaysAuthorization) } - @Test - func startLiveLocationWithAlwaysAuthorization() { - setupViewModel(liveLocationManagerConfiguration: .init(authorizationStatus: .authorizedAlways)) - context.send(viewAction: .startLiveLocation) - #expect(context.alertInfo == nil) - } - @Test func startLiveLocationWithWhenInUseAuthorizationAlreadyRequested() { setupViewModel(liveLocationManagerConfiguration: .init(authorizationStatus: .authorizedWhenInUse, @@ -191,8 +184,81 @@ final class LocationSharingScreenViewModelTests { #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 - + private func setupViewModel(liveLocationManagerConfiguration: LiveLocationManagerMock.Configuration = .init()) { timelineProxy = TimelineProxyMock(.init()) viewModel = LocationSharingScreenViewModel(interactionMode: .picker,