918 lines
44 KiB
Swift
918 lines
44 KiB
Swift
//
|
|
// Copyright 2022-2024 New Vector Ltd.
|
|
//
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
// Please see LICENSE in the repository root for full details.
|
|
//
|
|
|
|
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 }
|
|
}
|
|
}
|