From 9db1dda71806ff627dc6e1048e4882cb7dbd0f2e Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Wed, 30 Mar 2022 14:32:41 +0300 Subject: [PATCH] Added AttributedStringBuilder tests. --- ElementX.xcodeproj/project.pbxproj | 14 +- .../xcshareddata/xcschemes/ElementX.xcscheme | 99 +++++++ .../HTMLParsing/AttributedStringBuilder.swift | 31 ++- .../Sources/Other/MatrixEntitityRegex.swift | 3 - .../AttributedStringBuilderTests.swift | 260 +++++++++++++++++- ElementXTests/ElementXTests.swift | 16 -- 6 files changed, 387 insertions(+), 36 deletions(-) create mode 100644 ElementX.xcodeproj/xcshareddata/xcschemes/ElementX.xcscheme diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index a57626a6e..c7b139eb6 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -27,6 +27,9 @@ 18920C1627F2E3F400A717B5 /* NoticeRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18920C1427F2E3F400A717B5 /* NoticeRoomTimelineView.swift */; }; 18920C1727F2E3F400A717B5 /* EmoteRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18920C1527F2E3F400A717B5 /* EmoteRoomTimelineView.swift */; }; 18920C1927F3222E00A717B5 /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18920C1827F3222E00A717B5 /* Colors.swift */; }; + 189E495227F43BCD00D86BA3 /* ElementXAttributeScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18DDB72027EB9D57000F1ABF /* ElementXAttributeScope.swift */; }; + 189E495327F474A200D86BA3 /* RoomScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18F2BAC127D25B4000DD1988 /* RoomScreenUITests.swift */; }; + 189E495427F474A500D86BA3 /* RoomScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18F2BAC327D25B4000DD1988 /* RoomScreenViewModelTests.swift */; }; 18ADC7D527E4B20300A8C953 /* PlaceholderAvatarImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18ADC7D427E4B20300A8C953 /* PlaceholderAvatarImage.swift */; }; 18ADC7D827E4B63C00A8C953 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 18ADC7D727E4B63C00A8C953 /* MatrixRustSDK */; }; 18ADC7FA27EB02D900A8C953 /* AttributedStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18ADC7F927EB02D900A8C953 /* AttributedStringBuilder.swift */; }; @@ -113,8 +116,6 @@ 18F2BB1A27D25BE800DD1988 /* RoomTimelineController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18F2BB1927D25BE800DD1988 /* RoomTimelineController.swift */; }; 18F2BB1D27D25D1C00DD1988 /* LoginScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18F2BAD127D25B4000DD1988 /* LoginScreenViewModelTests.swift */; }; 18F2BB1E27D25D2200DD1988 /* HomeScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18F2BAA927D25B4000DD1988 /* HomeScreenViewModelTests.swift */; }; - 18F2BB1F27D25D3100DD1988 /* RoomScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18F2BAC327D25B4000DD1988 /* RoomScreenViewModelTests.swift */; }; - 18F2BB2027D25D3400DD1988 /* RoomScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18F2BAC127D25B4000DD1988 /* RoomScreenUITests.swift */; }; 18F2BB2127D25D3C00DD1988 /* HomeScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18F2BAA727D25B4000DD1988 /* HomeScreenUITests.swift */; }; 18F2BB2227D25D4600DD1988 /* LoginScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18F2BACF27D25B4000DD1988 /* LoginScreenUITests.swift */; }; 18F2BB2827D2647A00DD1988 /* MockRoomTimelineController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18F2BB2727D2647A00DD1988 /* MockRoomTimelineController.swift */; }; @@ -501,15 +502,15 @@ 18F2BA7D27D25B4000DD1988 /* Other */ = { isa = PBXGroup; children = ( + 18920C1827F3222E00A717B5 /* Colors.swift */, 18F2BA9A27D25B4000DD1988 /* Coordinator.swift */, - 18F2BA9427D25B4000DD1988 /* MXLog.swift */, 18DDB72D27EF936D000F1ABF /* MatrixEntitityRegex.swift */, + 18F2BA9427D25B4000DD1988 /* MXLog.swift */, 18F2BA8727D25B4000DD1988 /* Activity */, 18ADC7F827EB02D900A8C953 /* HTMLParsing */, 18F2BA7E27D25B4000DD1988 /* Routers */, 18F2BA9B27D25B4000DD1988 /* SwiftUI */, 18F2BA9527D25B4000DD1988 /* WeakDictionary */, - 18920C1827F3222E00A717B5 /* Colors.swift */, ); path = Other; sourceTree = ""; @@ -1018,11 +1019,12 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 18F2BB2027D25D3400DD1988 /* RoomScreenUITests.swift in Sources */, + 189E495227F43BCD00D86BA3 /* ElementXAttributeScope.swift in Sources */, 18DDB73227F19F82000F1ABF /* AttributedStringBuilderTests.swift in Sources */, 18F2BB1E27D25D2200DD1988 /* HomeScreenViewModelTests.swift in Sources */, 18F2BB1D27D25D1C00DD1988 /* LoginScreenViewModelTests.swift in Sources */, 1850253F27B6918D002E6B18 /* ElementXTests.swift in Sources */, + 189E495427F474A500D86BA3 /* RoomScreenViewModelTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1032,9 +1034,9 @@ files = ( 18F2BB2127D25D3C00DD1988 /* HomeScreenUITests.swift in Sources */, 1850254B27B6918D002E6B18 /* ElementXUITestsLaunchTests.swift in Sources */, + 189E495327F474A200D86BA3 /* RoomScreenUITests.swift in Sources */, 1850254927B6918D002E6B18 /* ElementXUITests.swift in Sources */, 18F2BB2227D25D4600DD1988 /* LoginScreenUITests.swift in Sources */, - 18F2BB1F27D25D3100DD1988 /* RoomScreenViewModelTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ElementX.xcodeproj/xcshareddata/xcschemes/ElementX.xcscheme b/ElementX.xcodeproj/xcshareddata/xcschemes/ElementX.xcscheme new file mode 100644 index 000000000..f3ba4bd42 --- /dev/null +++ b/ElementX.xcodeproj/xcshareddata/xcschemes/ElementX.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilder.swift b/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilder.swift index aaa510771..e6e0285f2 100644 --- a/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilder.swift +++ b/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilder.swift @@ -18,7 +18,6 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol { private let userIdDetector: NSRegularExpression private let roomIdDetector: NSRegularExpression private let eventIdDetector: NSRegularExpression - private let groupIdDetector: NSRegularExpression private let roomAliasDetector: NSRegularExpression private let linkDetector: NSDataDetector @@ -27,7 +26,6 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol { userIdDetector = try NSRegularExpression(pattern: MatrixEntityRegex.userId.rawValue, options: .caseInsensitive) roomIdDetector = try NSRegularExpression(pattern: MatrixEntityRegex.roomId.rawValue, options: .caseInsensitive) eventIdDetector = try NSRegularExpression(pattern: MatrixEntityRegex.eventId.rawValue, options: .caseInsensitive) - groupIdDetector = try NSRegularExpression(pattern: MatrixEntityRegex.groupId.rawValue, options: .caseInsensitive) roomAliasDetector = try NSRegularExpression(pattern: MatrixEntityRegex.roomAlias.rawValue, options: .caseInsensitive) linkDetector = try NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) } catch { @@ -67,7 +65,6 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol { DTDefaultFontFamily: defaultFont.familyName, DTDefaultFontName: defaultFont.fontName, DTDefaultFontSize: defaultFont.pointSize, - DTDefaultLinkDecoration: false, DTDefaultStyleSheet: DTCSSStylesheet(styleBlock: self.defaultCSS) as Any ] @@ -84,6 +81,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol { } let mutableAttributedString = NSMutableAttributedString(attributedString: attributedString) + removeDefaultForegroundColor(mutableAttributedString) addLinks(mutableAttributedString) removeLinkColors(mutableAttributedString) removeDTCoreTextArtifacts(mutableAttributedString) @@ -161,7 +159,6 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol { var matches = userIdDetector.matches(in: string, options: [], range: range) matches.append(contentsOf: roomIdDetector.matches(in: string, options: [], range: range)) matches.append(contentsOf: eventIdDetector.matches(in: string, options: [], range: range)) - matches.append(contentsOf: groupIdDetector.matches(in: string, options: [], range: range)) matches.append(contentsOf: roomAliasDetector.matches(in: string, options: [], range: range)) matches.append(contentsOf: linkDetector.matches(in: string, options: [], range: range)) @@ -174,14 +171,36 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol { return } + var hasLink = false + attributedString.enumerateAttribute(.link, in: match.range, options: []) { value, _, stop in + if value != nil { + hasLink = true + stop.pointee = true + } + } + + if hasLink { + return + } + let link = string[matchRange].addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) attributedString.addAttribute(.link, value: link as Any, range: match.range) } } + private func removeDefaultForegroundColor(_ attributedString: NSMutableAttributedString) { + attributedString.enumerateAttribute(.foregroundColor, in: .init(location: 0, length: attributedString.length), options: []) { value, range, _ in + if value as? UIColor == UIColor.black { + attributedString.removeAttribute(.foregroundColor, range: range) + } + } + } + private func removeLinkColors(_ attributedString: NSMutableAttributedString) { - attributedString.enumerateAttribute(.link, in: .init(location: 0, length: attributedString.length), options: []) { _, range, _ in - attributedString.removeAttribute(.foregroundColor, range: range) + attributedString.enumerateAttribute(.link, in: .init(location: 0, length: attributedString.length), options: []) { value, range, _ in + if value != nil { + attributedString.removeAttribute(.foregroundColor, range: range) + } } } diff --git a/ElementX/Sources/Other/MatrixEntitityRegex.swift b/ElementX/Sources/Other/MatrixEntitityRegex.swift index f6909e261..e4d72d600 100644 --- a/ElementX/Sources/Other/MatrixEntitityRegex.swift +++ b/ElementX/Sources/Other/MatrixEntitityRegex.swift @@ -14,7 +14,6 @@ enum MatrixEntityRegex: String { case roomAlias case roomId case eventId - case groupId var rawValue: String { switch self { @@ -28,8 +27,6 @@ enum MatrixEntityRegex: String { return "![A-Z0-9]+:" + MatrixEntityRegex.homeserver.rawValue case .eventId: return "\\$[A-Z0-9]+:" + MatrixEntityRegex.homeserver.rawValue - case .groupId: - return "\\+[A-Z0-9=_\\-./]+:" + MatrixEntityRegex.homeserver.rawValue } } } diff --git a/ElementXTests/AttributedStringBuilderTests.swift b/ElementXTests/AttributedStringBuilderTests.swift index 0b4ac53ce..d3b12a966 100644 --- a/ElementXTests/AttributedStringBuilderTests.swift +++ b/ElementXTests/AttributedStringBuilderTests.swift @@ -11,11 +11,261 @@ import XCTest class AttributedStringBuilderTests: XCTestCase { - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. + let attributedStringBuilder = AttributedStringBuilder() + let maxHeaderPointSize = ceil(UIFont.preferredFont(forTextStyle: .body).pointSize * 1.2) + + func testRenderHTMLStringWithHeaders() { + let h1HTMLString = "

Large Heading

"; + let h2HTMLString = "

Smaller Heading

"; + let h3HTMLString = "

Acceptable Heading

"; + + guard let h1AttributedString = attributedStringBuilder.fromHTML(h1HTMLString), + let h2AttributedString = attributedStringBuilder.fromHTML(h2HTMLString), + let h3AttributedString = attributedStringBuilder.fromHTML(h3HTMLString) else { + XCTFail("Could not build the attributed string") + return + } + + XCTAssertEqual(String(h1AttributedString.characters), "Large Heading") + XCTAssertEqual(String(h2AttributedString.characters), "Smaller Heading") + XCTAssertEqual(String(h3AttributedString.characters), "Acceptable Heading") + + XCTAssert(h1AttributedString.runs.count == 1) + XCTAssert(h2AttributedString.runs.count == 1) + XCTAssert(h3AttributedString.runs.count == 1) + + let h1Font = h1AttributedString.runs.first?.uiKit.font + let h2Font = h2AttributedString.runs.first?.uiKit.font + let h3Font = h3AttributedString.runs.first?.uiKit.font + + XCTAssertEqual(h1Font, h2Font) + XCTAssertEqual(h2Font, h3Font) + + XCTAssert(h1Font!.pointSize > UIFont.preferredFont(forTextStyle: .body).pointSize) + + XCTAssert(h1Font!.pointSize <= maxHeaderPointSize) } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. + + func testRenderHTMLStringWithPreCode() { + let htmlString = "
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, "Menlo-Regular") + + let string = String(attributedString.characters) + let regex = try! NSRegularExpression(pattern: "\\R", options: []) + 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(where: { $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(where: { $0.link != nil })?.link + + XCTAssertEqual(link?.host, "www.matrix.org") + } + + func testRenderHTMLStringWithLinkInHeader() { + let h1HTMLString = "

Matrix.org

"; + let h2HTMLString = "

Matrix.org

"; + let h3HTMLString = "

Matrix.org

"; + + guard let h1AttributedString = attributedStringBuilder.fromHTML(h1HTMLString), + let h2AttributedString = attributedStringBuilder.fromHTML(h2HTMLString), + let h3AttributedString = attributedStringBuilder.fromHTML(h3HTMLString) else { + XCTFail("Could not build the attributed string") + return + } + + XCTAssertEqual(String(h1AttributedString.characters), "Matrix.org") + XCTAssertEqual(String(h2AttributedString.characters), "Matrix.org") + XCTAssertEqual(String(h3AttributedString.characters), "Matrix.org") + + XCTAssertEqual(h1AttributedString.runs.count, 1) + XCTAssertEqual(h2AttributedString.runs.count, 1) + XCTAssertEqual(h3AttributedString.runs.count, 1) + + let h1Font = h1AttributedString.runs.first?.uiKit.font + let h2Font = h2AttributedString.runs.first?.uiKit.font + let h3Font = h3AttributedString.runs.first?.uiKit.font + + XCTAssertEqual(h1Font, h2Font) + XCTAssertEqual(h2Font, h3Font) + + XCTAssert(h1Font!.pointSize > UIFont.preferredFont(forTextStyle: .body).pointSize) + XCTAssert(h1Font!.pointSize <= maxHeaderPointSize) + + XCTAssertEqual(h1AttributedString.runs.first?.link?.host, "www.matrix.org") + XCTAssertEqual(h2AttributedString.runs.first?.link?.host, "www.matrix.org") + XCTAssertEqual(h3AttributedString.runs.first?.link?.host, "www.matrix.org") + } + + func testRenderHTMLStringWithIFrame() { + let htmlString = ""; + + guard let attributedString = attributedStringBuilder.fromHTML(htmlString) else { + XCTFail("Could not build the attributed string") + return + } + + XCTAssertNil(attributedString.uiKit.attachment, "iFrame attachments should be removed as they're not included in the allowedHTMLTags array.") + } + + func testUserIdLink() { + let userId = "@user:matrix.org" + let string = "The user is \(userId)."; + checkMatrixEntityLinkIn(attributedString: attributedStringBuilder.fromHTML(string), expected: userId) + checkMatrixEntityLinkIn(attributedString: attributedStringBuilder.fromPlain(string), expected: userId) + } + + func testRoomAliasLink() { + let roomAlias = "#matrix:matrix.org" + let string = "The room alias is \(roomAlias)."; + checkMatrixEntityLinkIn(attributedString: attributedStringBuilder.fromHTML(string), expected: roomAlias) + checkMatrixEntityLinkIn(attributedString: attributedStringBuilder.fromPlain(string), expected: roomAlias) + } + + func testRoomIdLink() { + let roomId = "!roomidentifier:matrix.org" + let string = "The room is \(roomId)."; + checkMatrixEntityLinkIn(attributedString: attributedStringBuilder.fromHTML(string), expected: roomId) + checkMatrixEntityLinkIn(attributedString: attributedStringBuilder.fromPlain(string), expected: roomId) + } + + func testEventIdLink() { + let eventId = "$eventidentifier:matrix.org" + let string = "The event is \(eventId)."; + checkMatrixEntityLinkIn(attributedString: attributedStringBuilder.fromHTML(string), expected: eventId) + checkMatrixEntityLinkIn(attributedString: attributedStringBuilder.fromPlain(string), expected: eventId) + } + + func testDefaultFont() { + let htmlString = "Test string." + + guard let attributedString = attributedStringBuilder.fromHTML(htmlString) else { + XCTFail("Could not build the attributed string") + return + } + + XCTAssertEqual(attributedString.runs.count, 4) + + for run in attributedString.runs { + XCTAssertEqual(run.uiKit.font?.familyName, UIFont.preferredFont(forTextStyle: .body).familyName) + } + } + + func testDefaultForegroundColor() { + let htmlString = "Test string." + + guard let attributedString = attributedStringBuilder.fromHTML(htmlString) else { + XCTFail("Could not build the attributed string") + return + } + + XCTAssertEqual(attributedString.runs.count, 4) + + for run in attributedString.runs { + XCTAssertNil(run.uiKit.foregroundColor) + } + } + + func testCustomForegroundColor() { + 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, 8) + + var foundLink = false + for run in attributedString.runs { + if run.link != nil { + XCTAssertEqual(run.link?.path, "www.matrix.org") + XCTAssertNil(run.uiKit.foregroundColor) + foundLink = true + } else { + XCTAssertNotNil(run.uiKit.foregroundColor) + } + } + + XCTAssertTrue(foundLink) + } + + func testBlockquotes() { + 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) + + for run in attributedString.runs { + if run.elementX.blockquote != nil { + return + } + } + + XCTFail("Couldn't find blockquote") + + XCTAssertEqual(attributedStringBuilder.blockquoteCoalescedComponentsFrom(attributedString)?.count, 3) + } + + // MARK: - Private + + private func checkMatrixEntityLinkIn(attributedString: AttributedString?, expected: String) { + + guard let attributedString = attributedString else { + XCTFail("Could not build the attributed string") + return + } + + XCTAssertEqual(attributedString.runs.count, 3) + + for run in attributedString.runs { + if run.link != nil { + XCTAssertEqual(run.link?.path, expected) + return + } + } + + XCTFail("Couldn't find expected value.") } } diff --git a/ElementXTests/ElementXTests.swift b/ElementXTests/ElementXTests.swift index 6a9d07719..7464c9941 100644 --- a/ElementXTests/ElementXTests.swift +++ b/ElementXTests/ElementXTests.swift @@ -17,20 +17,4 @@ class ElementXTests: XCTestCase { override func tearDownWithError() throws { // Put teardown code here. This method is called after the invocation of each test method in the class. } - - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. - // Any test you write for XCTest can be annotated as throws and async. - // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. - // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. - } - - func testPerformanceExample() throws { - // This is an example of a performance test case. - self.measure { - // Put the code you want to measure the time of here. - } - } - }