// // Copyright 2022-2024 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. // @testable import ElementX import XCTest class AttributedStringBuilderTests: XCTestCase { private lazy var attributedStringBuilder = AttributedStringBuilder(mentionBuilder: MentionBuilder()) private let maxHeaderPointSize = ceil(UIFont.preferredFont(forTextStyle: .body).pointSize * 1.2) func testRenderHTMLStringWithHeaders() { let h1HTMLString = "
1\n2\n3\n4\n
"
guard let attributedString = attributedStringBuilder.fromHTML(htmlString) else {
XCTFail("Could not build the attributed string")
return
}
XCTAssertEqual(attributedString.runs.first?.uiKit.font?.fontName, ".AppleSystemUIFontMonospaced-Regular")
let string = String(attributedString.characters)
guard let regex = try? NSRegularExpression(pattern: "\\R", options: []) else {
XCTFail("Could not build the regex for the test.")
return
}
XCTAssertEqual(regex.numberOfMatches(in: string, options: [], range: .init(location: 0, length: string.count)), 3)
}
func testRenderHTMLStringWithLink() {
let htmlString = "This text contains a link."
guard let attributedString = attributedStringBuilder.fromHTML(htmlString) else {
XCTFail("Could not build the attributed string")
return
}
XCTAssertEqual(String(attributedString.characters), "This text contains a link.")
XCTAssertEqual(attributedString.runs.count, 3)
let link = attributedString.runs.first { $0.link != nil }?.link
XCTAssertEqual(link?.host, "www.matrix.org")
}
func testRenderPlainStringWithLink() {
let plainString = "This text contains a https://www.matrix.org link."
guard let attributedString = attributedStringBuilder.fromPlain(plainString) else {
XCTFail("Could not build the attributed string")
return
}
XCTAssertEqual(String(attributedString.characters), plainString)
XCTAssertEqual(attributedString.runs.count, 3)
let link = attributedString.runs.first { $0.link != nil }?.link
XCTAssertEqual(link?.host, "www.matrix.org")
}
func testPunctuationAtTheEndOfPlainStringLinks() {
let plainString = "This text contains a https://www.matrix.org:;., link."
guard let attributedString = attributedStringBuilder.fromPlain(plainString) else {
XCTFail("Could not build the attributed string")
return
}
XCTAssertEqual(String(attributedString.characters), plainString)
XCTAssertEqual(attributedString.runs.count, 3)
let link = attributedString.runs.first { $0.link != nil }?.link
XCTAssertEqual(link?.host, "www.matrix.org")
}
func testLinkDefaultScheme() {
let plainString = "This text contains a matrix.org link."
guard let attributedString = attributedStringBuilder.fromPlain(plainString) else {
XCTFail("Could not build the attributed string")
return
}
XCTAssertEqual(String(attributedString.characters), plainString)
XCTAssertEqual(attributedString.runs.count, 3)
let link = attributedString.runs.first { $0.link != nil }?.link
XCTAssertEqual(link, "https://matrix.org")
}
func testRenderHTMLStringWithLinkInHeader() {
let h1HTMLString = "link
"
guard let attributedString = attributedStringBuilder.fromHTML(htmlString) else {
XCTFail("Could not build the attributed string")
return
}
XCTAssertEqual(attributedString.runs.count, 7)
for run in attributedString.runs {
XCTAssertNil(run.uiKit.foregroundColor)
}
}
func testCustomForegroundColor() {
// swiftlint:disable:next line_length
let htmlString = "Rain www.matrix.org bow"
guard let attributedString = attributedStringBuilder.fromHTML(htmlString) else {
XCTFail("Could not build the attributed string")
return
}
XCTAssertEqual(attributedString.runs.count, 3)
var foundLink = false
// Foreground colors should be completely stripped from the attributed string
// letting UI components chose the defaults (e.g. tintColor)
for run in attributedString.runs {
if run.link != nil {
XCTAssertEqual(run.link?.host, "www.matrix.org")
XCTAssertNil(run.uiKit.foregroundColor)
foundLink = true
} else {
XCTAssertNil(run.uiKit.foregroundColor)
}
}
XCTAssertTrue(foundLink)
}
func testSingleBlockquote() {
let htmlString = "Blockquote" guard let attributedString = attributedStringBuilder.fromHTML(htmlString) else { XCTFail("Could not build the attributed string") return } XCTAssertEqual(attributedString.runs.count, 1) XCTAssertEqual(attributedString.formattedComponents.count, 1) for run in attributedString.runs where run.elementX.blockquote ?? false { return } XCTFail("Couldn't find blockquote") } // swiftlint:disable line_length func testBlockquoteWithinText() { let htmlString = """ The text before the blockquote
For 50 years, WWF has been protecting the future of nature. The world's leading conservation organization, WWF works in 100 countries and is supported by 1.2 million members in the United States and close to 5 million globally.The text after the blockquote """ guard let attributedString = attributedStringBuilder.fromHTML(htmlString) else { XCTFail("Could not build the attributed string") return } XCTAssertEqual(attributedString.runs.count, 3) XCTAssertEqual(attributedString.formattedComponents.count, 3) for run in attributedString.runs where run.elementX.blockquote ?? false { return } XCTFail("Couldn't find blockquote") } // swiftlint:enable line_length func testBlockquoteWithLink() { let htmlString = "
Blockquote with a link in it" guard let attributedString = attributedStringBuilder.fromHTML(htmlString) else { XCTFail("Could not build the attributed string") return } XCTAssertEqual(attributedString.runs.count, 3) let coalescedComponents = attributedString.formattedComponents XCTAssertEqual(coalescedComponents.count, 1) XCTAssertEqual(coalescedComponents.first?.attributedString.runs.count, 3, "Link not present in the component") var foundBlockquoteAndLink = false for run in attributedString.runs where run.elementX.blockquote ?? false && run.link != nil { foundBlockquoteAndLink = true } XCTAssertNotNil(foundBlockquoteAndLink, "Couldn't find blockquote or link") } func testReplyBlockquote() { let htmlString = "
In reply to @user:matrix.org" guard let attributedString = attributedStringBuilder.fromHTML(htmlString) else { XCTFail("Could not build the attributed string") return } let coalescedComponents = attributedString.formattedComponents XCTAssertEqual(coalescedComponents.count, 1) guard let component = coalescedComponents.first else { XCTFail("Could not get the first component") return } XCTAssertTrue(component.isBlockquote, "The reply quote should be a blockquote.") } func testMultipleGroupedBlockquotes() { let htmlString = """
The future isswift run tools
😎
First blockquote with a link in it
Second blockquote with a link in it
Third blockquote with a link in it""" guard let attributedString = attributedStringBuilder.fromHTML(htmlString) else { XCTFail("Could not build the attributed string") return } XCTAssertEqual(attributedString.runs.count, 7) XCTAssertEqual(attributedString.formattedComponents.count, 1) var numberOfBlockquotes = 0 for run in attributedString.runs where run.elementX.blockquote ?? false && run.link != nil { numberOfBlockquotes += 1 } XCTAssertEqual(numberOfBlockquotes, 3, "Couldn't find all the blockquotes") } func testMultipleSeparatedBlockquotes() { let htmlString = """ First
blockquote with a link in itSecond
blockquote with a link in itThird
blockquote with a link in it""" guard let attributedString = attributedStringBuilder.fromHTML(htmlString) else { XCTFail("Could not build the attributed string") return } XCTAssertEqual(attributedString.runs.count, 12) let coalescedComponents = attributedString.formattedComponents XCTAssertEqual(coalescedComponents.count, 6) var numberOfBlockquotes = 0 for run in attributedString.runs where run.elementX.blockquote ?? false && run.link != nil { numberOfBlockquotes += 1 } XCTAssertEqual(numberOfBlockquotes, 3, "Couldn't find all the blockquotes") } func testUserMentionAtachment() { let string = "https://matrix.to/#/@test:matrix.org" let attributedStringFromHTML = attributedStringBuilder.fromHTML(string) XCTAssertNotNil(attributedStringFromHTML?.attachment) XCTAssertNotNil(attributedStringFromHTML?.link) let attributedStringFromPlain = attributedStringBuilder.fromPlain(string) XCTAssertNotNil(attributedStringFromPlain?.attachment) XCTAssertNotNil(attributedStringFromHTML?.link) } func testUserMentionAtachmentInBlockQuotes() { let link = "https://matrix.to/#/@test:matrix.org" let string = "
hello \(link) how are you?" guard let attributedStringFromHTML = attributedStringBuilder.fromHTML(string) else { XCTFail("Attributed string is nil") return } for run in attributedStringFromHTML.runs { XCTAssertNotNil(run.blockquote) } checkAttachment(attributedString: attributedStringFromHTML, expectedRuns: 3) checkLinkIn(attributedString: attributedStringFromHTML, expectedLink: link, expectedRuns: 3) } func testAllUsersMentionAtachmentInBlockQuotes() { let string = "
hello @room how are you?" guard let attributedStringFromHTML = attributedStringBuilder.fromHTML(string) else { XCTFail("Attributed string is nil") return } for run in attributedStringFromHTML.runs { XCTAssertNotNil(run.blockquote) } checkAttachment(attributedString: attributedStringFromHTML, expectedRuns: 3) } func testAllUsersMentionAttachment() { let string = "@room" let attributedStringFromHTML = attributedStringBuilder.fromHTML(string) checkAttachment(attributedString: attributedStringFromHTML, expectedRuns: 1) let attributedStringFromPlain = attributedStringBuilder.fromPlain(string) checkAttachment(attributedString: attributedStringFromPlain, expectedRuns: 1) let string2 = "Hello @room" let attributedStringFromHTML2 = attributedStringBuilder.fromHTML(string2) checkAttachment(attributedString: attributedStringFromHTML2, expectedRuns: 2) let attributedStringFromPlain2 = attributedStringBuilder.fromPlain(string2) checkAttachment(attributedString: attributedStringFromPlain2, expectedRuns: 2) let string3 = "Hello @room how are you doing?" let attributedStringFromHTML3 = attributedStringBuilder.fromHTML(string3) checkAttachment(attributedString: attributedStringFromHTML3, expectedRuns: 3) let attributedStringFromPlain3 = attributedStringBuilder.fromPlain(string3) checkAttachment(attributedString: attributedStringFromPlain3, expectedRuns: 3) } func testLinksHavePriorityOverAllUserMention() { let string = "https://test@room.org" let attributedStringFromHTML = attributedStringBuilder.fromHTML(string) checkLinkIn(attributedString: attributedStringFromHTML, expectedLink: string, expectedRuns: 1) let attributedStringFromPlain = attributedStringBuilder.fromPlain(string) checkLinkIn(attributedString: attributedStringFromPlain, expectedLink: string, expectedRuns: 1) let string2 = "https://matrix.to/#/@roomusername:matrix.org" let attributedStringFromHTML2 = attributedStringBuilder.fromHTML(string2) checkLinkIn(attributedString: attributedStringFromHTML2, expectedLink: string2, expectedRuns: 1) checkAttachment(attributedString: attributedStringFromHTML2, expectedRuns: 1) let attributedStringFromPlain2 = attributedStringBuilder.fromPlain(string2) checkLinkIn(attributedString: attributedStringFromPlain2, expectedLink: string2, expectedRuns: 1) checkAttachment(attributedString: attributedStringFromPlain2, expectedRuns: 1) } func testURLsAreIgnoredInCode() { var htmlString = "
test https://matrix.org test
"
var attributedStringFromHTML = attributedStringBuilder.fromHTML(htmlString)
XCTAssert(attributedStringFromHTML?.runs.count == 1)
XCTAssertNil(attributedStringFromHTML?.link)
htmlString = "matrix.org
"
attributedStringFromHTML = attributedStringBuilder.fromHTML(htmlString)
XCTAssert(attributedStringFromHTML?.runs.count == 1)
XCTAssertNil(attributedStringFromHTML?.link)
}
func testHyperlinksAreIgnoredInCode() {
let htmlString = "test matrix test
"
let attributedStringFromHTML = attributedStringBuilder.fromHTML(htmlString)
XCTAssertNil(attributedStringFromHTML?.link)
}
func testUserMentionIsIgnoredInCode() {
let htmlString = "test https://matrix.org/#/@test:matrix.org test
"
let attributedStringFromHTML = attributedStringBuilder.fromHTML(htmlString)
XCTAssert(attributedStringFromHTML?.runs.count == 1)
XCTAssertNil(attributedStringFromHTML?.attachment)
}
func testPlainTextUserMentionIsIgnoredInCode() {
let htmlString = "Hey @some.user.ceriu:matrix.org
"
let attributedStringFromHTML = attributedStringBuilder.fromHTML(htmlString)
XCTAssert(attributedStringFromHTML?.runs.count == 1)
XCTAssertNil(attributedStringFromHTML?.attachment)
}
func testAllUsersIsIgnoredInCode() {
let htmlString = "test @room test
"
let attributedStringFromHTML = attributedStringBuilder.fromHTML(htmlString)
XCTAssert(attributedStringFromHTML?.runs.count == 1)
XCTAssertNil(attributedStringFromHTML?.attachment)
}
func testMultipleMentions() {
guard let url = URL(string: "https://matrix.to/#/@test:matrix.org") else {
XCTFail("Invalid url")
return
}
let string = "Hello @room, but especially hello to you \(url)"
guard let attributedStringFromHTML = attributedStringBuilder.fromHTML(string) else {
XCTFail("Attributed string is nil")
return
}
var foundAttachments = 0
var foundLink: URL?
for run in attributedStringFromHTML.runs {
if run.attachment != nil {
foundAttachments += 1
}
if let link = run.link {
foundLink = link
}
}
XCTAssertEqual(foundLink, url)
XCTAssertEqual(foundAttachments, 2)
guard let attributedStringFromPlain = attributedStringBuilder.fromPlain(string) else {
XCTFail("Attributed string is nil")
return
}
foundAttachments = 0
foundLink = nil
for run in attributedStringFromPlain.runs {
if run.attachment != nil {
foundAttachments += 1
}
if let link = run.link {
foundLink = link
}
}
XCTAssertEqual(foundLink, url)
XCTAssertEqual(foundAttachments, 2)
}
func testMultipleMentions2() {
guard let url = URL(string: "https://matrix.to/#/@test:matrix.org") else {
XCTFail("Invalid url")
return
}
let string = "\(url) @room"
guard let attributedStringFromHTML = attributedStringBuilder.fromHTML(string) else {
XCTFail("Attributed string is nil")
return
}
var foundAttachments = 0
var foundLink: URL?
for run in attributedStringFromHTML.runs {
if run.attachment != nil {
foundAttachments += 1
}
if let link = run.link {
foundLink = link
}
}
XCTAssertEqual(foundLink, url)
XCTAssertEqual(foundAttachments, 2)
guard let attributedStringFromPlain = attributedStringBuilder.fromPlain(string) else {
XCTFail("Attributed string is nil")
return
}
foundAttachments = 0
foundLink = nil
for run in attributedStringFromPlain.runs {
if run.attachment != nil {
foundAttachments += 1
}
if let link = run.link {
foundLink = link
}
}
XCTAssertEqual(foundLink, url)
XCTAssertEqual(foundAttachments, 2)
}
// MARK: - Private
private func checkLinkIn(attributedString: AttributedString?, expectedLink: String, expectedRuns: Int) {
guard let attributedString else {
XCTFail("Could not build the attributed string")
return
}
XCTAssertEqual(attributedString.runs.count, expectedRuns)
for run in attributedString.runs where run.link != nil {
XCTAssertEqual(run.link?.absoluteString, expectedLink)
return
}
XCTFail("Couldn't find expected value.")
}
private func checkAttachment(attributedString: AttributedString?, expectedRuns: Int) {
guard let attributedString else {
XCTFail("Could not build the attributed string")
return
}
XCTAssertEqual(attributedString.runs.count, expectedRuns)
for run in attributedString.runs where run.attachment != nil {
return
}
XCTFail("Couldn't find expected value.")
}
}