diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 41890c3f1..8ea327191 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -437,6 +437,7 @@ 4715FE33667C5899E64DD0E6 /* ResolveVerifiedUserSendFailureScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97287090CA64DAA95386ECED /* ResolveVerifiedUserSendFailureScreen.swift */; }; 4716587A9BA69ED8FD1B986B /* PollOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6B19D10B102956066AF117B /* PollOptionView.swift */; }; 47305C0911C9E1AA774A4000 /* TemplateScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA90BD288E5AE6BC643AFDDF /* TemplateScreenCoordinator.swift */; }; + 473D6362E1D1A714E07B1B0B /* RoomSDKMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27A6C38B29A60E187D6A4B0A /* RoomSDKMock.swift */; }; 4764FC9A843D1F9865EDC29C /* EditRoomAddressScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4048547AC50ADCF201684E87 /* EditRoomAddressScreen.swift */; }; 478C363F63A5DFC3C83E334C /* MediaProviderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F65E4BB9E82EB8373207CF8 /* MediaProviderMock.swift */; }; 4807E8F51DB54F56B25E1C7E /* AppLockSetupSettingsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D8C38663020DF2EB2D13F5E /* AppLockSetupSettingsScreenViewModel.swift */; }; @@ -1980,6 +1981,7 @@ 276833816F5DEB1CE3B8BE1E /* ReportRoomScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportRoomScreen.swift; sourceTree = ""; }; 27A1AD6389A4659AF0CEAE62 /* NotificationServiceExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationServiceExtension.swift; sourceTree = ""; }; 27A5FBB74981BF8EFFDAE90D /* HTMLFixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTMLFixtures.swift; sourceTree = ""; }; + 27A6C38B29A60E187D6A4B0A /* RoomSDKMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSDKMock.swift; sourceTree = ""; }; 27A9E3FBE8A66B5A17AD7F74 /* AppRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRoutes.swift; sourceTree = ""; }; 27D0EA07BD545CC9F234DB8D /* UserDetailsEditScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDetailsEditScreenModels.swift; sourceTree = ""; }; 27DF257F5D968E5DD719583C /* BugReportTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportTests.swift; sourceTree = ""; }; @@ -7051,6 +7053,7 @@ 580BDCD23DD02481AB5FFB47 /* LeaveSpaceHandleSDKMock.swift */, C2B8AA3199BAAD066E36CB07 /* RoomListSDKMock.swift */, E3454C79D9E2444BBDBF9F5A /* RoomListServiceSDKMock.swift */, + 27A6C38B29A60E187D6A4B0A /* RoomSDKMock.swift */, 0EF5721148A5D59B476BA976 /* SearchServiceSDKMock.swift */, 90B1E4D1F0A6FAAE64B54D76 /* SpaceServiceSDKMock.swift */, F70373EF23A73181D2568D79 /* SyncServiceBuilderSDKMock.swift */, @@ -9253,6 +9256,7 @@ 8358D145F9BF94F412BEDCA8 /* RoomRolesAndPermissionsScreenModels.swift in Sources */, 69B3C6010B42010F591FC3CB /* RoomRolesAndPermissionsScreenViewModel.swift in Sources */, F8F47CE757EE656905F01F2C /* RoomRolesAndPermissionsScreenViewModelProtocol.swift in Sources */, + 473D6362E1D1A714E07B1B0B /* RoomSDKMock.swift in Sources */, C55A44C99F64A479ABA85B46 /* RoomScreen.swift in Sources */, A851635B3255C6DC07034A12 /* RoomScreenCoordinator.swift in Sources */, E8C65C19F7C40EE545172DD6 /* RoomScreenFooterView.swift in Sources */, diff --git a/ElementX/Sources/Mocks/RoomSummaryProviderMock.swift b/ElementX/Sources/Mocks/RoomSummaryProviderMock.swift index 02d4ae95e..4791555bc 100644 --- a/ElementX/Sources/Mocks/RoomSummaryProviderMock.swift +++ b/ElementX/Sources/Mocks/RoomSummaryProviderMock.swift @@ -73,7 +73,7 @@ extension RoomSummary { static func mock(id: String, name: String, canonicalAlias: String? = nil) -> RoomSummary { - RoomSummary(room: RoomSDKMock(), + RoomSummary(room: RoomSDKMock(.init()), id: id, joinRequestType: nil, name: name, diff --git a/ElementX/Sources/Mocks/SDK/RoomSDKMock.swift b/ElementX/Sources/Mocks/SDK/RoomSDKMock.swift new file mode 100644 index 000000000..389c3b9e1 --- /dev/null +++ b/ElementX/Sources/Mocks/SDK/RoomSDKMock.swift @@ -0,0 +1,22 @@ +// +// Copyright 2026 Element Creations Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. +// Please see LICENSE files in the repository root for full details. +// + +import Foundation +import MatrixRustSDK +import MatrixRustSDKMocks + +nonisolated extension RoomSDKMock { + struct Configuration { + var membership: Membership = .joined + } + + convenience init(_ configuration: Configuration) { + self.init() + + membershipReturnValue = configuration.membership + } +} diff --git a/UnitTests/Sources/JoinRoomScreenViewModelTests.swift b/UnitTests/Sources/JoinRoomScreenViewModelTests.swift index 372399f66..63210178c 100644 --- a/UnitTests/Sources/JoinRoomScreenViewModelTests.swift +++ b/UnitTests/Sources/JoinRoomScreenViewModelTests.swift @@ -6,6 +6,7 @@ // Please see LICENSE files in the repository root for full details. // +import Combine @testable import ElementX import Testing @@ -16,12 +17,16 @@ final class JoinRoomScreenViewModelTests { case knocked case invited case banned + case alreadyJoined } var viewModel: JoinRoomScreenViewModelProtocol! var clientProxy: ClientProxyMock! + /// Backs the client's static room summary provider so tests can push membership changes. + var roomListSubject: CurrentValueSubject<[RoomSummary], Never>! + var context: JoinRoomScreenViewModelType.Context { viewModel.context } @@ -35,6 +40,7 @@ final class JoinRoomScreenViewModelTests { isolated deinit { viewModel = nil clientProxy = nil + roomListSubject = nil } @Test @@ -114,6 +120,26 @@ final class JoinRoomScreenViewModelTests { try await deferred.fulfill() } + @Test + func alreadyJoinedRoomAdvancesTheScreen() async throws { + setupViewModel(mode: .alreadyJoined) + + let deferred = deferFulfillment(viewModel.actionsPublisher) { $0 == .joined(.roomID("1")) } + try await deferred.fulfill() + } + + @Test + func membershipChangeWhileInvitedAdvancesTheScreen() async throws { + setupViewModel(mode: .invited) + try await deferFulfillment(viewModel.context.$viewState) { $0.mode == .invited(isDM: false) }.fulfill() + + // Simulate the server-side join arriving through the room list. + clientProxy.roomForIdentifierClosure = { .joined(JoinedRoomProxyMock(.init(id: $0))) } + let deferred = deferFulfillment(viewModel.actionsPublisher) { $0 == .joined(.roomID("1")) } + roomListSubject.send([.mock(id: "1", name: "Test")]) + try await deferred.fulfill() + } + @Test func declineAndBlockInviteLegacyInteraction() async throws { setupViewModel(mode: .invited) @@ -169,6 +195,11 @@ final class JoinRoomScreenViewModelTests { private func setupViewModel(throwing: Bool = false, mode: TestMode = .joined) { clientProxy = ClientProxyMock(.init()) + roomListSubject = .init([]) + let summaryProvider = RoomSummaryProviderMock(.init()) + summaryProvider.roomListPublisher = roomListSubject.asCurrentValuePublisher() + clientProxy.staticRoomSummaryProvider = summaryProvider + clientProxy.joinRoomViaReturnValue = throwing ? .failure(.sdkError(ClientProxyMockError.generic)) : .success(()) clientProxy.joinRoomAliasReturnValue = clientProxy.joinRoomViaReturnValue @@ -198,6 +229,11 @@ final class JoinRoomScreenViewModelTests { roomProxy.forgetRoomReturnValue = .success(()) return .banned(roomProxy) } + case .alreadyJoined: + clientProxy.roomPreviewForIdentifierViaReturnValue = .success(RoomPreviewProxyMock.joinable) + clientProxy.roomForIdentifierClosure = { roomID in + .joined(JoinedRoomProxyMock(.init(id: roomID))) + } } viewModel = JoinRoomScreenViewModel(source: .generic(roomID: "1", via: []),