// // Copyright 2025 Element Creations Ltd. // Copyright 2023-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 @testable import ElementX import Testing @MainActor struct CompletionSuggestionServiceTests { @Test func userSuggestions() async throws { let alice: RoomMemberProxyMock = .mockAlice let members: [RoomMemberProxyMock] = [alice, .mockBob, .mockCharlie, .mockMe] let roomProxyMock = JoinedRoomProxyMock(.init(id: "roomID", name: "test", members: members)) let roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms))) let service = CompletionSuggestionService(roomProxy: roomProxyMock, roomListPublisher: roomSummaryProvider.roomListPublisher.eraseToAnyPublisher()) var deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in suggestions == [] } try await deferred.fulfill() deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in suggestions == [.init(suggestionType: .user(.init(id: alice.userID, displayName: alice.displayName, avatarURL: alice.avatarURL)), range: .init(), rawSuggestionText: "ali")] } service.setSuggestionTrigger(.init(type: .user, text: "ali", range: .init())) try await deferred.fulfill() deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in suggestions == [] } service.setSuggestionTrigger(.init(type: .user, text: "me", range: .init())) try await deferred.fulfill() deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in suggestions == [] } service.setSuggestionTrigger(.init(type: .user, text: "room", range: .init())) try await deferred.fulfill() deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in suggestions == [] } service.setSuggestionTrigger(.init(type: .user, text: "everyon", range: .init())) try await deferred.fulfill() } @Test func userSuggestionsIncludingAllUsers() async throws { let alice: RoomMemberProxyMock = .mockAlice let members: [RoomMemberProxyMock] = [alice, .mockBob, .mockCharlie, .mockMe] let roomProxyMock = JoinedRoomProxyMock(.init(id: "roomID", name: "test", members: members, powerLevelsConfiguration: .init(canUserTriggerRoomNotification: true))) let roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms))) let service = CompletionSuggestionService(roomProxy: roomProxyMock, roomListPublisher: roomSummaryProvider.roomListPublisher.eraseToAnyPublisher()) var deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in suggestions == [] } try await deferred.fulfill() deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in suggestions == [.init(suggestionType: .allUsers(.room(id: "roomID", name: "test", avatarURL: nil)), range: .init(), rawSuggestionText: "ro")] } service.setSuggestionTrigger(.init(type: .user, text: "ro", range: .init())) try await deferred.fulfill() deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in suggestions == [.init(suggestionType: .allUsers(.room(id: "roomID", name: "test", avatarURL: nil)), range: .init(), rawSuggestionText: "every")] } service.setSuggestionTrigger(.init(type: .user, text: "every", range: .init())) try await deferred.fulfill() } @Test func userSuggestionsWithEmptyText() async throws { let alice: RoomMemberProxyMock = .mockAlice let bob: RoomMemberProxyMock = .mockBob let members: [RoomMemberProxyMock] = [alice, bob, .mockMe] let roomProxyMock = JoinedRoomProxyMock(.init(id: "roomID", name: "test", members: members, powerLevelsConfiguration: .init(canUserTriggerRoomNotification: true))) let roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms))) let service = CompletionSuggestionService(roomProxy: roomProxyMock, roomListPublisher: roomSummaryProvider.roomListPublisher.eraseToAnyPublisher()) var deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in suggestions == [] } try await deferred.fulfill() deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in suggestions == [.init(suggestionType: .allUsers(.room(id: "roomID", name: "test", avatarURL: nil)), range: .init(), rawSuggestionText: ""), .init(suggestionType: .user(.init(id: alice.userID, displayName: alice.displayName, avatarURL: alice.avatarURL)), range: .init(), rawSuggestionText: ""), .init(suggestionType: .user(.init(id: bob.userID, displayName: bob.displayName, avatarURL: bob.avatarURL)), range: .init(), rawSuggestionText: "")] } service.setSuggestionTrigger(.init(type: .user, text: "", range: .init())) try await deferred.fulfill() // Let's test the same with the processTextMessage method deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in suggestions == [.init(suggestionType: .allUsers(.room(id: "roomID", name: "test", avatarURL: nil)), range: .init(location: 0, length: 1), rawSuggestionText: ""), .init(suggestionType: .user(.init(id: alice.userID, displayName: alice.displayName, avatarURL: alice.avatarURL)), range: .init(location: 0, length: 1), rawSuggestionText: ""), .init(suggestionType: .user(.init(id: bob.userID, displayName: bob.displayName, avatarURL: bob.avatarURL)), range: .init(location: 0, length: 1), rawSuggestionText: "")] } service.processTextMessage("@", selectedRange: .init(location: 0, length: 1)) try await deferred.fulfill() } @Test func userSuggestionInDifferentMessagePositions() async throws { let alice: RoomMemberProxyMock = .mockAlice let members: [RoomMemberProxyMock] = [alice, .mockBob, .mockCharlie, .mockMe] let roomProxyMock = JoinedRoomProxyMock(.init(name: "test", members: members)) let roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms))) let service = CompletionSuggestionService(roomProxy: roomProxyMock, roomListPublisher: roomSummaryProvider.roomListPublisher.eraseToAnyPublisher()) var deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in suggestions == [.init(suggestionType: .user(.init(id: alice.userID, displayName: alice.displayName, avatarURL: alice.avatarURL)), range: .init(location: 0, length: 3), rawSuggestionText: "al")] } service.processTextMessage("@al hello", selectedRange: .init(location: 0, length: 1)) try await deferred.fulfill() deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in suggestions == [.init(suggestionType: .user(.init(id: alice.userID, displayName: alice.displayName, avatarURL: alice.avatarURL)), range: .init(location: 5, length: 3), rawSuggestionText: "al")] } service.processTextMessage("test @al", selectedRange: .init(location: 5, length: 1)) try await deferred.fulfill() deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in suggestions == [.init(suggestionType: .user(.init(id: alice.userID, displayName: alice.displayName, avatarURL: alice.avatarURL)), range: .init(location: 5, length: 3), rawSuggestionText: "al")] } service.processTextMessage("test @al test", selectedRange: .init(location: 5, length: 1)) try await deferred.fulfill() } @Test func userSuggestionWithMultipleMentionSymbol() async throws { let alice: RoomMemberProxyMock = .mockAlice let bob: RoomMemberProxyMock = .mockBob let members: [RoomMemberProxyMock] = [alice, bob, .mockCharlie, .mockMe] let roomProxyMock = JoinedRoomProxyMock(.init(name: "test", members: members)) let roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms))) let service = CompletionSuggestionService(roomProxy: roomProxyMock, roomListPublisher: roomSummaryProvider.roomListPublisher.eraseToAnyPublisher()) var deffered = deferFulfillment(service.suggestionsPublisher) { suggestions in suggestions == [.init(suggestionType: .user(.init(id: alice.userID, displayName: alice.displayName, avatarURL: alice.avatarURL)), range: .init(location: 0, length: 3), rawSuggestionText: "al")] } service.processTextMessage("@al test @bo", selectedRange: .init(location: 0, length: 1)) try await deffered.fulfill() deffered = deferFulfillment(service.suggestionsPublisher) { suggestions in suggestions == [.init(suggestionType: .user(.init(id: bob.userID, displayName: bob.displayName, avatarURL: bob.avatarURL)), range: .init(location: 9, length: 3), rawSuggestionText: "bo")] } service.processTextMessage("@al test @bo", selectedRange: .init(location: 9, length: 1)) try await deffered.fulfill() deffered = deferFulfillment(service.suggestionsPublisher) { suggestion in suggestion == [] } service.processTextMessage("@al test @bo", selectedRange: .init(location: 4, length: 1)) try await deffered.fulfill() } @Test func roomSuggestions() async throws { let alice: RoomMemberProxyMock = .mockAlice // We keep the users in the tests since they should not appear in the suggestions when using the room trigger let members: [RoomMemberProxyMock] = [alice, .mockBob, .mockCharlie, .mockMe] let roomProxyMock = JoinedRoomProxyMock(.init(id: "roomID", name: "test", members: members)) let roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms))) let service = CompletionSuggestionService(roomProxy: roomProxyMock, roomListPublisher: roomSummaryProvider.roomListPublisher.eraseToAnyPublisher()) var deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in suggestions == [] } try await deferred.fulfill() // The empty # should trigger suggestions from any room with an alias deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in suggestions == [.init(suggestionType: .room(.init(id: "2", canonicalAlias: "#foundation-and-empire:matrix.org", name: "Foundation and Empire", avatar: .room(id: "2", name: "Foundation and Empire", avatarURL: .mockMXCAvatar))), range: .init(), rawSuggestionText: ""), .init(suggestionType: .room(.init(id: "6", canonicalAlias: "#prelude-foundation:matrix.org", name: "Prelude to Foundation", avatar: .room(id: "6", name: "Prelude to Foundation", avatarURL: nil))), range: .init(), rawSuggestionText: "")] } service.setSuggestionTrigger(.init(type: .room, text: "", range: .init())) try await deferred.fulfill() // Same but with the processTextMessage method deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in suggestions == [.init(suggestionType: .room(.init(id: "2", canonicalAlias: "#foundation-and-empire:matrix.org", name: "Foundation and Empire", avatar: .room(id: "2", name: "Foundation and Empire", avatarURL: .mockMXCAvatar))), range: .init(location: 0, length: 1), rawSuggestionText: ""), .init(suggestionType: .room(.init(id: "6", canonicalAlias: "#prelude-foundation:matrix.org", name: "Prelude to Foundation", avatar: .room(id: "6", name: "Prelude to Foundation", avatarURL: nil))), range: .init(location: 0, length: 1), rawSuggestionText: "")] } service.processTextMessage("#", selectedRange: .init(location: 0, length: 1)) try await deferred.fulfill() deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in suggestions == [.init(suggestionType: .room(.init(id: "6", canonicalAlias: "#prelude-foundation:matrix.org", name: "Prelude to Foundation", avatar: .room(id: "6", name: "Prelude to Foundation", avatarURL: nil))), range: .init(), rawSuggestionText: "prelude")] } service.setSuggestionTrigger(.init(type: .room, text: "prelude", range: .init())) try await deferred.fulfill() } @Test func roomSuggestionInDifferentMessagePositions() async throws { let alice: RoomMemberProxyMock = .mockAlice // We keep the users in the tests since they should not appear in the suggestions when using the room trigger let members: [RoomMemberProxyMock] = [alice, .mockBob, .mockCharlie, .mockMe] let roomProxyMock = JoinedRoomProxyMock(.init(id: "roomID", name: "test", members: members)) let roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms))) let service = CompletionSuggestionService(roomProxy: roomProxyMock, roomListPublisher: roomSummaryProvider.roomListPublisher.eraseToAnyPublisher()) var deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in suggestions == [.init(suggestionType: .room(.init(id: "6", canonicalAlias: "#prelude-foundation:matrix.org", name: "Prelude to Foundation", avatar: .room(id: "6", name: "Prelude to Foundation", avatarURL: nil))), range: .init(location: 0, length: 3), rawSuggestionText: "pr")] } service.processTextMessage("#pr hello", selectedRange: .init(location: 0, length: 1)) try await deferred.fulfill() deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in suggestions == [.init(suggestionType: .room(.init(id: "6", canonicalAlias: "#prelude-foundation:matrix.org", name: "Prelude to Foundation", avatar: .room(id: "6", name: "Prelude to Foundation", avatarURL: nil))), range: .init(location: 5, length: 3), rawSuggestionText: "pr")] } service.processTextMessage("test #pr", selectedRange: .init(location: 5, length: 1)) try await deferred.fulfill() deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in suggestions == [.init(suggestionType: .room(.init(id: "6", canonicalAlias: "#prelude-foundation:matrix.org", name: "Prelude to Foundation", avatar: .room(id: "6", name: "Prelude to Foundation", avatarURL: nil))), range: .init(location: 5, length: 3), rawSuggestionText: "pr")] } service.processTextMessage("test #pr test", selectedRange: .init(location: 5, length: 1)) try await deferred.fulfill() } @Test func roomSuggestionWithMultipleMentionSymbol() async throws { let alice: RoomMemberProxyMock = .mockAlice // We keep the users in the tests since they should not appear in the suggestions when using the room trigger let members: [RoomMemberProxyMock] = [alice, .mockBob, .mockCharlie, .mockMe] let roomProxyMock = JoinedRoomProxyMock(.init(id: "roomID", name: "test", members: members)) let roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms))) let service = CompletionSuggestionService(roomProxy: roomProxyMock, roomListPublisher: roomSummaryProvider.roomListPublisher.eraseToAnyPublisher()) var deffered = deferFulfillment(service.suggestionsPublisher) { suggestions in suggestions == [.init(suggestionType: .room(.init(id: "6", canonicalAlias: "#prelude-foundation:matrix.org", name: "Prelude to Foundation", avatar: .room(id: "6", name: "Prelude to Foundation", avatarURL: nil))), range: .init(location: 0, length: 3), rawSuggestionText: "pr")] } service.processTextMessage("#pr test #fo", selectedRange: .init(location: 0, length: 1)) try await deffered.fulfill() deffered = deferFulfillment(service.suggestionsPublisher) { suggestions in suggestions == [.init(suggestionType: .room(.init(id: "2", canonicalAlias: "#foundation-and-empire:matrix.org", name: "Foundation and Empire", avatar: .room(id: "2", name: "Foundation and Empire", avatarURL: .mockMXCAvatar))), range: .init(location: 9, length: 3), rawSuggestionText: "fo"), .init(suggestionType: .room(.init(id: "6", canonicalAlias: "#prelude-foundation:matrix.org", name: "Prelude to Foundation", avatar: .room(id: "6", name: "Prelude to Foundation", avatarURL: nil))), range: .init(location: 9, length: 3), rawSuggestionText: "fo")] } service.processTextMessage("#pr test #fo", selectedRange: .init(location: 9, length: 1)) try await deffered.fulfill() deffered = deferFulfillment(service.suggestionsPublisher) { suggestion in suggestion == [] } service.processTextMessage("#pr test #fo", selectedRange: .init(location: 4, length: 1)) try await deffered.fulfill() } @Test func suggestionsWithMultipleDifferentTriggers() async throws { let alice: RoomMemberProxyMock = .mockAlice // We keep the users in the tests since they should not appear in the suggestions when using the room trigger let members: [RoomMemberProxyMock] = [alice, .mockBob, .mockCharlie, .mockMe] let roomProxyMock = JoinedRoomProxyMock(.init(id: "roomID", name: "test", members: members)) let roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms))) let service = CompletionSuggestionService(roomProxy: roomProxyMock, roomListPublisher: roomSummaryProvider.roomListPublisher.eraseToAnyPublisher()) var deffered = deferFulfillment(service.suggestionsPublisher) { suggestions in suggestions == [.init(suggestionType: .room(.init(id: "6", canonicalAlias: "#prelude-foundation:matrix.org", name: "Prelude to Foundation", avatar: .room(id: "6", name: "Prelude to Foundation", avatarURL: nil))), range: .init(location: 0, length: 3), rawSuggestionText: "pr")] } service.processTextMessage("#pr test @al", selectedRange: .init(location: 0, length: 1)) try await deffered.fulfill() deffered = deferFulfillment(service.suggestionsPublisher) { suggestions in suggestions == [.init(suggestionType: .user(.init(id: alice.userID, displayName: alice.displayName, avatarURL: alice.avatarURL)), range: .init(location: 9, length: 3), rawSuggestionText: "al")] } service.processTextMessage("#pr test @al", selectedRange: .init(location: 9, length: 1)) try await deffered.fulfill() } @Test func suggestionsContainingNonAlphanumericCharacters() async throws { let alice: RoomMemberProxyMock = .mockAlice // We keep the users in the tests since they should not appear in the suggestions when using the room trigger let members: [RoomMemberProxyMock] = [alice, .mockBob, .mockCharlie, .mockMe] let roomProxyMock = JoinedRoomProxyMock(.init(id: "roomID", name: "test", members: members)) let roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms))) let service = CompletionSuggestionService(roomProxy: roomProxyMock, roomListPublisher: roomSummaryProvider.roomListPublisher.eraseToAnyPublisher()) var deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in suggestions == [] } try await deferred.fulfill() deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in suggestions == [.init(suggestionType: .room(.init(id: "6", canonicalAlias: "#prelude-foundation:matrix.org", name: "Prelude to Foundation", avatar: .room(id: "6", name: "Prelude to Foundation", avatarURL: nil))), range: .init(), rawSuggestionText: "#prelude-")] } service.setSuggestionTrigger(.init(type: .room, text: "#prelude-", range: .init())) try await deferred.fulfill() } }