// // Copyright 2025 Element Creations Ltd. // Copyright 2022-2025 New Vector Ltd. // // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. // Please see LICENSE files in the repository root for full details. // import Combine import MatrixRustSDK import MatrixRustSDKMocks import SwiftUI import UIKit class UITestsAppCoordinator: AppCoordinatorProtocol, SecureWindowManagerDelegate { private let appSettings: AppSettings private let analytics: AnalyticsServiceProtocol private let userIndicatorController: UserIndicatorControllerProtocol private let navigationRootCoordinator: NavigationRootCoordinator // periphery:ignore - retaining purpose private var mockScreen: MockScreen? // periphery:ignore - retaining purpose private var alternateWindowMockScreen: MockScreen? let windowManager: SecureWindowManagerProtocol init(appDelegate: AppDelegate) { windowManager = WindowManager(appDelegate: appDelegate) // disabling View animations UIView.setAnimationsEnabled(false) navigationRootCoordinator = NavigationRootCoordinator() MXLog.configure(currentTarget: "uitests") appSettings = AppSettings.volatile() let analyticsClient = AnalyticsClientMock() analyticsClient.isRunning = false analytics = AnalyticsServiceMock(.init()) userIndicatorController = UserIndicatorController() windowManager.delegate = self } func start() { guard let screenID = ProcessInfo.testScreenID else { fatalError("Unable to launch with unknown screen.") } let mockScreen = MockScreen(id: screenID, windowManager: windowManager, navigationRootCoordinator: navigationRootCoordinator, appSettings: appSettings, analytics: analytics, userIndicatorController: userIndicatorController) if let coordinator = mockScreen.coordinator { navigationRootCoordinator.setRootCoordinator(coordinator) } self.mockScreen = mockScreen } func toPresentable() -> AnyView { AnyView(navigationRootCoordinator.toPresentable() .environment(\.analyticsService, analytics)) } func handlePotentialPhishingAttempt(url: URL, openURLAction: @escaping (URL) -> Void) -> Bool { fatalError("Not implemented.") } func handleDeepLink(_ url: URL, isExternalURL: Bool, windowType: SecondaryWindowType?) -> Bool { fatalError("Not implemented.") } func handleAppRoute(_ appRoute: AppRoute, windowType: SecondaryWindowType?) { fatalError("Not implemented.") } func handleUserActivity(_ activity: NSUserActivity) { fatalError("Not implemented.") } func windowManagerDidConfigureWindows(_ windowManager: SecureWindowManagerProtocol) { userIndicatorController.window = windowManager.overlayWindow // Set up the alternate window for the App Lock flow coordinator tests. guard let screenID = ProcessInfo.testScreenID, screenID == .appLockFlow || screenID == .appLockFlowDisabled else { return } let screen = MockScreen(id: screenID == .appLockFlow ? .appLockFlowAlternateWindow : .appLockFlowDisabledAlternateWindow, windowManager: windowManager, navigationRootCoordinator: navigationRootCoordinator, appSettings: appSettings, analytics: analytics, userIndicatorController: userIndicatorController) guard let coordinator = screen.coordinator else { fatalError() } windowManager.alternateWindow.rootViewController = UIHostingController(rootView: coordinator.toPresentable().statusBarHidden()) alternateWindowMockScreen = screen } } class MockScreen: Identifiable { let id: UITestsScreenIdentifier let windowManager: SecureWindowManagerProtocol let navigationRootCoordinator: NavigationRootCoordinator private let appSettings: AppSettings private let analytics: AnalyticsServiceProtocol private let userIndicatorController: UserIndicatorControllerProtocol private var client: UITestsSignalling.Client? private var retainedState = [Any]() private var cancellables = Set() init(id: UITestsScreenIdentifier, windowManager: SecureWindowManagerProtocol, navigationRootCoordinator: NavigationRootCoordinator, appSettings: AppSettings, analytics: AnalyticsServiceProtocol, userIndicatorController: UserIndicatorControllerProtocol) { self.id = id self.windowManager = windowManager self.navigationRootCoordinator = navigationRootCoordinator self.appSettings = appSettings self.analytics = analytics self.userIndicatorController = userIndicatorController } lazy var coordinator: CoordinatorProtocol? = { switch id { case .serverSelection: let navigationStackCoordinator = NavigationStackCoordinator() let coordinator = ServerSelectionScreenCoordinator(parameters: .init(authenticationService: AuthenticationService.mock, authenticationFlow: .login, appSettings: appSettings, userIndicatorController: userIndicatorController)) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator case .authenticationFlow, .provisionedAuthenticationFlow, .singleProviderAuthenticationFlow, .multipleProvidersAuthenticationFlow: let appSettings: AppSettings! = appSettings if id == .singleProviderAuthenticationFlow || id == .multipleProvidersAuthenticationFlow { let accountProviders = id == .singleProviderAuthenticationFlow ? ["example.com"] : ["guest.example.com", "example.com"] appSettings.override(accountProviders: accountProviders, allowOtherAccountProviders: false, hideBrandChrome: false, pushGatewayBaseURL: appSettings.pushGatewayBaseURL, oAuthRedirectURL: appSettings.oAuthRedirectURL, websiteURL: appSettings.websiteURL, logoURL: appSettings.logoURL, copyrightURL: appSettings.copyrightURL, acceptableUseURL: appSettings.acceptableUseURL, privacyURL: appSettings.privacyURL, encryptionURL: appSettings.encryptionURL, deviceVerificationURL: appSettings.deviceVerificationURL, chatBackupDetailsURL: appSettings.chatBackupDetailsURL, identityPinningViolationDetailsURL: appSettings.identityPinningViolationDetailsURL, historySharingDetailsURL: appSettings.historySharingDetailsURL, elementWebHosts: appSettings.elementWebHosts, accountProvisioningHost: appSettings.accountProvisioningHost, bugReportApplicationID: appSettings.bugReportApplicationID, analyticsTermsURL: appSettings.analyticsTermsURL, mapTilerConfiguration: AppSettings.bundledMapTilerConfiguration) } let flowCoordinator = AuthenticationFlowCoordinator(authenticationService: AuthenticationService.mock, bugReportService: BugReportServiceMock(.init()), navigationRootCoordinator: navigationRootCoordinator, appMediator: AppMediatorMock(.init()), appSettings: appSettings, appHooks: AppHooks(), analytics: analytics, userIndicatorController: userIndicatorController) flowCoordinator.start() retainedState.append(flowCoordinator) if id == .provisionedAuthenticationFlow { flowCoordinator.handleAppRoute(.accountProvisioningLink(.init(accountProvider: "example.com", loginHint: nil)), animated: false) } return nil case .appLockFlow, .appLockFlowDisabled: // The tested coordinator is setup below in the alternate window. // Here we just return a blank screen to snapshot as the unlocked app. return BlankFormCoordinator() case .appLockFlowAlternateWindow, .appLockFlowDisabledAlternateWindow: let navigationCoordinator = NavigationRootCoordinator() let keychainController = KeychainController(service: .tests, accessGroup: InfoPlistReader.main.keychainAccessGroupIdentifier) keychainController.resetSecrets() let context = LAContextMock() context.biometryTypeValue = UIDevice.current.isPhone ? .faceID : .touchID // (iPhone 14 & iPad 9th gen) context.evaluatePolicyReturnValue = true context.evaluatedPolicyDomainStateValue = Data("😎".utf8) let appLockService = AppLockService(keychainController: keychainController, appSettings: appSettings, context: context) if id == .appLockFlowAlternateWindow { let pinCode = "2023" guard case .success = appLockService.setupPINCode(pinCode), appLockService.unlock(with: pinCode) else { fatalError("Failed to preset the PIN code.") } } let notificationCenter = UITestsNotificationCenter() do { try notificationCenter.startListening() } catch { fatalError("Failed to start listening for notifications.") } let flowCoordinator = AppLockFlowCoordinator(initialState: .unlocked, appLockService: appLockService, navigationCoordinator: navigationCoordinator, notificationCenter: notificationCenter, appSettings: appSettings) flowCoordinator.actions .sink { [weak self] action in guard let self else { return } switch action { case .lockApp: windowManager.switchToAlternate() case .unlockApp: windowManager.switchToMain() case .forceLogout: break } } .store(in: &cancellables) retainedState.append(flowCoordinator) return navigationCoordinator case .appLockSetupFlow, .appLockSetupFlowUnlock, .appLockSetupFlowMandatory: let navigationStackCoordinator = NavigationStackCoordinator() // The flow expects an existing root coordinator, use a blank form as a placeholder. navigationStackCoordinator.setRootCoordinator(BlankFormCoordinator()) let keychainController = KeychainController(service: .tests, accessGroup: InfoPlistReader.main.keychainAccessGroupIdentifier) keychainController.resetSecrets() let context = LAContextMock() context.biometryTypeValue = UIDevice.current.isPhone ? .faceID : .touchID // (iPhone 14 & iPad 9th gen) context.evaluatePolicyReturnValue = true context.evaluatedPolicyDomainStateValue = Data("😎".utf8) let appLockService = AppLockService(keychainController: keychainController, appSettings: appSettings, context: context) if id == .appLockSetupFlowUnlock, case .failure = appLockService.setupPINCode("2023") { fatalError("Failed to pre-set the PIN code") } let flow: AppLockSetupFlowCoordinator.PresentationFlow = id == .appLockSetupFlowMandatory ? .onboarding : .settings let flowCoordinator = AppLockSetupFlowCoordinator(presentingFlow: flow, appLockService: appLockService, navigationStackCoordinator: navigationStackCoordinator) flowCoordinator.start() retainedState.append(flowCoordinator) return navigationStackCoordinator case .bugReport: let navigationStackCoordinator = NavigationStackCoordinator() let clientProxy = ClientProxyMock(.init(userID: "@mock:client.com", roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms))))) let userSession = UserSessionMock(.init(clientProxy: clientProxy)) let coordinator = BugReportScreenCoordinator(parameters: .init(bugReportService: BugReportServiceMock(.init()), userSession: userSession, userIndicatorController: nil, screenshot: nil, isModallyPresented: true)) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator case .roomPlainNoAvatar: let navigationStackCoordinator = NavigationStackCoordinator() let parameters = RoomScreenCoordinatorParameters(userSession: UserSessionMock(.init()), roomProxy: JoinedRoomProxyMock(.init(name: "Some room name", avatarURL: nil)), timelineController: TimelineControllerMock(.init()), mediaPlayerProvider: MediaPlayerProviderMock(), emojiProvider: EmojiProvider(appSettings: appSettings), linkMetadataProvider: LinkMetadataProvider(), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock(.init()), appSettings: appSettings, appHooks: AppHooks(), analytics: analytics, composerDraftService: ComposerDraftServiceMock(.init()), timelineControllerFactory: TimelineControllerFactoryMock(.init()), userIndicatorController: UserIndicatorControllerMock()) let coordinator = RoomScreenCoordinator(parameters: parameters) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator case .roomSmallTimeline: let navigationStackCoordinator = NavigationStackCoordinator() let timelineController = TimelineControllerMock(.init(timelineItems: TimelineFixtures.smallChunk)) let parameters = RoomScreenCoordinatorParameters(userSession: UserSessionMock(.init()), roomProxy: JoinedRoomProxyMock(.init(name: "New room", avatarURL: .mockMXCAvatar)), timelineController: timelineController, mediaPlayerProvider: MediaPlayerProviderMock(), emojiProvider: EmojiProvider(appSettings: appSettings), linkMetadataProvider: LinkMetadataProvider(), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock(.init()), appSettings: appSettings, appHooks: AppHooks(), analytics: analytics, composerDraftService: ComposerDraftServiceMock(.init()), timelineControllerFactory: TimelineControllerFactoryMock(.init()), userIndicatorController: UserIndicatorControllerMock()) let coordinator = RoomScreenCoordinator(parameters: parameters) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator case .roomSmallTimelineWithReactions: let navigationStackCoordinator = NavigationStackCoordinator() let timelineController = TimelineControllerMock(.init(timelineItems: TimelineFixtures.default)) let parameters = RoomScreenCoordinatorParameters(userSession: UserSessionMock(.init()), roomProxy: JoinedRoomProxyMock(.init(name: "New room", avatarURL: .mockMXCAvatar)), timelineController: timelineController, mediaPlayerProvider: MediaPlayerProviderMock(), emojiProvider: EmojiProvider(appSettings: appSettings), linkMetadataProvider: LinkMetadataProvider(), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock(.init()), appSettings: appSettings, appHooks: AppHooks(), analytics: analytics, composerDraftService: ComposerDraftServiceMock(.init()), timelineControllerFactory: TimelineControllerFactoryMock(.init()), userIndicatorController: UserIndicatorControllerMock()) let coordinator = RoomScreenCoordinator(parameters: parameters) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator case .roomSmallTimelineWithReadReceipts: let navigationStackCoordinator = NavigationStackCoordinator() let timelineController = TimelineControllerMock(.init(timelineItems: TimelineFixtures.smallChunkWithReadReceipts)) let parameters = RoomScreenCoordinatorParameters(userSession: UserSessionMock(.init()), roomProxy: JoinedRoomProxyMock(.init(name: "New room", avatarURL: .mockMXCAvatar)), timelineController: timelineController, mediaPlayerProvider: MediaPlayerProviderMock(), emojiProvider: EmojiProvider(appSettings: appSettings), linkMetadataProvider: LinkMetadataProvider(), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock(.init()), appSettings: appSettings, appHooks: AppHooks(), analytics: analytics, composerDraftService: ComposerDraftServiceMock(.init()), timelineControllerFactory: TimelineControllerFactoryMock(.init()), userIndicatorController: UserIndicatorControllerMock()) let coordinator = RoomScreenCoordinator(parameters: parameters) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator case .roomSmallTimelineIncomingAndSmallPagination: let navigationStackCoordinator = NavigationStackCoordinator() let itemProvider = SignallingTimelineItemProviderMock(itemProxies: TimelineFixtures.smallChunkProxies, listenForSignals: true) itemProvider.backPaginationResponses = [TimelineFixtures.singleMessageChunkProxies] itemProvider.incomingItems = [TimelineFixtures.incomingMessageProxy] let roomProxy = JoinedRoomProxyMock(.init(name: "Small timeline", avatarURL: .mockMXCAvatar)) let timelineController = makeSignallingTimelineController(itemProvider: itemProvider, roomProxy: roomProxy) let parameters = RoomScreenCoordinatorParameters(userSession: UserSessionMock(.init()), roomProxy: roomProxy, timelineController: timelineController, mediaPlayerProvider: MediaPlayerProviderMock(), emojiProvider: EmojiProvider(appSettings: appSettings), linkMetadataProvider: LinkMetadataProvider(), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock(.init()), appSettings: appSettings, appHooks: AppHooks(), analytics: analytics, composerDraftService: ComposerDraftServiceMock(.init()), timelineControllerFactory: TimelineControllerFactoryMock(.init()), userIndicatorController: UserIndicatorControllerMock()) let coordinator = RoomScreenCoordinator(parameters: parameters) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator case .roomSmallTimelineLargePagination: let navigationStackCoordinator = NavigationStackCoordinator() let itemProvider = SignallingTimelineItemProviderMock(itemProxies: TimelineFixtures.smallChunkProxies, listenForSignals: true) itemProvider.backPaginationResponses = [TimelineFixtures.largeChunkProxies] let roomProxy = JoinedRoomProxyMock(.init(name: "Small timeline, paginating", avatarURL: .mockMXCAvatar)) let timelineController = makeSignallingTimelineController(itemProvider: itemProvider, roomProxy: roomProxy) let parameters = RoomScreenCoordinatorParameters(userSession: UserSessionMock(.init()), roomProxy: roomProxy, timelineController: timelineController, mediaPlayerProvider: MediaPlayerProviderMock(), emojiProvider: EmojiProvider(appSettings: appSettings), linkMetadataProvider: LinkMetadataProvider(), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock(.init()), appSettings: appSettings, appHooks: AppHooks(), analytics: analytics, composerDraftService: ComposerDraftServiceMock(.init()), timelineControllerFactory: TimelineControllerFactoryMock(.init()), userIndicatorController: UserIndicatorControllerMock()) let coordinator = RoomScreenCoordinator(parameters: parameters) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator case .roomLayoutTop: let navigationStackCoordinator = NavigationStackCoordinator() let itemProvider = SignallingTimelineItemProviderMock(itemProxies: TimelineFixtures.largeChunkProxies, listenForSignals: true) itemProvider.backPaginationResponses = [TimelineFixtures.largeChunkProxies] let roomProxy = JoinedRoomProxyMock(.init(name: "Large timeline", avatarURL: .mockMXCAvatar)) let timelineController = makeSignallingTimelineController(itemProvider: itemProvider, roomProxy: roomProxy) let parameters = RoomScreenCoordinatorParameters(userSession: UserSessionMock(.init()), roomProxy: roomProxy, timelineController: timelineController, mediaPlayerProvider: MediaPlayerProviderMock(), emojiProvider: EmojiProvider(appSettings: appSettings), linkMetadataProvider: LinkMetadataProvider(), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock(.init()), appSettings: appSettings, appHooks: AppHooks(), analytics: analytics, composerDraftService: ComposerDraftServiceMock(.init()), timelineControllerFactory: TimelineControllerFactoryMock(.init()), userIndicatorController: UserIndicatorControllerMock()) let coordinator = RoomScreenCoordinator(parameters: parameters) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator case .roomLayoutMiddle: let navigationStackCoordinator = NavigationStackCoordinator() let itemProvider = SignallingTimelineItemProviderMock(itemProxies: TimelineFixtures.largeChunkProxies, listenForSignals: true) itemProvider.backPaginationResponses = [TimelineFixtures.largeChunkProxies] itemProvider.incomingItems = [TimelineFixtures.incomingMessageProxy] let roomProxy = JoinedRoomProxyMock(.init(name: "Large timeline", avatarURL: .mockMXCAvatar)) let timelineController = makeSignallingTimelineController(itemProvider: itemProvider, roomProxy: roomProxy) let parameters = RoomScreenCoordinatorParameters(userSession: UserSessionMock(.init()), roomProxy: roomProxy, timelineController: timelineController, mediaPlayerProvider: MediaPlayerProviderMock(), emojiProvider: EmojiProvider(appSettings: appSettings), linkMetadataProvider: LinkMetadataProvider(), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock(.init()), appSettings: appSettings, appHooks: AppHooks(), analytics: analytics, composerDraftService: ComposerDraftServiceMock(.init()), timelineControllerFactory: TimelineControllerFactoryMock(.init()), userIndicatorController: UserIndicatorControllerMock()) let coordinator = RoomScreenCoordinator(parameters: parameters) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator case .roomLayoutBottom: let navigationStackCoordinator = NavigationStackCoordinator() let itemProvider = SignallingTimelineItemProviderMock(itemProxies: TimelineFixtures.largeChunkProxies, listenForSignals: true) itemProvider.incomingItems = [TimelineFixtures.incomingMessageProxy] let roomProxy = JoinedRoomProxyMock(.init(name: "Large timeline", avatarURL: .mockMXCAvatar)) let timelineController = makeSignallingTimelineController(itemProvider: itemProvider, roomProxy: roomProxy) let parameters = RoomScreenCoordinatorParameters(userSession: UserSessionMock(.init()), roomProxy: roomProxy, timelineController: timelineController, mediaPlayerProvider: MediaPlayerProviderMock(), emojiProvider: EmojiProvider(appSettings: appSettings), linkMetadataProvider: LinkMetadataProvider(), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock(.init()), appSettings: appSettings, appHooks: AppHooks(), analytics: analytics, composerDraftService: ComposerDraftServiceMock(.init()), timelineControllerFactory: TimelineControllerFactoryMock(.init()), userIndicatorController: UserIndicatorControllerMock()) let coordinator = RoomScreenCoordinator(parameters: parameters) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator case .roomLayoutHighlight: let navigationStackCoordinator = NavigationStackCoordinator() let timelineController = TimelineControllerMock(.init(timelineItems: TimelineFixtures.permalinkChunk)) let parameters = RoomScreenCoordinatorParameters(userSession: UserSessionMock(.init()), roomProxy: JoinedRoomProxyMock(.init(name: "Timeline highlight", avatarURL: .mockMXCAvatar)), timelineController: timelineController, mediaPlayerProvider: MediaPlayerProviderMock(), emojiProvider: EmojiProvider(appSettings: appSettings), linkMetadataProvider: LinkMetadataProvider(), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock(.init()), appSettings: appSettings, appHooks: AppHooks(), analytics: analytics, composerDraftService: ComposerDraftServiceMock(.init()), timelineControllerFactory: TimelineControllerFactoryMock(.init()), userIndicatorController: UserIndicatorControllerMock()) let coordinator = RoomScreenCoordinator(parameters: parameters) navigationStackCoordinator.setRootCoordinator(coordinator) do { let client = try UITestsSignalling.Client(mode: .app) client.signals.sink { [weak self] signal in guard case .timeline(.focusOnEvent(let eventID)) = signal else { return } coordinator.focusOnEvent(.init(eventID: eventID, shouldSetPin: false)) try? client.send(.success) } .store(in: &cancellables) } catch { fatalError("Failure setting up signalling: \(error)") } self.client = client return navigationStackCoordinator case .roomWithDisclosedPolls: let navigationStackCoordinator = NavigationStackCoordinator() let timelineController = TimelineControllerMock(.init(timelineItems: TimelineFixtures.disclosedPolls)) let parameters = RoomScreenCoordinatorParameters(userSession: UserSessionMock(.init()), roomProxy: JoinedRoomProxyMock(.init(name: "Polls timeline", avatarURL: .mockMXCAvatar)), timelineController: timelineController, mediaPlayerProvider: MediaPlayerProviderMock(), emojiProvider: EmojiProvider(appSettings: appSettings), linkMetadataProvider: LinkMetadataProvider(), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock(.init()), appSettings: appSettings, appHooks: AppHooks(), analytics: analytics, composerDraftService: ComposerDraftServiceMock(.init()), timelineControllerFactory: TimelineControllerFactoryMock(.init()), userIndicatorController: UserIndicatorControllerMock()) let coordinator = RoomScreenCoordinator(parameters: parameters) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator case .roomWithUndisclosedPolls: let navigationStackCoordinator = NavigationStackCoordinator() let timelineController = TimelineControllerMock(.init(timelineItems: TimelineFixtures.undisclosedPolls)) let parameters = RoomScreenCoordinatorParameters(userSession: UserSessionMock(.init()), roomProxy: JoinedRoomProxyMock(.init(name: "Polls timeline", avatarURL: .mockMXCAvatar)), timelineController: timelineController, mediaPlayerProvider: MediaPlayerProviderMock(), emojiProvider: EmojiProvider(appSettings: appSettings), linkMetadataProvider: LinkMetadataProvider(), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock(.init()), appSettings: appSettings, appHooks: AppHooks(), analytics: analytics, composerDraftService: ComposerDraftServiceMock(.init()), timelineControllerFactory: TimelineControllerFactoryMock(.init()), userIndicatorController: UserIndicatorControllerMock()) let coordinator = RoomScreenCoordinator(parameters: parameters) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator case .roomWithOutgoingPolls: let navigationStackCoordinator = NavigationStackCoordinator() let timelineController = TimelineControllerMock(.init(timelineItems: TimelineFixtures.outgoingPolls)) let parameters = RoomScreenCoordinatorParameters(userSession: UserSessionMock(.init()), roomProxy: JoinedRoomProxyMock(.init(name: "Polls timeline", avatarURL: .mockMXCAvatar)), timelineController: timelineController, mediaPlayerProvider: MediaPlayerProviderMock(), emojiProvider: EmojiProvider(appSettings: appSettings), linkMetadataProvider: LinkMetadataProvider(), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock(.init()), appSettings: appSettings, appHooks: AppHooks(), analytics: analytics, composerDraftService: ComposerDraftServiceMock(.init()), timelineControllerFactory: TimelineControllerFactoryMock(.init()), userIndicatorController: UserIndicatorControllerMock()) let coordinator = RoomScreenCoordinator(parameters: parameters) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator case .sessionVerification: var sessionVerificationControllerProxy = SessionVerificationControllerProxyMock.configureMock(otherDeviceStartsSasVerification: true, requestDelay: .seconds(5)) let parameters = SessionVerificationScreenCoordinatorParameters(sessionVerificationControllerProxy: sessionVerificationControllerProxy, flow: .deviceInitiator, appSettings: appSettings, mediaProvider: MediaProviderMock(.init())) return SessionVerificationScreenCoordinator(parameters: parameters) case .userSessionScreen, .userSessionScreenReply, .userSessionSpacesFlow: let appSettings: AppSettings = appSettings appSettings.hasRunIdentityConfirmationOnboarding = true appSettings.hasRunNotificationPermissionsOnboarding = true appSettings.analyticsConsentState = .optedOut let roomSummaries: [RoomSummary] = if id == .userSessionSpacesFlow { [[RoomSummary].mockSpaceInvites[0]] + .mockRooms } else { .mockRooms } let clientProxy = ClientProxyMock(.init(userID: "@mock:client.com", deviceID: "MOCKCLIENT", roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(roomSummaries))), spaceServiceConfiguration: .init(topLevelSpaces: .mockSpaceList.filter(\.isSpace) + .mockSingleRoom), roomPreviews: [SpaceServiceRoom].mockSpaceList.map { RoomPreviewProxyMock(spaceServiceRoom: $0) }, defaultRoomMembers: .allMembersAsAdmin)) clientProxy.recentlyVisitedRoomsFilterReturnValue = .mockRooms // The tab bar remains hidden for the non-spaces tests as we don't supply any mock spaces. let spaceServiceProxy = SpaceServiceProxyMock(id == .userSessionSpacesFlow ? .populated : .init()) clientProxy.spaceService = spaceServiceProxy let appMediator = AppMediatorMock(.init()) appMediator.windowManager = windowManager let itemProvider = SignallingTimelineItemProviderMock(itemProxies: TimelineFixtures.largeChunkProxies) let timelineController = makeSignallingTimelineController(itemProvider: itemProvider, roomProxy: JoinedRoomProxyMock(.init())) let timelineControllerFactory = TimelineControllerFactoryMock(.init(timelineController: timelineController)) let flowCoordinator = UserSessionFlowCoordinator(isNewLogin: false, navigationRootCoordinator: navigationRootCoordinator, appLockService: AppLockService(keychainController: KeychainControllerMock(), appSettings: appSettings), flowParameters: CommonFlowParameters(userSession: UserSessionMock(.init(clientProxy: clientProxy)), bugReportService: BugReportServiceMock(.init()), elementCallService: ElementCallServiceMock(.init()), timelineControllerFactory: timelineControllerFactory, emojiProvider: EmojiProvider(appSettings: appSettings), linkMetadataProvider: LinkMetadataProvider(), appMediator: appMediator, appSettings: appSettings, appHooks: AppHooks(), analytics: analytics, userIndicatorController: UserIndicatorControllerMock(), notificationManager: NotificationManagerMock(), stateMachineFactory: StateMachineFactory())) flowCoordinator.start() retainedState.append(flowCoordinator) return nil case .roomMembersListScreenPendingInvites: let navigationStackCoordinator = NavigationStackCoordinator() let members: [RoomMemberProxyMock] = [.mockInvitedAlice, .mockBob, .mockCharlie] let coordinator = RoomMembersListScreenCoordinator(parameters: .init(userSession: UserSessionMock(.init()), roomProxy: JoinedRoomProxyMock(.init(name: "test", members: members)), userIndicatorController: userIndicatorController, analytics: analytics)) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator case .roomRolesAndPermissionsFlow: let navigationStackCoordinator = NavigationStackCoordinator() navigationStackCoordinator.setRootCoordinator(BlankFormCoordinator()) let coordinator = RoomRolesAndPermissionsFlowCoordinator(parameters: .init(roomProxy: JoinedRoomProxyMock(.init(members: .allMembersAsAdmin)), mediaProvider: MediaProviderMock(.init()), navigationStackCoordinator: navigationStackCoordinator, userIndicatorController: userIndicatorController, analytics: analytics)) retainedState.append(coordinator) coordinator.start() return navigationStackCoordinator case .startChatFlow: let clientProxy = ClientProxyMock(.init(userID: "@mock:client.com")) clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReturnValue = .success("!new-room:client.com") clientProxy.roomForIdentifierClosure = { roomID in .joined(JoinedRoomProxyMock(.init(id: roomID, members: []))) } let userDiscoveryService = UserDiscoveryServiceMock() userDiscoveryService.searchProfilesWithReturnValue = .success([.mockBob, .mockBobby]) let navigationStackCoordinator = NavigationStackCoordinator() let flowCoordinator = StartChatFlowCoordinator(entryPoint: .startChat, userDiscoveryService: userDiscoveryService, navigationStackCoordinator: navigationStackCoordinator, flowParameters: CommonFlowParameters(userSession: UserSessionMock(.init(clientProxy: clientProxy)), bugReportService: BugReportServiceMock(.init()), elementCallService: ElementCallServiceMock(.init()), timelineControllerFactory: TimelineControllerFactoryMock(.init()), emojiProvider: EmojiProvider(appSettings: appSettings), linkMetadataProvider: LinkMetadataProvider(), appMediator: AppMediatorMock(.init()), appSettings: appSettings, appHooks: AppHooks(), analytics: analytics, userIndicatorController: UserIndicatorControllerMock(), notificationManager: NotificationManagerMock(), stateMachineFactory: StateMachineFactory())) flowCoordinator.actionsPublisher .sink { [weak self] action in guard let self else { return } switch action { case .finished(let roomID): navigationRootCoordinator.setSheetCoordinator(nil) case .showRoomDirectory: break // The test doesn't cover this. } } .store(in: &cancellables) retainedState.append(flowCoordinator) flowCoordinator.start() // Use a sheet on top the the placeholder so we can test the dismissal. navigationRootCoordinator.setSheetCoordinator(navigationStackCoordinator) return PlaceholderScreenCoordinator(hideBrandChrome: false) case .createPoll: let navigationStackCoordinator = NavigationStackCoordinator() let coordinator = PollFormScreenCoordinator(parameters: .init(mode: .new(topic: nil), maxNumberOfOptions: 10, timelineController: TimelineControllerMock(.init()), analytics: analytics, userIndicatorController: UserIndicatorControllerMock())) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator case .encryptionSettings, .encryptionSettingsOutOfSync: let recoveryState: SecureBackupRecoveryState = id == .encryptionSettings ? .enabled : .incomplete let clientProxy = ClientProxyMock(.init(userID: "@mock:client.com", recoveryState: recoveryState)) let userSession = UserSessionMock(.init(clientProxy: clientProxy)) let navigationStackCoordinator = NavigationStackCoordinator() navigationStackCoordinator.setRootCoordinator(BlankFormCoordinator()) let coordinator = EncryptionSettingsFlowCoordinator(parameters: .init(userSession: userSession, appSettings: appSettings, appHooks: AppHooks(), userIndicatorController: UserIndicatorControllerMock(), navigationStackCoordinator: navigationStackCoordinator)) retainedState.append(coordinator) coordinator.start() return navigationStackCoordinator case .encryptionReset: let recoveryState: SecureBackupRecoveryState = id == .encryptionSettings ? .enabled : .incomplete let clientProxy = ClientProxyMock(.init(userID: "@mock:client.com", recoveryState: recoveryState)) let userSession = UserSessionMock(.init(clientProxy: clientProxy)) let userIndicatorController = UserIndicatorController() userIndicatorController.window = windowManager.overlayWindow let navigationStackCoordinator = NavigationStackCoordinator() let coordinator = EncryptionResetFlowCoordinator(parameters: .init(userSession: userSession, appMediator: AppMediatorMock(.init()), appSettings: appSettings, appHooks: AppHooks(), userIndicatorController: userIndicatorController, navigationStackCoordinator: navigationStackCoordinator, windowManger: windowManager)) retainedState.append(coordinator) coordinator.start() return navigationStackCoordinator case .linkNewDevice, .linkNewDeviceWithAppLockPIN: let linkMobileProgressSubject: CurrentValueSubject = .init(.qrReady(LinkNewDeviceServiceMock.mockQRCodeImage)) let linkNewDeviceService = LinkNewDeviceServiceMock(.init(linkMobileProgressPublisher: linkMobileProgressSubject.asCurrentValuePublisher())) let clientProxy = ClientProxyMock(.init()) clientProxy.linkNewDeviceServiceReturnValue = linkNewDeviceService let keychainController = KeychainController(service: .tests, accessGroup: InfoPlistReader.main.keychainAccessGroupIdentifier) keychainController.resetSecrets() let context = LAContextMock() context.canEvaluatePolicyReturnValue = false // No device passcode in the simulator. let appLockService = AppLockService(keychainController: keychainController, appSettings: appSettings, context: context) // Set an App Lock PIN for the dedicated variant so linking requires it (and forced logout can be tested). // Without a PIN the service is disabled, so verification is unavailable and linking proceeds. if id == .linkNewDeviceWithAppLockPIN, case .failure = appLockService.setupPINCode("2023") { fatalError("Failed to preset the PIN code.") } let navigationStackCoordinator = NavigationStackCoordinator() let flowCoordinator = LinkNewDeviceFlowCoordinator(navigationStackCoordinator: navigationStackCoordinator, appLockService: appLockService, flowParameters: CommonFlowParameters(userSession: UserSessionMock(.init(clientProxy: clientProxy)), bugReportService: BugReportServiceMock(.init()), elementCallService: ElementCallServiceMock(.init()), timelineControllerFactory: TimelineControllerFactoryMock(.init()), emojiProvider: EmojiProvider(appSettings: appSettings), linkMetadataProvider: LinkMetadataProvider(), appMediator: AppMediatorMock(.init()), appSettings: appSettings, appHooks: AppHooks(), analytics: analytics, userIndicatorController: UserIndicatorControllerMock(), notificationManager: NotificationManagerMock(), stateMachineFactory: StateMachineFactory())) flowCoordinator.actionsPublisher .sink { [weak self] action in guard let self else { return } switch action { case .dismiss: navigationRootCoordinator.setSheetCoordinator(nil) case .requestOAuthAuthorisation, .forceLogout: break } } .store(in: &cancellables) retainedState.append(flowCoordinator) flowCoordinator.start() // Use a sheet on top the the placeholder so we can test the dismissal. navigationRootCoordinator.setSheetCoordinator(navigationStackCoordinator) return PlaceholderScreenCoordinator(hideBrandChrome: false) case .autoUpdatingTimeline: let appSettings: AppSettings = appSettings appSettings.hasRunIdentityConfirmationOnboarding = true appSettings.hasRunNotificationPermissionsOnboarding = true appSettings.analyticsConsentState = .optedOut let navigationSplitCoordinator = NavigationSplitCoordinator(placeholderCoordinator: PlaceholderScreenCoordinator(hideBrandChrome: false)) navigationRootCoordinator.setRootCoordinator(navigationSplitCoordinator) let clientProxy = ClientProxyMock(.init(userID: "@mock:client.com", roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms))))) let roomProxy = JoinedRoomProxyMock(.init(id: "whatever", name: "okay", shouldUseAutoUpdatingTimeline: true)) clientProxy.roomForIdentifierReturnValue = .joined(roomProxy) let timelineController = TimelineController(roomProxy: roomProxy, timelineProxy: roomProxy.timeline, initialFocussedEventID: nil, timelineItemFactory: RoomTimelineItemFactory(userID: "@alice:matrix.org", attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), stateEventStringBuilder: RoomStateEventStringBuilder(userID: "@alice:matrix.org")), mediaProvider: MediaProviderMock(.init()), appSettings: appSettings) let flowCoordinator = ChatsTabFlowCoordinator(isNewLogin: false, navigationSplitCoordinator: navigationSplitCoordinator, flowParameters: CommonFlowParameters(userSession: UserSessionMock(.init(clientProxy: clientProxy)), bugReportService: BugReportServiceMock(.init()), elementCallService: ElementCallServiceMock(.init()), timelineControllerFactory: TimelineControllerFactoryMock(.init(timelineController: timelineController)), emojiProvider: EmojiProvider(appSettings: appSettings), linkMetadataProvider: LinkMetadataProvider(), appMediator: AppMediatorMock(.init()), appSettings: appSettings, appHooks: AppHooks(), analytics: analytics, userIndicatorController: UserIndicatorControllerMock(), notificationManager: NotificationManagerMock(), stateMachineFactory: StateMachineFactory())) flowCoordinator.start() retainedState.append(flowCoordinator) return nil } }() // MARK: - Private private func makeSignallingTimelineController(itemProvider: SignallingTimelineItemProviderMock, roomProxy: JoinedRoomProxyMock) -> TimelineController { let timelineProxy = TimelineProxyMock(.init(timelineItemProvider: itemProvider)) var pendingEditText: String? timelineProxy.sendMessageHtmlInReplyToEventIDIntentionalMentionsClosure = { [weak itemProvider] message, _, _, _ in itemProvider?.inject([.mockOwnMessage(message)]) return .success(()) } timelineProxy.buildMessageContentForHtmlIntentionalMentionsClosure = { text, _, _ in pendingEditText = text return RoomMessageEventContentWithoutRelationSDKMock() } timelineProxy.editNewContentClosure = { [weak itemProvider] _, _ in if let text = pendingEditText { itemProvider?.replaceLastItem(with: .mockOwnMessage(text, isEdited: true)) pendingEditText = nil } return .success(()) } return TimelineController(roomProxy: roomProxy, timelineProxy: timelineProxy, initialFocussedEventID: nil, timelineItemFactory: RoomTimelineItemFactory(userID: "@alice:matrix.org", attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), stateEventStringBuilder: RoomStateEventStringBuilder(userID: "@alice:matrix.org")), mediaProvider: MediaProviderMock(.init()), appSettings: appSettings) } }