From d457736673c5bb7455d51f2b08e5758d73c8b658 Mon Sep 17 00:00:00 2001 From: Mauro <34335419+Velin92@users.noreply.github.com> Date: Wed, 25 Oct 2023 19:16:37 +0200 Subject: [PATCH] Check Application state before sending RR (#1960) * check the state * better handling * protocol to mock the application state * test improvement --- ElementX.xcodeproj/project.pbxproj | 6 +- ElementX/Sources/Mocks/ApplicationMock.swift | 73 +++++++++++++++++ .../Mocks/Generated/GeneratedMocks.swift | 79 ++++++++++++++++++ .../RoomScreen/RoomScreenCoordinator.swift | 3 +- .../RoomScreen/RoomScreenViewModel.swift | 27 ++++--- .../RoomScreen/View/RoomHeaderView.swift | 6 +- .../Screens/RoomScreen/View/RoomScreen.swift | 3 +- .../TimelineReadReceiptsView.swift | 3 +- .../View/Timeline/UITimelineView.swift | 3 +- .../RoomScreen/View/TimelineView.swift | 3 +- .../Background/ApplicationProtocol.swift | 9 ++- UnitTests/Sources/BackgroundTaskTests.swift | 80 ++----------------- UnitTests/Sources/PillContextTests.swift | 9 ++- .../Sources/RoomScreenViewModelTests.swift | 37 ++++++--- 14 files changed, 235 insertions(+), 106 deletions(-) create mode 100644 ElementX/Sources/Mocks/ApplicationMock.swift diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 5ec5ec110..ca5ced104 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -634,6 +634,7 @@ A722F426FD81FC67706BB1E0 /* CustomLayoutLabelStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42236480CF0431535EBE8387 /* CustomLayoutLabelStyle.swift */; }; A743841F91B62B0E56217B04 /* SecureBackupKeyBackupScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58DCB219D7B7B0299358FF81 /* SecureBackupKeyBackupScreenUITests.swift */; }; A74438ED16F8683A4B793E6A /* AnalyticsSettingsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BCE3FAF40932AC7C7639AC4 /* AnalyticsSettingsScreenViewModel.swift */; }; + A7BACE682AE97D4500FFBBEA /* ApplicationMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7BACE672AE97D4500FFBBEA /* ApplicationMock.swift */; }; A7D48E44D485B143AADDB77D /* Strings+Untranslated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */; }; A7FD7B992E6EE6E5A8429197 /* RoomSummaryDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142808B69851451AC32A2CEA /* RoomSummaryDetails.swift */; }; A816F7087C495D85048AC50E /* RoomMemberDetailsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B6E30BB748F3F480F077969 /* RoomMemberDetailsScreenModels.swift */; }; @@ -1030,7 +1031,7 @@ 033DB41C51865A2E83174E87 /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = ""; }; 035177BCD8E8308B098AC3C2 /* WindowManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowManager.swift; sourceTree = ""; }; 0376C429FAB1687C3D905F3E /* MockCoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCoder.swift; sourceTree = ""; }; - 0392E3FDE372C9B56FEEED8B /* test_voice_message.m4a */ = {isa = PBXFileReference; path = test_voice_message.m4a; sourceTree = ""; }; + 0392E3FDE372C9B56FEEED8B /* test_voice_message.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = test_voice_message.m4a; sourceTree = ""; }; 03BA7958A4BB9C22CA8884EF /* WaveformViewDragGestureModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaveformViewDragGestureModifier.swift; sourceTree = ""; }; 03DD998E523D4EC93C7ED703 /* RoomNotificationSettingsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsScreenViewModelProtocol.swift; sourceTree = ""; }; 03FABD73FD8086EFAB699F42 /* MediaUploadPreviewScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreenViewModelTests.swift; sourceTree = ""; }; @@ -1585,6 +1586,7 @@ A6B891A6DA826E2461DBB40F /* PHGPostHogConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PHGPostHogConfiguration.swift; sourceTree = ""; }; A6C11AD9813045E44F950410 /* ElementCallWidgetDriverProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementCallWidgetDriverProtocol.swift; sourceTree = ""; }; A73A07BAEDD74C48795A996A /* AsyncSequence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncSequence.swift; sourceTree = ""; }; + A7BACE672AE97D4500FFBBEA /* ApplicationMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationMock.swift; sourceTree = ""; }; A7C4EA55DA62F9D0F984A2AE /* CollapsibleTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsibleTimelineItem.swift; sourceTree = ""; }; A861DA5932B128FE1DCB5CE2 /* InviteUsersScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersScreenCoordinator.swift; sourceTree = ""; }; A8903A9F615BBD0E6D7CD133 /* ApplicationProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationProtocol.swift; sourceTree = ""; }; @@ -2439,6 +2441,7 @@ AAD01F7FC2BBAC7351948595 /* UserProfile+Mock.swift */, B23135B06B044CB811139D2F /* Generated */, E5E545F92D01588360A9BAC5 /* SDK */, + A7BACE672AE97D4500FFBBEA /* ApplicationMock.swift */, ); path = Mocks; sourceTree = ""; @@ -5289,6 +5292,7 @@ 0F6C8033FA60CFD36F7CA205 /* AppLockSetupPINScreenViewModel.swift in Sources */, 4C356F5CCB4CDC99BFA45185 /* AppLockSetupPINScreenViewModelProtocol.swift in Sources */, 4E8A2A2CFEB212F14E49E1A1 /* AppLockSetupSettingsScreen.swift in Sources */, + A7BACE682AE97D4500FFBBEA /* ApplicationMock.swift in Sources */, 8DCD9CC5361FF22A5B2C20F1 /* AppLockSetupSettingsScreenCoordinator.swift in Sources */, 6E4E401BE97AC241DA7C7716 /* AppLockSetupSettingsScreenModels.swift in Sources */, 4807E8F51DB54F56B25E1C7E /* AppLockSetupSettingsScreenViewModel.swift in Sources */, diff --git a/ElementX/Sources/Mocks/ApplicationMock.swift b/ElementX/Sources/Mocks/ApplicationMock.swift new file mode 100644 index 000000000..172fbe162 --- /dev/null +++ b/ElementX/Sources/Mocks/ApplicationMock.swift @@ -0,0 +1,73 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit + +extension ApplicationMock { + static var `default`: ApplicationProtocol { + ApplicationMock(withState: .active, + backgroundTimeRemaining: 10, + allowTasks: true) + } + + static var mockBroken: ApplicationProtocol { + ApplicationMock(withState: .inactive, + backgroundTimeRemaining: 0, + allowTasks: false) + } + + static var mockAboutToSuspend: ApplicationProtocol { + ApplicationMock(withState: .background, + backgroundTimeRemaining: 2, + allowTasks: false) + } + + private static var bgTaskIdentifier = 0 + + convenience init(withState applicationState: UIApplication.State, + backgroundTimeRemaining: TimeInterval, + allowTasks: Bool) { + self.init() + + underlyingApplicationState = applicationState + underlyingBackgroundTimeRemaining = backgroundTimeRemaining + + beginBackgroundTaskExpirationHandlerClosure = { [weak self] handler in + guard let self else { + return .invalid + } + + guard allowTasks else { + return .invalid + } + + return beginBackgroundTask(withName: nil, expirationHandler: handler) + } + + beginBackgroundTaskWithNameExpirationHandlerClosure = { _, handler in + guard allowTasks else { + return .invalid + } + Self.bgTaskIdentifier += 1 + + let identifier = UIBackgroundTaskIdentifier(rawValue: Self.bgTaskIdentifier) + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + handler?() + } + return identifier + } + } +} diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 84a5f5acf..5053f0918 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -279,6 +279,85 @@ class AppLockServiceMock: AppLockServiceProtocol { } } } +class ApplicationMock: ApplicationProtocol { + var backgroundTimeRemaining: TimeInterval { + get { return underlyingBackgroundTimeRemaining } + set(value) { underlyingBackgroundTimeRemaining = value } + } + var underlyingBackgroundTimeRemaining: TimeInterval! + var applicationState: UIApplication.State { + get { return underlyingApplicationState } + set(value) { underlyingApplicationState = value } + } + var underlyingApplicationState: UIApplication.State! + + //MARK: - beginBackgroundTask + + var beginBackgroundTaskExpirationHandlerCallsCount = 0 + var beginBackgroundTaskExpirationHandlerCalled: Bool { + return beginBackgroundTaskExpirationHandlerCallsCount > 0 + } + var beginBackgroundTaskExpirationHandlerReturnValue: UIBackgroundTaskIdentifier! + var beginBackgroundTaskExpirationHandlerClosure: (((() -> Void)?) -> UIBackgroundTaskIdentifier)? + + func beginBackgroundTask(expirationHandler handler: (() -> Void)?) -> UIBackgroundTaskIdentifier { + beginBackgroundTaskExpirationHandlerCallsCount += 1 + if let beginBackgroundTaskExpirationHandlerClosure = beginBackgroundTaskExpirationHandlerClosure { + return beginBackgroundTaskExpirationHandlerClosure(handler) + } else { + return beginBackgroundTaskExpirationHandlerReturnValue + } + } + //MARK: - beginBackgroundTask + + var beginBackgroundTaskWithNameExpirationHandlerCallsCount = 0 + var beginBackgroundTaskWithNameExpirationHandlerCalled: Bool { + return beginBackgroundTaskWithNameExpirationHandlerCallsCount > 0 + } + var beginBackgroundTaskWithNameExpirationHandlerReturnValue: UIBackgroundTaskIdentifier! + var beginBackgroundTaskWithNameExpirationHandlerClosure: ((String?, (() -> Void)?) -> UIBackgroundTaskIdentifier)? + + func beginBackgroundTask(withName taskName: String?, expirationHandler handler: (() -> Void)?) -> UIBackgroundTaskIdentifier { + beginBackgroundTaskWithNameExpirationHandlerCallsCount += 1 + if let beginBackgroundTaskWithNameExpirationHandlerClosure = beginBackgroundTaskWithNameExpirationHandlerClosure { + return beginBackgroundTaskWithNameExpirationHandlerClosure(taskName, handler) + } else { + return beginBackgroundTaskWithNameExpirationHandlerReturnValue + } + } + //MARK: - endBackgroundTask + + var endBackgroundTaskCallsCount = 0 + var endBackgroundTaskCalled: Bool { + return endBackgroundTaskCallsCount > 0 + } + var endBackgroundTaskReceivedIdentifier: UIBackgroundTaskIdentifier? + var endBackgroundTaskReceivedInvocations: [UIBackgroundTaskIdentifier] = [] + var endBackgroundTaskClosure: ((UIBackgroundTaskIdentifier) -> Void)? + + func endBackgroundTask(_ identifier: UIBackgroundTaskIdentifier) { + endBackgroundTaskCallsCount += 1 + endBackgroundTaskReceivedIdentifier = identifier + endBackgroundTaskReceivedInvocations.append(identifier) + endBackgroundTaskClosure?(identifier) + } + //MARK: - open + + var openCallsCount = 0 + var openCalled: Bool { + return openCallsCount > 0 + } + var openReceivedUrl: URL? + var openReceivedInvocations: [URL] = [] + var openClosure: ((URL) -> Void)? + + func open(_ url: URL) { + openCallsCount += 1 + openReceivedUrl = url + openReceivedInvocations.append(url) + openClosure?(url) + } +} class AudioConverterMock: AudioConverterProtocol { //MARK: - convertToOpusOgg diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift index 49c68f81f..173d064cf 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift @@ -65,7 +65,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol { roomProxy: parameters.roomProxy, appSettings: parameters.appSettings, analytics: ServiceLocator.shared.analytics, - userIndicatorController: ServiceLocator.shared.userIndicatorController) + userIndicatorController: ServiceLocator.shared.userIndicatorController, + application: UIApplication.shared) wysiwygViewModel = WysiwygComposerViewModel(minHeight: ComposerConstant.minHeight, maxCompressedHeight: ComposerConstant.maxHeight, diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index c53738285..d85fe3b76 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -32,6 +32,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol private let roomProxy: RoomProxyProtocol private let appSettings: AppSettings private let analytics: AnalyticsService + private let application: ApplicationProtocol private unowned let userIndicatorController: UserIndicatorControllerProtocol private let notificationCenterProtocol: NotificationCenterProtocol private let voiceMessageRecorder: VoiceMessageRecorderProtocol @@ -48,6 +49,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol appSettings: AppSettings, analytics: AnalyticsService, userIndicatorController: UserIndicatorControllerProtocol, + application: ApplicationProtocol, notificationCenterProtocol: NotificationCenterProtocol = NotificationCenter.default) { self.roomProxy = roomProxy self.timelineController = timelineController @@ -56,6 +58,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol self.userIndicatorController = userIndicatorController self.notificationCenterProtocol = notificationCenterProtocol self.mediaPlayerProvider = mediaPlayerProvider + self.application = application voiceMessageRecorder = VoiceMessageRecorder(audioRecorder: AudioRecorder(), mediaPlayerProvider: mediaPlayerProvider) super.init(initialViewState: RoomScreenViewState(roomID: timelineController.roomID, @@ -327,16 +330,19 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol /// The ID of the newest item in the room that the user has seen. /// This includes both event based items and virtual items. private var lastReadItemID: TimelineItemIdentifier? - private func sendReadReceiptIfNeeded(for lastVisibleItemID: TimelineItemIdentifier) async -> Result { + private func sendReadReceiptIfNeeded(for lastVisibleItemID: TimelineItemIdentifier) async { + guard application.applicationState == .active else { return } + guard lastReadItemID != lastVisibleItemID, - let eventItemID = eventBasedItem(nearest: lastVisibleItemID) - else { return .success(()) } + let eventItemID = eventBasedItem(nearest: lastVisibleItemID) else { + return + } // Make sure the item is newer than the item that was last marked as read. if let lastReadItemIndex = state.timelineViewState.timelineIDs.firstIndex(of: lastReadItemID?.timelineID ?? ""), let lastVisibleItemIndex = state.timelineViewState.timelineIDs.firstIndex(of: eventItemID.timelineID), lastReadItemIndex > lastVisibleItemIndex { - return .success(()) + return } // Update the last read item ID to avoid attempting duplicate requests. @@ -348,10 +354,10 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol } switch await timelineController.sendReadReceipt(for: eventItemID) { - case .success: - return .success(()) - case .failure: - return .failure(.generic) + case .success(): + break + case let .failure(error): + MXLog.error("[TimelineViewController] Failed to send read receipt: \(error)") } } @@ -1028,7 +1034,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol private func openSystemSettings() { guard let url = URL(string: UIApplication.openSettingsURLString) else { return } - UIApplication.shared.open(url) + application.open(url) } } @@ -1061,7 +1067,8 @@ extension RoomScreenViewModel { roomProxy: RoomProxyMock(with: .init(displayName: "Preview room")), appSettings: ServiceLocator.shared.settings, analytics: ServiceLocator.shared.analytics, - userIndicatorController: ServiceLocator.shared.userIndicatorController) + userIndicatorController: ServiceLocator.shared.userIndicatorController, + application: ApplicationMock.default) } private struct ReplyInfo { diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomHeaderView.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomHeaderView.swift index ec1eaa531..5a8d50657 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomHeaderView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomHeaderView.swift @@ -61,7 +61,8 @@ struct RoomHeaderView_Previews: PreviewProvider, TestablePreview { roomProxy: RoomProxyMock(with: .init(displayName: "Some Room name", avatarURL: URL.picturesDirectory)), appSettings: ServiceLocator.shared.settings, analytics: ServiceLocator.shared.analytics, - userIndicatorController: ServiceLocator.shared.userIndicatorController) + userIndicatorController: ServiceLocator.shared.userIndicatorController, + application: ApplicationMock.default) RoomHeaderView(context: viewModel.context) .previewLayout(.sizeThatFits) @@ -76,7 +77,8 @@ struct RoomHeaderView_Previews: PreviewProvider, TestablePreview { roomProxy: RoomProxyMock(with: .init(displayName: "Some Room name")), appSettings: ServiceLocator.shared.settings, analytics: ServiceLocator.shared.analytics, - userIndicatorController: ServiceLocator.shared.userIndicatorController) + userIndicatorController: ServiceLocator.shared.userIndicatorController, + application: ApplicationMock.default) RoomHeaderView(context: viewModel.context) .previewLayout(.sizeThatFits) diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift index ce40ba00a..dbcdc8e93 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift @@ -195,7 +195,8 @@ struct RoomScreen_Previews: PreviewProvider, TestablePreview { roomProxy: RoomProxyMock(with: .init(displayName: "Preview room", isCallOngoing: true)), appSettings: ServiceLocator.shared.settings, analytics: ServiceLocator.shared.analytics, - userIndicatorController: ServiceLocator.shared.userIndicatorController) + userIndicatorController: ServiceLocator.shared.userIndicatorController, + application: ApplicationMock.default) static var previews: some View { NavigationStack { diff --git a/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineReadReceiptsView.swift b/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineReadReceiptsView.swift index 020b250df..faa8940c7 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineReadReceiptsView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineReadReceiptsView.swift @@ -65,7 +65,8 @@ struct TimelineReadReceiptsView_Previews: PreviewProvider, TestablePreview { members: members)), appSettings: ServiceLocator.shared.settings, analytics: ServiceLocator.shared.analytics, - userIndicatorController: ServiceLocator.shared.userIndicatorController) + userIndicatorController: ServiceLocator.shared.userIndicatorController, + application: ApplicationMock.default) static let singleReceipt = [ReadReceipt(userID: RoomMemberProxyMock.mockAlice.userID, formattedTimestamp: "Now")] static let doubleReceipt = [ReadReceipt(userID: RoomMemberProxyMock.mockAlice.userID, formattedTimestamp: "Now"), diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/UITimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/UITimelineView.swift index 4c041b269..13d452cdd 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/UITimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/UITimelineView.swift @@ -86,7 +86,8 @@ struct UITimelineView_Previews: PreviewProvider, TestablePreview { roomProxy: RoomProxyMock(with: .init(displayName: "Preview room")), appSettings: ServiceLocator.shared.settings, analytics: ServiceLocator.shared.analytics, - userIndicatorController: ServiceLocator.shared.userIndicatorController) + userIndicatorController: ServiceLocator.shared.userIndicatorController, + application: ApplicationMock.default) static var previews: some View { NavigationStack { diff --git a/ElementX/Sources/Screens/RoomScreen/View/TimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/TimelineView.swift index 339f5e312..e005afc93 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/TimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/TimelineView.swift @@ -174,7 +174,8 @@ struct TimelineView_Previews: PreviewProvider, TestablePreview { roomProxy: RoomProxyMock(with: .init(displayName: "Preview room")), appSettings: ServiceLocator.shared.settings, analytics: ServiceLocator.shared.analytics, - userIndicatorController: ServiceLocator.shared.userIndicatorController) + userIndicatorController: ServiceLocator.shared.userIndicatorController, + application: ApplicationMock.default) static var previews: some View { NavigationStack { diff --git a/ElementX/Sources/Services/Background/ApplicationProtocol.swift b/ElementX/Sources/Services/Background/ApplicationProtocol.swift index abf113cdc..f06d141bc 100644 --- a/ElementX/Sources/Services/Background/ApplicationProtocol.swift +++ b/ElementX/Sources/Services/Background/ApplicationProtocol.swift @@ -17,16 +17,23 @@ import Foundation import UIKit +// sourcery: AutoMockable protocol ApplicationProtocol { func beginBackgroundTask(expirationHandler handler: (() -> Void)?) -> UIBackgroundTaskIdentifier func beginBackgroundTask(withName taskName: String?, expirationHandler handler: (() -> Void)?) -> UIBackgroundTaskIdentifier func endBackgroundTask(_ identifier: UIBackgroundTaskIdentifier) + + func open(_ url: URL) var backgroundTimeRemaining: TimeInterval { get } var applicationState: UIApplication.State { get } } -extension UIApplication: ApplicationProtocol { } +extension UIApplication: ApplicationProtocol { + func open(_ url: URL) { + open(url, options: [:], completionHandler: nil) + } +} diff --git a/UnitTests/Sources/BackgroundTaskTests.swift b/UnitTests/Sources/BackgroundTaskTests.swift index b28d8bbb4..e657a1afb 100644 --- a/UnitTests/Sources/BackgroundTaskTests.swift +++ b/UnitTests/Sources/BackgroundTaskTests.swift @@ -35,7 +35,7 @@ class BackgroundTaskTests: XCTestCase { func testInitAndStop() { let service = UIKitBackgroundTaskService { - UIApplication.mockHealty + ApplicationMock.default } guard let task = service.startBackgroundTask(withName: Constants.bgTaskName) else { XCTFail("Failed to setup test conditions") @@ -53,7 +53,7 @@ class BackgroundTaskTests: XCTestCase { func testNotReusableInit() { let service = UIKitBackgroundTaskService { - UIApplication.mockHealty + ApplicationMock.default } // create two not reusable task with the same name @@ -70,7 +70,7 @@ class BackgroundTaskTests: XCTestCase { func testReusableInit() { let service = UIKitBackgroundTaskService { - UIApplication.mockHealty + ApplicationMock.default } // create two reusable task with the same name @@ -91,7 +91,7 @@ class BackgroundTaskTests: XCTestCase { func testMultipleStops() { let service = UIKitBackgroundTaskService { - UIApplication.mockHealty + ApplicationMock.default } // create two reusable task with the same name @@ -114,7 +114,7 @@ class BackgroundTaskTests: XCTestCase { func testNotValidReuse() { let service = UIKitBackgroundTaskService { - UIApplication.mockHealty + ApplicationMock.default } // create two reusable task with the same name @@ -136,7 +136,7 @@ class BackgroundTaskTests: XCTestCase { func testValidReuse() { let service = UIKitBackgroundTaskService { - UIApplication.mockHealty + ApplicationMock.default } // create two reusable task with the same name @@ -162,7 +162,7 @@ class BackgroundTaskTests: XCTestCase { func testBrokenApp() { let service = UIKitBackgroundTaskService { - UIApplication.mockBroken + ApplicationMock.mockBroken } // create two reusable task with the same name @@ -173,7 +173,7 @@ class BackgroundTaskTests: XCTestCase { func testNoTimeApp() { let service = UIKitBackgroundTaskService { - UIApplication.mockAboutToSuspend + ApplicationMock.mockAboutToSuspend } // create two reusable task with the same name @@ -182,67 +182,3 @@ class BackgroundTaskTests: XCTestCase { XCTAssertNil(task, "Task should not be created") } } - -private extension UIApplication { - static var mockHealty: ApplicationProtocol { - MockApplication() - } - - static var mockBroken: ApplicationProtocol { - MockApplication(withState: .inactive, - backgroundTimeRemaining: 0, - allowTasks: false) - } - - static var mockAboutToSuspend: ApplicationProtocol { - MockApplication(withState: .background, - backgroundTimeRemaining: 2, - allowTasks: false) - } -} - -private class MockApplication: ApplicationProtocol { - let applicationState: UIApplication.State - let backgroundTimeRemaining: TimeInterval - private let allowTasks: Bool - - init(withState applicationState: UIApplication.State = .active, - backgroundTimeRemaining: TimeInterval = 10, - allowTasks: Bool = true) { - self.applicationState = applicationState - self.backgroundTimeRemaining = backgroundTimeRemaining - self.allowTasks = allowTasks - } - - private static var bgTaskIdentifier = 0 - - private var bgTasks: [UIBackgroundTaskIdentifier: Bool] = [:] - - func beginBackgroundTask(expirationHandler handler: (() -> Void)?) -> UIBackgroundTaskIdentifier { - guard allowTasks else { - return .invalid - } - return beginBackgroundTask(withName: nil, expirationHandler: handler) - } - - func beginBackgroundTask(withName taskName: String?, expirationHandler handler: (() -> Void)?) -> UIBackgroundTaskIdentifier { - guard allowTasks else { - return .invalid - } - Self.bgTaskIdentifier += 1 - - let identifier = UIBackgroundTaskIdentifier(rawValue: Self.bgTaskIdentifier) - bgTasks[identifier] = true - DispatchQueue.main.asyncAfter(deadline: .now() + 2) { - handler?() - } - return identifier - } - - func endBackgroundTask(_ identifier: UIBackgroundTaskIdentifier) { - guard allowTasks else { - return - } - bgTasks.removeValue(forKey: identifier) - } -} diff --git a/UnitTests/Sources/PillContextTests.swift b/UnitTests/Sources/PillContextTests.swift index ab973b13b..08ec099e1 100644 --- a/UnitTests/Sources/PillContextTests.swift +++ b/UnitTests/Sources/PillContextTests.swift @@ -32,7 +32,8 @@ class PillContextTests: XCTestCase { roomProxy: proxyMock, appSettings: ServiceLocator.shared.settings, analytics: ServiceLocator.shared.analytics, - userIndicatorController: ServiceLocator.shared.userIndicatorController) + userIndicatorController: ServiceLocator.shared.userIndicatorController, + application: ApplicationMock.default) let context = PillContext(roomContext: mock.context, data: PillTextAttachmentData(type: .user(userID: id), font: .preferredFont(forTextStyle: .body))) XCTAssertFalse(context.viewState.isOwnMention) @@ -64,7 +65,8 @@ class PillContextTests: XCTestCase { roomProxy: proxyMock, appSettings: ServiceLocator.shared.settings, analytics: ServiceLocator.shared.analytics, - userIndicatorController: ServiceLocator.shared.userIndicatorController) + userIndicatorController: ServiceLocator.shared.userIndicatorController, + application: ApplicationMock.default) let context = PillContext(roomContext: mock.context, data: PillTextAttachmentData(type: .user(userID: id), font: .preferredFont(forTextStyle: .body))) XCTAssertTrue(context.viewState.isOwnMention) @@ -83,7 +85,8 @@ class PillContextTests: XCTestCase { roomProxy: proxyMock, appSettings: ServiceLocator.shared.settings, analytics: ServiceLocator.shared.analytics, - userIndicatorController: ServiceLocator.shared.userIndicatorController) + userIndicatorController: ServiceLocator.shared.userIndicatorController, + application: ApplicationMock.default) let context = PillContext(roomContext: mock.context, data: PillTextAttachmentData(type: .allUsers, font: .preferredFont(forTextStyle: .body))) XCTAssertTrue(context.viewState.isOwnMention) diff --git a/UnitTests/Sources/RoomScreenViewModelTests.swift b/UnitTests/Sources/RoomScreenViewModelTests.swift index c27c2668d..89b97dc1c 100644 --- a/UnitTests/Sources/RoomScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomScreenViewModelTests.swift @@ -55,7 +55,8 @@ class RoomScreenViewModelTests: XCTestCase { roomProxy: RoomProxyMock(with: .init(displayName: "")), appSettings: ServiceLocator.shared.settings, analytics: ServiceLocator.shared.analytics, - userIndicatorController: userIndicatorControllerMock) + userIndicatorController: userIndicatorControllerMock, + application: ApplicationMock.default) // Then the messages should be grouped together. XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[0].groupStyle, .first, "Nothing should prevent the first message from being grouped.") @@ -89,7 +90,8 @@ class RoomScreenViewModelTests: XCTestCase { roomProxy: RoomProxyMock(with: .init(displayName: "")), appSettings: ServiceLocator.shared.settings, analytics: ServiceLocator.shared.analytics, - userIndicatorController: userIndicatorControllerMock) + userIndicatorController: userIndicatorControllerMock, + application: ApplicationMock.default) // Then the messages should be grouped by sender. XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[0].groupStyle, .single, "A message should not be grouped when the sender changes.") @@ -121,7 +123,8 @@ class RoomScreenViewModelTests: XCTestCase { roomProxy: RoomProxyMock(with: .init(displayName: "")), appSettings: ServiceLocator.shared.settings, analytics: ServiceLocator.shared.analytics, - userIndicatorController: userIndicatorControllerMock) + userIndicatorController: userIndicatorControllerMock, + application: ApplicationMock.default) // Then the first message should not be grouped but the other two should. XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[0].groupStyle, .single, "When the first message has reactions it should not be grouped.") @@ -150,7 +153,8 @@ class RoomScreenViewModelTests: XCTestCase { roomProxy: RoomProxyMock(with: .init(displayName: "")), appSettings: ServiceLocator.shared.settings, analytics: ServiceLocator.shared.analytics, - userIndicatorController: userIndicatorControllerMock) + userIndicatorController: userIndicatorControllerMock, + application: ApplicationMock.default) // Then the first and second messages should be grouped and the last one should not. XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[0].groupStyle, .first, "Nothing should prevent the first message from being grouped.") @@ -179,7 +183,8 @@ class RoomScreenViewModelTests: XCTestCase { roomProxy: RoomProxyMock(with: .init(displayName: "")), appSettings: ServiceLocator.shared.settings, analytics: ServiceLocator.shared.analytics, - userIndicatorController: userIndicatorControllerMock) + userIndicatorController: userIndicatorControllerMock, + application: ApplicationMock.default) // Then the messages should be grouped together. XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[0].groupStyle, .first, "Nothing should prevent the first message from being grouped.") @@ -204,7 +209,8 @@ class RoomScreenViewModelTests: XCTestCase { roomProxy: roomProxyMock, appSettings: ServiceLocator.shared.settings, analytics: ServiceLocator.shared.analytics, - userIndicatorController: userIndicatorControllerMock) + userIndicatorController: userIndicatorControllerMock, + application: ApplicationMock.default) viewModel.actions .sink { action in switch action { @@ -243,7 +249,8 @@ class RoomScreenViewModelTests: XCTestCase { roomProxy: roomProxyMock, appSettings: ServiceLocator.shared.settings, analytics: ServiceLocator.shared.analytics, - userIndicatorController: userIndicatorControllerMock) + userIndicatorController: userIndicatorControllerMock, + application: ApplicationMock.default) viewModel.actions .sink { action in @@ -283,7 +290,8 @@ class RoomScreenViewModelTests: XCTestCase { roomProxy: roomProxyMock, appSettings: ServiceLocator.shared.settings, analytics: ServiceLocator.shared.analytics, - userIndicatorController: userIndicatorControllerMock) + userIndicatorController: userIndicatorControllerMock, + application: ApplicationMock.default) viewModel.actions .sink { _ in XCTFail("Should not receive any action") @@ -315,7 +323,8 @@ class RoomScreenViewModelTests: XCTestCase { roomProxy: roomProxyMock, appSettings: ServiceLocator.shared.settings, analytics: ServiceLocator.shared.analytics, - userIndicatorController: userIndicatorControllerMock) + userIndicatorController: userIndicatorControllerMock, + application: ApplicationMock.default) viewModel.context.send(viewAction: .retrySend(itemID: .init(timelineID: UUID().uuidString, transactionID: "test retry send id"))) @@ -335,7 +344,8 @@ class RoomScreenViewModelTests: XCTestCase { roomProxy: roomProxyMock, appSettings: ServiceLocator.shared.settings, analytics: ServiceLocator.shared.analytics, - userIndicatorController: userIndicatorControllerMock) + userIndicatorController: userIndicatorControllerMock, + application: ApplicationMock.default) viewModel.context.send(viewAction: .retrySend(itemID: .random)) @@ -354,7 +364,8 @@ class RoomScreenViewModelTests: XCTestCase { roomProxy: roomProxyMock, appSettings: ServiceLocator.shared.settings, analytics: ServiceLocator.shared.analytics, - userIndicatorController: userIndicatorControllerMock) + userIndicatorController: userIndicatorControllerMock, + application: ApplicationMock.default) viewModel.context.send(viewAction: .cancelSend(itemID: .init(timelineID: UUID().uuidString, transactionID: "test cancel send id"))) @@ -374,7 +385,8 @@ class RoomScreenViewModelTests: XCTestCase { roomProxy: roomProxyMock, appSettings: ServiceLocator.shared.settings, analytics: ServiceLocator.shared.analytics, - userIndicatorController: userIndicatorControllerMock) + userIndicatorController: userIndicatorControllerMock, + application: ApplicationMock.default) viewModel.context.send(viewAction: .cancelSend(itemID: .random)) @@ -512,6 +524,7 @@ class RoomScreenViewModelTests: XCTestCase { appSettings: ServiceLocator.shared.settings, analytics: ServiceLocator.shared.analytics, userIndicatorController: userIndicatorControllerMock, + application: ApplicationMock.default, notificationCenterProtocol: notificationCenter) return (viewModel, roomProxy, timelineController, notificationCenter)