// // Copyright 2022 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 XCTest @testable import Element // MARK: - Inputs private enum Inputs { static let messageStart = "Hello " static let aliceDisplayname = "Alice" static let aliceUserId = "@alice:matrix.org" static let aliceAvatarUrl = "mxc://matrix.org/VyNYAgahaiAzUoOeZETtQ" static let aliceAwayDisplayname = "Alice_away" static let aliceNewAvatarUrl = "mxc://matrix.org/VyNYAgaFdlLojoOeZETtQ" static let aliceMember = FakeMXRoomMember(displayname: aliceDisplayname, avatarUrl: aliceAvatarUrl, userId: aliceUserId) static let aliceMemberAway = FakeMXRoomMember(displayname: aliceAwayDisplayname, avatarUrl: aliceNewAvatarUrl, userId: "@alice:matrix.org") static let alicePermalink = "https://matrix.to/#/@alice:matrix.org" static let mentionToAlice = NSAttributedString(string: aliceDisplayname, attributes: [.link: URL(string: alicePermalink)!]) static let markdownLinkToAlice = "[\(aliceDisplayname)](\(alicePermalink))" static let bobUserId = "@bob:matrix.org" static let bobDisplayname = "Bob" static let bobAvatarUrl = "mxc://matrix.org/VyNYBgahazAzUuOeZETtQ" static let bobMember = FakeMXRoomMember(displayname: bobDisplayname, avatarUrl: bobAvatarUrl, userId: bobUserId) static let bobPermalink = "https://matrix.to/#/@bob:matrix.org" static let markdownLinkToBob = "[\(bobDisplayname)](\(bobPermalink))" static let anotherUserId = "@another.user:matrix.org" static let anotherUserPermalink = "https://matrix.to/#/@another.user:matrix.org" static let markdownLinkToAnotherUser = "[Another user](\(alicePermalink))" static let mentionToAnotherUser = NSAttributedString(string: anotherUserPermalink, attributes: [.link: URL(string: anotherUserPermalink)!]) static let mentionToAnotherUserWithLabel = NSAttributedString(string: "Link text", attributes: [.link: URL(string: anotherUserPermalink)!]) static let roomId = "!vWieJcXcUdMwavNSvy:matrix.org" static let roomAlias = "#fake_room_alias:matrix.org" static let roomDisplayName = "Sample Room" static let roomPermalink = "https://matrix.to/#/\(roomId)" static let roomAliasPermalink = "https://matrix.to/%23/\(roomAlias)" static let roomAvatarUrl = "mxc://matrix.org/VzNZAgahaiAzUoOeZETtQ" static let mentionToRoom = NSAttributedString(string: roomPermalink, attributes: [.link: URL(string: roomPermalink)!]) static let mentionToRoomWithLabel = NSAttributedString(string: roomDisplayName, attributes: [.link: URL(string: roomPermalink)!]) static let mentionToRoomAlias = NSAttributedString(string: roomDisplayName, attributes: [.link: URL(string: roomAliasPermalink)!]) static let anotherRoomId = "!zWieBcUcUdMwavNSvy:matrix.org" static let anotherRoomDisplayName = "Room/Space" static let anotherRoomAvatarUrl = "mxc://matrix.org/VzNZBgajauAzUoOeZETtQ" static let messageEventId = "$JrEsoQO77MCdAubG6z-5oXlOBy1I5QL9FTut_Giztoc" static let messagePermalink = "https://matrix.to/#/\(roomId)/\(messageEventId)?via=matrix.org" static let messageAnotherRoomPermalink = "https://matrix.to/#/\(anotherRoomId)/\(messageEventId)?via=matrix.org" static let pillAnotherUserWithLinkText = "Link text" static let pillMessageAnotherRoomText = "Message in Sample Room" static let pillMessageFromBobText = "Message from Bob" } // MARK: - Tests @available(iOS 15.0, *) class PillsFormatterTests: XCTestCase { func testPillsInsertionAndRefresh() { let messageWithPills = createMessageWithMentionFromBobToAlice() XCTAssertEqual(messageWithPills.length, Inputs.messageStart.count + 1) // +1 non-unicode character for the pill/textAttachment XCTAssert(messageWithPills.attribute(.attachment, at: messageWithPills.length - 1, effectiveRange: nil) is PillTextAttachment) let pillTextAttachment = messageWithPills.attribute(.attachment, at: messageWithPills.length - 1, effectiveRange: nil) as? PillTextAttachment // Alice's pill is highlighted. XCTAssert(pillTextAttachment?.data?.isHighlighted == true) // Attachment has correct type. XCTAssert(pillTextAttachment?.fileType == PillsFormatter.pillUTType) // Pill data contains Alice's displayname and avatar url. XCTAssertNotNil(pillTextAttachment?.data) let pillTextAttachmentData: PillTextAttachmentData! = pillTextAttachment?.data XCTAssertEqual(pillTextAttachmentData.displayText, Inputs.aliceDisplayname) switch pillTextAttachmentData.pillType { case .user(let userId): XCTAssertEqual(userId, Inputs.aliceUserId) switch pillTextAttachmentData.items.first { case .avatar(let url, _, _): XCTAssertEqual(url, Inputs.aliceAvatarUrl) default: XCTFail("First pill item should be the avatar") } default: XCTFail("Pill should be of type .user") } // Pill has expected size. let expectedSize = pillTextAttachment?.size(forFont: pillTextAttachment!.data!.font) XCTAssertEqual(pillTextAttachment?.bounds.size, expectedSize) PillsFormatter.refreshPills(in: messageWithPills, with: FakeMXRoomState(roomMembers: FakeMXUpdatedRoomMembers())) let refreshedPillTextAttachment = messageWithPills.attribute(.attachment, at: messageWithPills.length - 1, effectiveRange: nil) as? PillTextAttachment // Alice's pill is still highlighted. XCTAssert(pillTextAttachment?.data?.isHighlighted == true) // Pill data is refreshed with correct data. let updatedPillTextAttachmentData: PillTextAttachmentData! = pillTextAttachment?.data XCTAssertEqual(updatedPillTextAttachmentData.displayText, Inputs.aliceAwayDisplayname) switch updatedPillTextAttachmentData.pillType { case .user(let userId): XCTAssertEqual(userId, Inputs.aliceUserId) switch updatedPillTextAttachmentData.items.first { case .avatar(let url, _, _): XCTAssertEqual(url, Inputs.aliceNewAvatarUrl) default: XCTFail("First pill item should be the avatar") } default: XCTFail("Pill should be of type .user") } // Pill size is updated let newExpectedSize = pillTextAttachment?.size(forFont: refreshedPillTextAttachment!.data!.font) XCTAssertEqual(refreshedPillTextAttachment?.bounds.size, newExpectedSize) } func testPillsUsingLatestRoomState() { let messageWithPills = createMessageWithMentionFromBobToAliceWithLatestRoomState() let pillTextAttachment = messageWithPills.attribute(.attachment, at: messageWithPills.length - 1, effectiveRange: nil) as? PillTextAttachment // Pill uses the latest room state data. XCTAssertNotNil(pillTextAttachment?.data) let pillTextAttachmentData: PillTextAttachmentData! = pillTextAttachment?.data XCTAssertEqual(pillTextAttachmentData.displayText, Inputs.aliceAwayDisplayname) switch pillTextAttachmentData.pillType { case .user(let userId): XCTAssertEqual(userId, Inputs.aliceUserId) switch pillTextAttachmentData.items.first { case .avatar(let url, _, _): XCTAssertEqual(url, Inputs.aliceNewAvatarUrl) default: XCTFail("First pill item should be the avatar") } default: XCTFail("Pill should be of type .message") } } func testPillsToMarkdown() { let messageWithPills = createMessageWithMentionFromBobToAlice() let markdownMessage = PillsFormatter.stringByReplacingPills(in: messageWithPills, mode: .markdown) XCTAssertEqual(markdownMessage, Inputs.messageStart + Inputs.markdownLinkToAlice) } func testPillsToRawBody() { let messageWithPills = createMessageWithMentionFromBobToAlice() let messageWithDisplayname = PillsFormatter.stringByReplacingPills(in: messageWithPills, mode: .displayname) let messageWithUserId = PillsFormatter.stringByReplacingPills(in: messageWithPills, mode: .identifier) XCTAssertEqual(messageWithDisplayname, Inputs.messageStart + Inputs.aliceDisplayname) XCTAssertEqual(messageWithUserId, Inputs.messageStart + Inputs.aliceUserId) } // Test case: a mention to an unknown user (not a room member) func testPillMentionningRoomMember() { let messageWithPills = createMessageWithMentionFromBobToAlice() let pillTextAttachment = messageWithPills.attribute(.attachment, at: messageWithPills.length - 1, effectiveRange: nil) as? PillTextAttachment // Pill uses the latest room state data. XCTAssertNotNil(pillTextAttachment?.data) let pillTextAttachmentData: PillTextAttachmentData! = pillTextAttachment?.data XCTAssertEqual(pillTextAttachmentData.displayText, Inputs.aliceDisplayname) switch pillTextAttachmentData.pillType { case .user(let userId): XCTAssertEqual(userId, Inputs.aliceUserId) switch pillTextAttachmentData.items.first { case .avatar(let url, _, _): XCTAssertEqual(url, Inputs.aliceAvatarUrl) default: XCTFail("First pill item should be the avatar") } default: XCTFail("Pill should be of type .user") } } // Test case: a mention to an unknown user (not a room member) func testPillMentionningUnknownUser() { let messageWithPills = createMessageWithMentionFromBobToAnotherUser() let pillTextAttachment = messageWithPills.attribute(.attachment, at: messageWithPills.length - 1, effectiveRange: nil) as? PillTextAttachment // Pill uses the latest room state data. XCTAssertNotNil(pillTextAttachment?.data) let pillTextAttachmentData: PillTextAttachmentData! = pillTextAttachment?.data XCTAssertEqual(pillTextAttachmentData.displayText, Inputs.anotherUserId) switch pillTextAttachmentData.pillType { case .user(let userId): XCTAssertEqual(userId, Inputs.anotherUserId) switch pillTextAttachmentData.items.first { case .asset(let name, _): XCTAssertEqual(name, "pill_user") default: XCTFail("First pill item should be the asset") } default: XCTFail("Pill should be of type .user") } } // Test case: a mention to an unknown user (not a room member) with a formatted text (HTML or MARKDOWN) // In this case, we don't want to pillify the link func testPillMentionningUnknownUserWithFormattedText() { let messageWithPills = createMessageWithMentionFromBobToAnotherUser(withLinkText: true) let pillTextAttachment = messageWithPills.attribute(.attachment, at: messageWithPills.length - 1, effectiveRange: nil) as? PillTextAttachment XCTAssertNil(pillTextAttachment) } // Test case: a mention to a room func testPillMentionningRoom() { let messageWithPills = createMessageWithMentionToRoom() XCTAssertEqual(messageWithPills.length, Inputs.messageStart.count + 1) // +1 non-unicode character for the pill/textAttachment XCTAssert(messageWithPills.attribute(.attachment, at: messageWithPills.length - 1, effectiveRange: nil) is PillTextAttachment) let pillTextAttachment = messageWithPills.attribute(.attachment, at: messageWithPills.length - 1, effectiveRange: nil) as? PillTextAttachment // Pill is not highlighted. XCTAssert(pillTextAttachment?.data?.isHighlighted == false) // Attachment has correct type. XCTAssert(pillTextAttachment?.fileType == PillsFormatter.pillUTType) // Pill data contains the correct displayname and avatar url. XCTAssertNotNil(pillTextAttachment?.data) let pillTextAttachmentData: PillTextAttachmentData! = pillTextAttachment?.data XCTAssertEqual(pillTextAttachmentData.displayText, Inputs.roomDisplayName) switch pillTextAttachmentData.pillType { case .room(let userId): XCTAssertEqual(userId, Inputs.roomId) switch pillTextAttachmentData.items.first { case .avatar(let url, _, _): XCTAssertEqual(url, Inputs.roomAvatarUrl) default: XCTFail("First pill item should be the avatar") } default: XCTFail("Pill should be of type .room") } } // Test case: a mention to a space func testPillMentionningSpace() { let messageWithPills = createMessageWithMentionToRoom(isSpace: true) let pillTextAttachment = messageWithPills.attribute(.attachment, at: messageWithPills.length - 1, effectiveRange: nil) as? PillTextAttachment // Pill is not highlighted. XCTAssert(pillTextAttachment?.data?.isHighlighted == false) // Attachment has correct type. XCTAssert(pillTextAttachment?.fileType == PillsFormatter.pillUTType) // Pill data contains the correct displayname and avatar url. XCTAssertNotNil(pillTextAttachment?.data) let pillTextAttachmentData: PillTextAttachmentData! = pillTextAttachment?.data XCTAssertEqual(pillTextAttachmentData.displayText, Inputs.roomDisplayName) switch pillTextAttachmentData.pillType { case .room(let userId): XCTAssertEqual(userId, Inputs.roomId) switch pillTextAttachmentData.items.first { case .spaceAvatar(let url, _, _): XCTAssertEqual(url, Inputs.roomAvatarUrl) default: XCTFail("First pill item should be the spaceAvatar") } default: XCTFail("Pill should be of type .room") } } // Test case: a mention to a room alias func testPillMentionningRoomByAlias() { let messageWithPills = createMessageWithMentionToRoom(usingAlias: true) let pillTextAttachment = messageWithPills.attribute(.attachment, at: messageWithPills.length - 1, effectiveRange: nil) as? PillTextAttachment // Pill is not highlighted. XCTAssert(pillTextAttachment?.data?.isHighlighted == false) // Attachment has correct type. XCTAssert(pillTextAttachment?.fileType == PillsFormatter.pillUTType) // Pill data contains the correct displayname and avatar url. XCTAssertNotNil(pillTextAttachment?.data) let pillTextAttachmentData: PillTextAttachmentData! = pillTextAttachment?.data XCTAssertEqual(pillTextAttachmentData.displayText, Inputs.roomDisplayName) switch pillTextAttachmentData.pillType { case .room(let userId): XCTAssertEqual(userId, Inputs.roomAlias) switch pillTextAttachmentData.items.first { case .avatar(let url, _, _): XCTAssertEqual(url, Inputs.roomAvatarUrl) default: XCTFail("First pill item should be the avatar") } default: XCTFail("Pill should be of type .room") } } // Test case: a mention to an unknown room func testPillMentionningUnknownRoom() { let messageWithPills = createMessageWithMentionToRoom(knownRoom: false) let pillTextAttachment = messageWithPills.attribute(.attachment, at: messageWithPills.length - 1, effectiveRange: nil) as? PillTextAttachment // Pill is not highlighted. XCTAssert(pillTextAttachment?.data?.isHighlighted == false) // Attachment has correct type. XCTAssert(pillTextAttachment?.fileType == PillsFormatter.pillUTType) // Pill data contains the correct displayname and avatar url. XCTAssertNotNil(pillTextAttachment?.data) let pillTextAttachmentData: PillTextAttachmentData! = pillTextAttachment?.data XCTAssertEqual(pillTextAttachmentData.displayText, VectorL10n.pillRoomFallbackDisplayName) switch pillTextAttachmentData.pillType { case .room(let userId): XCTAssertEqual(userId, Inputs.roomId) switch pillTextAttachmentData.items.first { case .asset(let assetName, _): XCTAssertEqual(assetName, "link_icon") default: XCTFail("First pill item should be the asset") } default: XCTFail("Pill should be of type .room") } } // Test case: a mention to an unknown room using a formatted text (HTML or MARKDOWN) func testPillMentionningUnknownRoomWithFormattedText() { let messageWithPills = createMessageWithMentionToRoom(knownRoom: false, withLinkText: "Link label") let pillTextAttachment = messageWithPills.attribute(.attachment, at: messageWithPills.length - 1, effectiveRange: nil) as? PillTextAttachment XCTAssertNil(pillTextAttachment) } // Test case: a mention to a message using a formatted text (HTML or MARKDOWN) func testPillMentionningMessageWithLabel() { let messageWithPills = createMessageWithMentionToMessage(from: Inputs.bobMember, withLabel: "Link label") let pillTextAttachment = messageWithPills.attribute(.attachment, at: messageWithPills.length - 1, effectiveRange: nil) as? PillTextAttachment XCTAssertNil(pillTextAttachment) } // Test case: a mention to a message sent by a room member in the current room func testPillMentionningMessageInCurrentRoomFromRoomMember() { // Test: a mention to current room message, sent by a room member (Bob) let messageWithPills = createMessageWithMentionToMessage(from: Inputs.bobMember, withLabel: Inputs.messagePermalink) let pillTextAttachment = messageWithPills.attribute(.attachment, at: messageWithPills.length - 1, effectiveRange: nil) as? PillTextAttachment // Pill is not highlighted. XCTAssert(pillTextAttachment?.data?.isHighlighted == false) // Attachment has correct type. XCTAssert(pillTextAttachment?.fileType == PillsFormatter.pillUTType) // Pill data contains the correct displayname and avatar url. XCTAssertNotNil(pillTextAttachment?.data) let pillTextAttachmentData: PillTextAttachmentData! = pillTextAttachment?.data XCTAssertEqual(pillTextAttachmentData.displayText, Inputs.pillMessageFromBobText) switch pillTextAttachmentData.pillType { case .message(let roomId, let messageId): XCTAssertEqual(roomId, Inputs.roomId) XCTAssertEqual(messageId, Inputs.messageEventId) let firstItem = pillTextAttachmentData.items[0] switch firstItem { case .avatar(let url, _, _): XCTAssertEqual(url, Inputs.bobAvatarUrl) default: XCTFail("First pill item should be the avatar") } default: XCTFail("Pill should be of type .message") } } // Test case: a mention to a message sent in the current room from an unknown user func testPillMentionningMessageInCurrentRoomFromUnknownUser() { let messageWithPills = createMessageWithMentionToMessage(sentBy: Inputs.anotherUserId, withLabel: Inputs.messagePermalink) let pillTextAttachment = messageWithPills.attribute(.attachment, at: messageWithPills.length - 1, effectiveRange: nil) as? PillTextAttachment // Pill is not highlighted. XCTAssert(pillTextAttachment?.data?.isHighlighted == false) // Attachment has correct type. XCTAssert(pillTextAttachment?.fileType == PillsFormatter.pillUTType) // Pill data contains the correct displayname and avatar url. XCTAssertNotNil(pillTextAttachment?.data) let pillTextAttachmentData: PillTextAttachmentData! = pillTextAttachment?.data XCTAssertEqual(pillTextAttachmentData.displayText, VectorL10n.pillMessage) switch pillTextAttachmentData.pillType { case .message(let roomId, let messageId): XCTAssertEqual(roomId, Inputs.roomId) XCTAssertEqual(messageId, Inputs.messageEventId) let firstItem = pillTextAttachmentData.items[0] switch firstItem { case .asset(let name, _): XCTAssertEqual(name, "link_icon") default: XCTFail("First pill item should be the asset") } default: XCTFail("Pill should be of type .message") } } // Test case: a mention to a message in another room func testPillMentionningMessageInAnotherRoom() { let messageWithPills = createMessageWithMentionToAnotherRoomMessage(knownRoom: true, withLabel: Inputs.messageAnotherRoomPermalink) let pillTextAttachment = messageWithPills.attribute(.attachment, at: messageWithPills.length - 1, effectiveRange: nil) as? PillTextAttachment // Pill is not highlighted. XCTAssert(pillTextAttachment?.data?.isHighlighted == false) // Attachment has correct type. XCTAssert(pillTextAttachment?.fileType == PillsFormatter.pillUTType) // Pill data contains the correct displayname and avatar url. XCTAssertNotNil(pillTextAttachment?.data) let pillTextAttachmentData: PillTextAttachmentData! = pillTextAttachment?.data XCTAssertEqual(pillTextAttachmentData.displayText, VectorL10n.pillMessageIn(Inputs.anotherRoomDisplayName)) switch pillTextAttachmentData.pillType { case .message(let roomId, let messageId): XCTAssertEqual(roomId, Inputs.anotherRoomId) XCTAssertEqual(messageId, Inputs.messageEventId) switch pillTextAttachmentData.items.first { case .avatar(let url, _, _): XCTAssertEqual(url, Inputs.anotherRoomAvatarUrl) default: XCTFail("First pill item should be the avatar") } default: XCTFail("Pill should be of type .message") } } // Test case: a mention to a message in an unknown room func testPillMentionningMessageInUnknownRoom() { let messageWithPills = createMessageWithMentionToAnotherRoomMessage(knownRoom: false, withLabel: Inputs.messageAnotherRoomPermalink) let pillTextAttachment = messageWithPills.attribute(.attachment, at: messageWithPills.length - 1, effectiveRange: nil) as? PillTextAttachment // Pill is not highlighted. XCTAssert(pillTextAttachment?.data?.isHighlighted == false) // Attachment has correct type. XCTAssert(pillTextAttachment?.fileType == PillsFormatter.pillUTType) // Pill data contains the correct displayname and avatar url. XCTAssertNotNil(pillTextAttachment?.data) let pillTextAttachmentData: PillTextAttachmentData! = pillTextAttachment?.data XCTAssertEqual(pillTextAttachmentData.displayText, VectorL10n.pillMessage) switch pillTextAttachmentData.pillType { case .message(let roomId, let messageId): XCTAssertEqual(roomId, Inputs.anotherRoomId) XCTAssertEqual(messageId, Inputs.messageEventId) switch pillTextAttachmentData.items.first { case .asset(let name, _): XCTAssertEqual(name, "link_icon") default: XCTFail("First pill item should be the asset") } default: XCTFail("Pill should be of type .message") } } func testInsertPillInMarkdownString() { let message = "Hello \(Inputs.markdownLinkToBob)" let messageWithPills = insertPillsInMarkdownString(message) XCTAssertTrue(messageWithPills.attribute(.attachment, at: 6, effectiveRange: nil) is PillTextAttachment) let pillTextAttachment = messageWithPills.attribute(.attachment, at: 6, effectiveRange: nil) as? PillTextAttachment XCTAssertEqual(pillTextAttachment?.data?.displayText, Inputs.bobDisplayname) } func testInsertMultiplePillsInMarkdownString() { let message = "Hello \(Inputs.markdownLinkToBob) and \(Inputs.markdownLinkToAlice)" let messageWithPills = insertPillsInMarkdownString(message) let bobPillTextAttachment = messageWithPills.attribute(.attachment, at: 6, effectiveRange: nil) as? PillTextAttachment XCTAssertEqual(bobPillTextAttachment?.data?.displayText, Inputs.bobDisplayname) let alicePillTextAttachment = messageWithPills.attribute(.attachment, at: 12, effectiveRange: nil) as? PillTextAttachment XCTAssertEqual(alicePillTextAttachment?.data?.displayText, Inputs.aliceDisplayname) // No self highlight XCTAssert(alicePillTextAttachment?.data?.isHighlighted == false) } func testMarkdownLinkToUnknownUserIsNotPillified() { let message = "Hello [Unknown user](https://matrix.to/#/@unknown:matrix.org)" let messageWithPills = insertPillsInMarkdownString(message) XCTAssertFalse(messageWithPills.attribute(.attachment, at: 6, effectiveRange: nil) is PillTextAttachment) } func testMarkdownSingleLinkDetection() { let message = NSAttributedString(string: "Hello \(Inputs.markdownLinkToAlice)") let expected = [ PillsFormatter.MarkdownLinkResult(url: URL(string: Inputs.alicePermalink)!, label: Inputs.aliceDisplayname, range: NSRange(location: 6, length: Inputs.markdownLinkToAlice.count)) ] XCTAssertEqual( PillsFormatter.markdownLinks(in: message), expected ) } func testMarkdownMultipleLinksDetection() { let message = NSAttributedString(string: "Hello \(Inputs.markdownLinkToAlice) and \(Inputs.markdownLinkToBob)") let expected = [ PillsFormatter.MarkdownLinkResult(url: URL(string: Inputs.alicePermalink)!, label: Inputs.aliceDisplayname, range: NSRange(location: 6, length: Inputs.markdownLinkToAlice.count)), PillsFormatter.MarkdownLinkResult(url: URL(string: Inputs.bobPermalink)!, label: Inputs.bobDisplayname, range: NSRange(location: 6 + Inputs.markdownLinkToAlice.count + 5, length: Inputs.markdownLinkToBob.count)) ] XCTAssertEqual( PillsFormatter.markdownLinks(in: message), expected ) } func testBrokenMarkdownLinkIsNotDetected() { let brokenMarkdownMessages = [ NSAttributedString(string: "Hello [Alice](https://matrix.to/#/@alice:matrix.org"), NSAttributedString(string: "Hello [Alice]https://matrix.to/#/@alice:matrix.org)"), NSAttributedString(string: "Hello [Alice(https://matrix.to/#/@alice:matrix.org)"), NSAttributedString(string: "Hello Alice](https://matrix.to/#/@alice:matrix.org)"), NSAttributedString(string: "Hello [Alice]](https://matrix.to/#/@alice:matrix.org)"), NSAttributedString(string: "Hello (https://matrix.to/#/@alice:matrix.org)"), ] for message in brokenMarkdownMessages { XCTAssertTrue(PillsFormatter.markdownLinks(in: message).isEmpty) } } } @available(iOS 15.0, *) private extension PillsFormatterTests { func createMessageWithMentionFromBobToAlice() -> NSAttributedString { let formattedMessage = NSMutableAttributedString(string: Inputs.messageStart) formattedMessage.append(Inputs.mentionToAlice) let session = FakeMXSession(myUserId: Inputs.aliceMember.userId) let messageWithPills = PillsFormatter.insertPills(in: formattedMessage, withSession: session, eventFormatter: EventFormatter(matrixSession: session), event: FakeMXEvent(sender: Inputs.bobMember.userId), roomState: FakeMXRoomState(roomMembers: FakeMXRoomMembers()), andLatestRoomState: nil) return messageWithPills } func createMessageWithMentionFromBobToAnotherUser(withLinkText: Bool = false) -> NSAttributedString { let formattedMessage = NSMutableAttributedString(string: Inputs.messageStart) if withLinkText { formattedMessage.append(Inputs.mentionToAnotherUserWithLabel) } else { formattedMessage.append(Inputs.mentionToAnotherUser) } let session = FakeMXSession(myUserId: Inputs.aliceMember.userId) let messageWithPills = PillsFormatter.insertPills(in: formattedMessage, withSession: session, eventFormatter: EventFormatter(matrixSession: session), event: FakeMXEvent(sender: Inputs.anotherUserId), roomState: FakeMXRoomState(roomMembers: FakeMXRoomMembers()), andLatestRoomState: nil) return messageWithPills } func createMessageWithMentionFromBobToAliceWithLatestRoomState() -> NSAttributedString { let formattedMessage = NSMutableAttributedString(string: Inputs.messageStart) formattedMessage.append(Inputs.mentionToAlice) let session = FakeMXSession(myUserId: Inputs.aliceMember.userId) let messageWithPills = PillsFormatter.insertPills(in: formattedMessage, withSession: session, eventFormatter: EventFormatter(matrixSession: session), event: FakeMXEvent(sender: Inputs.bobMember.userId), roomState: FakeMXRoomState(roomMembers: FakeMXRoomMembers()), andLatestRoomState: FakeMXRoomState(roomMembers: FakeMXUpdatedRoomMembers())) return messageWithPills } func createMessageWithMentionToRoom(isSpace: Bool = false, knownRoom: Bool = true, usingAlias: Bool = false, withLinkText: String? = nil) -> NSAttributedString { let formattedMessage = NSMutableAttributedString(string: Inputs.messageStart) let mention: NSAttributedString if usingAlias { mention = NSAttributedString(string: withLinkText ?? Inputs.roomAliasPermalink , attributes: [.link: URL(string: Inputs.roomAliasPermalink)!]) } else { mention = NSAttributedString(string: withLinkText ?? Inputs.roomPermalink , attributes: [.link: URL(string: Inputs.roomPermalink)!]) } formattedMessage.append(mention) let session = FakeMXSession(myUserId: Inputs.aliceMember.userId) let event = FakeMXEvent(eventId: Inputs.messageEventId, sender: Inputs.bobMember.userId) session.store = FakeMXStore(withEvents: [event]) if knownRoom { let room = FakeMXRoom(roomId: Inputs.roomId, matrixSession: session, andStore: nil)! let roomSummary = FakeMXRoomSummary(roomId: Inputs.roomId, displayName: Inputs.roomDisplayName, alias: Inputs.roomAlias, avatar: Inputs.roomAvatarUrl, matrixSession: session) if isSpace { roomSummary.roomType = .space } session.addFakeRoom(room) session.addFakeRoomSummary(roomSummary) } let messageWithPills = PillsFormatter.insertPills(in: formattedMessage, withSession: session, eventFormatter: EventFormatter(matrixSession: session), event: event, roomState: FakeMXRoomState(roomMembers: FakeMXRoomMembers()), andLatestRoomState: nil) return messageWithPills } func createMessageWithMentionToMessage(from sender: MXRoomMember, withLabel string: String) -> NSAttributedString { let formattedMessage = NSMutableAttributedString(string: Inputs.messageStart) formattedMessage.append(NSAttributedString(string: string, attributes: [.link: URL(string: Inputs.messagePermalink)!])) let session = FakeMXSession(myUserId: Inputs.aliceMember.userId) let event = FakeMXEvent(eventId: Inputs.messageEventId, sender: sender.userId) session.store = FakeMXStore(withEvents: [event]) let room = FakeMXRoom(roomId: Inputs.roomId, matrixSession: session, andStore: nil)! let roomSummary = FakeMXRoomSummary(roomId: Inputs.roomId, displayName: Inputs.roomDisplayName, alias: Inputs.roomAlias, avatar: Inputs.roomAvatarUrl, matrixSession: session) session.addFakeRoom(room) session.addFakeRoomSummary(roomSummary) let messageWithPills = PillsFormatter.insertPills(in: formattedMessage, withSession: session, eventFormatter: EventFormatter(matrixSession: session), event: event, roomState: FakeMXRoomState(roomMembers: FakeMXRoomMembers(), roomId: Inputs.roomId), andLatestRoomState: nil) return messageWithPills } func createMessageWithMentionToMessage(sentBy senderId: String, withLabel string: String) -> NSAttributedString { let formattedMessage = NSMutableAttributedString(string: Inputs.messageStart) formattedMessage.append(NSAttributedString(string: string, attributes: [.link: URL(string: Inputs.messagePermalink)!])) let session = FakeMXSession(myUserId: Inputs.aliceMember.userId) let event = FakeMXEvent(eventId: Inputs.messageEventId, sender: senderId) session.store = FakeMXStore(withEvents: [event]) let room = FakeMXRoom(roomId: Inputs.roomId, matrixSession: session, andStore: nil)! let roomSummary = FakeMXRoomSummary(roomId: Inputs.roomId, displayName: Inputs.roomDisplayName, alias: Inputs.roomAlias, avatar: Inputs.roomAvatarUrl, matrixSession: session) session.addFakeRoom(room) session.addFakeRoomSummary(roomSummary) let messageWithPills = PillsFormatter.insertPills(in: formattedMessage, withSession: session, eventFormatter: EventFormatter(matrixSession: session), event: event, roomState: FakeMXRoomState(roomMembers: FakeMXRoomMembers(), roomId: Inputs.roomId), andLatestRoomState: nil) return messageWithPills } func createMessageWithMentionToAnotherRoomMessage(knownRoom: Bool, withLabel string: String) -> NSAttributedString { let formattedMessage = NSMutableAttributedString(string: Inputs.messageStart) formattedMessage.append(NSAttributedString(string: string, attributes: [.link: URL(string: Inputs.messageAnotherRoomPermalink)!])) let session = FakeMXSession(myUserId: Inputs.aliceMember.userId) let event = FakeMXEvent(eventId: Inputs.messageEventId, sender: Inputs.anotherUserId) session.store = FakeMXStore(withEvents: [event]) if knownRoom { let room = FakeMXRoom(roomId: Inputs.anotherRoomId, matrixSession: session, andStore: nil)! let roomSummary = FakeMXRoomSummary(roomId: Inputs.anotherRoomId, displayName: Inputs.anotherRoomDisplayName, alias: nil, avatar: Inputs.anotherRoomAvatarUrl, matrixSession: session) session.addFakeRoom(room) session.addFakeRoomSummary(roomSummary) } let messageWithPills = PillsFormatter.insertPills(in: formattedMessage, withSession: session, eventFormatter: EventFormatter(matrixSession: session), event: event, roomState: FakeMXRoomState(roomMembers: FakeMXRoomMembers(), roomId: Inputs.roomId), andLatestRoomState: nil) return messageWithPills } private func insertPillsInMarkdownString(_ markdownString: String) -> NSAttributedString { let message = NSAttributedString(string: markdownString) let session = FakeMXSession(myUserId: Inputs.aliceUserId) return PillsFormatter.insertPills(in: message, withSession: session, eventFormatter: EventFormatter(matrixSession: session), roomState: FakeMXRoomState(roomMembers: FakeMXRoomMembers()), font: UIFont.systemFont(ofSize: 15.0)) } } // MARK: - Mock objects private class FakeMXSession: MXSession { private var mockMyUserId: String private var mockRooms: [FakeMXRoom] = [] private var mockRoomSummaries: [String: FakeMXRoomSummary] = [:] private var mockStore: FakeMXStore? init(myUserId: String) { mockMyUserId = myUserId let credentials = MXCredentials(homeServer: "mock_home_server", userId: "mock_user_id", accessToken: "mock_access_token") let client = MXRestClient(credentials: credentials) super.init(matrixRestClient: client) } override var myUserId: String! { return mockMyUserId } func addFakeRoom(_ room: FakeMXRoom) { mockRooms.append(room) } override func room(withRoomId roomId: String!) -> MXRoom! { return mockRooms.first(where: { $0.roomId == roomId }) } override func room(withAlias roomAlias: String) -> MXRoom? { for (roomId, summary) in mockRoomSummaries { if summary.aliases.contains(roomAlias) { return room(withRoomId: roomId) } } return nil } override func roomSummary(withRoomId roomId: String!) -> MXRoomSummary? { return mockRoomSummaries[roomId] } func addFakeRoomSummary(_ roomSummary: FakeMXRoomSummary) { self.mockRoomSummaries[roomSummary.roomId] = roomSummary } override var store: MXStore! { get { return mockStore } set { mockStore = newValue as? FakeMXStore } } } private class FakeMXStore: MXMemoryStore { private var mockEvents: [MXEvent] init(withEvents events: [MXEvent]) { self.mockEvents = events super.init() } override func event(withEventId eventId: String, inRoom roomId: String) -> MXEvent? { return mockEvents.first(where: { $0.eventId == eventId }) } } private class FakeMXRoom: MXRoom { private var mockDisplayName: String? = nil override init() { super.init() } override init!(roomId: String!, matrixSession mxSession: MXSession!, andStore store: MXStore!) { super.init(roomId: roomId, matrixSession: mxSession, andStore: store) } override var summary: MXRoomSummary! { return mxSession?.roomSummary(withRoomId: self.roomId) } } private class FakeMXRoomSummary: MXRoomSummary { private var mockDisplayName: String? private var mockAliases: [String]? private var mockAvatar: String? = nil override init() { super.init() } init(roomId: String, displayName: String, alias: String?, avatar: String?, matrixSession mxSession: MXSession) { super.init(roomId: roomId, andMatrixSession: mxSession) self.mockDisplayName = displayName self.mockAliases = alias.flatMap { [$0] } ?? [] self.mockAvatar = avatar } override init!(roomId: String!, matrixSession mxSession: MXSession!, andStore store: MXStore!) { super.init(roomId: roomId, matrixSession: mxSession, andStore: store) } override init!(roomId: String!, andMatrixSession mxSession: MXSession!) { super.init(roomId: roomId, andMatrixSession: mxSession) } required init?(coder: NSCoder) { fatalError() } override var displayName: String! { get { return mockDisplayName } set { mockDisplayName = newValue } } override var avatar: String! { get { return mockAvatar } set { mockAvatar = newValue } } override var aliases: [String]! { get { return mockAliases } set { mockAliases = newValue } } } private class FakeMXRoomState: MXRoomState { private let mockRoomMembers: MXRoomMembers private let mockRoomId: String? init(roomMembers: MXRoomMembers) { mockRoomMembers = roomMembers mockRoomId = nil super.init() } init(roomMembers: MXRoomMembers, roomId: String) { mockRoomMembers = roomMembers mockRoomId = roomId super.init() } override var members: MXRoomMembers! { return mockRoomMembers } override var roomId: String! { return mockRoomId } } private class FakeMXUpdatedRoomMembers: MXRoomMembers { override var members: [MXRoomMember]! { return [Inputs.aliceMemberAway, Inputs.bobMember] } override func member(withUserId userId: String!) -> MXRoomMember! { return members.first(where: { $0.userId == userId }) } } private class FakeMXRoomMembers: MXRoomMembers { override var members: [MXRoomMember]! { return [Inputs.aliceMember, Inputs.bobMember] } override func member(withUserId userId: String!) -> MXRoomMember! { return members.first(where: { $0.userId == userId }) } } private class FakeMXRoomMember: MXRoomMember { private let mockDisplayname: String private var mockAvatarUrl: String private let mockUserId: String init(displayname: String, avatarUrl: String, userId: String) { mockDisplayname = displayname mockAvatarUrl = avatarUrl mockUserId = userId super.init() } required init?(coder: NSCoder) { fatalError() } override var displayname: String! { return mockDisplayname } override var avatarUrl: String! { get { return mockAvatarUrl } set { mockAvatarUrl = newValue } } override var userId: String! { return mockUserId } } private class FakeMXEvent: MXEvent { private var mockSender: String private var mockEventId: String? init(sender: String) { mockSender = sender mockEventId = nil super.init() } init(eventId: String, sender: String) { mockEventId = eventId mockSender = sender super.init() } required init?(coder: NSCoder) { fatalError() } override var sender: String! { get { return mockSender } set { mockSender = newValue } } override var eventId: String! { get { return mockEventId } set { mockEventId = newValue } } }