From 56898cf7566976cace4929206b9aa34a460c248a Mon Sep 17 00:00:00 2001 From: Rasmus Tauts Date: Thu, 3 Aug 2023 22:04:22 +0300 Subject: [PATCH] Cover GVA persistent button with snapshot tests This PR covers Persistent button with VoiceOver and Layout snapshot tests. However, due to complications, dynamic font snapshot tests are not working as expected and will be postponed until later in order to maximise progress with the project. Rest assured, dynamic font in attributed string works well in simulator and in real device. This PR also utilizes textstyle as a key component in making attributed string scale with dynamic font. Because of that, no default value is provided in the styles for textStyle anymore. MetadataWrapper is necessary for making proper metadata json, as the nature of the Metadata type is somewhat restrictive. MOB-2375 --- GliaWidgets.xcodeproj/project.pbxproj | 4 +- GliaWidgets/Sources/Theme/Theme+Chat.swift | 6 ++ GliaWidgets/Sources/Theme/Theme+Gva.swift | 7 +- GliaWidgets/Sources/View/Chat/ChatView.swift | 2 +- .../GVA/GvaPersistentButtonOptionView.swift | 26 +++---- .../Chat/GVA/GvaPersistentButtonStyle.swift | 36 ++++++++- .../Message/Content/ChatMessageContent.swift | 2 +- .../ChoiceCardOptionStateStyle.swift | 2 + .../Content/Text/ChatTextContentStyle.swift | 6 +- .../Content/Text/ChatTextContentView.swift | 19 +++-- .../Chat/ChatViewController.Mock.swift | 76 +++++++++++++++++++ .../Chat/Data/ChatMessage.Mock.swift | 6 +- .../Sources/ViewModel/Chat/Data/Gva.swift | 64 +++++++++++----- .../ChatMessage/ChatMessageTests.swift | 24 ------ GliaWidgetsTests/CoreSdk/CoreSdk.swift | 4 +- .../ChatViewControllerLayoutTests.swift | 10 +++ .../ChatViewControllerVoiceOverTests.swift | 10 +++ 17 files changed, 219 insertions(+), 85 deletions(-) diff --git a/GliaWidgets.xcodeproj/project.pbxproj b/GliaWidgets.xcodeproj/project.pbxproj index 12faccb2a..357b1cf65 100644 --- a/GliaWidgets.xcodeproj/project.pbxproj +++ b/GliaWidgets.xcodeproj/project.pbxproj @@ -229,7 +229,6 @@ 7552DFA72A683A2C0093519B /* NSLayoutConstraint+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75940957298D386F008B173A /* NSLayoutConstraint+Extensions.swift */; }; 7552DFA82A683A2C0093519B /* UIStackView.Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75940956298D386F008B173A /* UIStackView.Extensions.swift */; }; 7552DFB12A6FB7DF0093519B /* ChatMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7552DFB02A6FB7DF0093519B /* ChatMessageTests.swift */; }; - 7552DFB42A6FBC7F0093519B /* CoreSdk.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7552DFB32A6FBC7F0093519B /* CoreSdk.swift */; }; 755D186529A6A4E20009F5E8 /* WelcomeStyle+TitleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 755D186429A6A4E20009F5E8 /* WelcomeStyle+TitleStyle.swift */; }; 755D186729A6A4FA0009F5E8 /* WelcomeStyle+SubtitleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 755D186629A6A4FA0009F5E8 /* WelcomeStyle+SubtitleStyle.swift */; }; 755D186929A6A5270009F5E8 /* WelcomeStyle+CheckMessagesButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 755D186829A6A5270009F5E8 /* WelcomeStyle+CheckMessagesButtonStyle.swift */; }; @@ -299,6 +298,7 @@ 75CF8D9129C3A85C00CB1524 /* SecureConversationsWelcomeScreenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75CF8D9029C3A85C00CB1524 /* SecureConversationsWelcomeScreenTests.swift */; }; 75CF8DAD29C8F2B500CB1524 /* SecureConversationsConfirmationScreenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75CF8DAC29C8F2B500CB1524 /* SecureConversationsConfirmationScreenTests.swift */; }; 75F58EE127E7D5300065BA2D /* Survey.ViewController.Props.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75F58EE027E7D5300065BA2D /* Survey.ViewController.Props.swift */; }; + 75FD003F2A80E8C5002DC458 /* CoreSdk.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7552DFB32A6FBC7F0093519B /* CoreSdk.swift */; }; 75FF151427F3A2D600FE7BE2 /* Theme+Survey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75FF151327F3A2D600FE7BE2 /* Theme+Survey.swift */; }; 75FF151727F4E13900FE7BE2 /* Theme.Survey.BooleanQuestion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75FF151627F4E13900FE7BE2 /* Theme.Survey.BooleanQuestion.swift */; }; 75FF151B27F4F52D00FE7BE2 /* Theme.Survey.SingleQuestion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75FF151A27F4F52D00FE7BE2 /* Theme.Survey.SingleQuestion.swift */; }; @@ -4478,6 +4478,7 @@ 754CC61227E2767A005676E9 /* Survey.BooleanQuestionView.swift in Sources */, 755D187529A6A6B70009F5E8 /* WelcomeStyle+SendButton.swift in Sources */, 6EA3516926E139DA00BF5941 /* GliaViewTransitionController.swift in Sources */, + 75FD003F2A80E8C5002DC458 /* CoreSdk.swift in Sources */, C0D2F07929A4E3DF00803B47 /* UserImageView.Mock.swift in Sources */, 84265E6B29912E2100D65842 /* RemoteConfiguration+CallVisualizer.swift in Sources */, 1A1E30C725F9FDAB00850E68 /* ChatImageFileContentView.swift in Sources */, @@ -4554,7 +4555,6 @@ 84681A982A61853300DD7406 /* GvaOption.Mock.swift in Sources */, AF6AB34B2989517100003645 /* FileUploader.Failing.swift in Sources */, EB03B00E27FFF6DD0058F6B1 /* CallViewTests.swift in Sources */, - 7552DFB42A6FBC7F0093519B /* CoreSdk.swift in Sources */, 3197F7AD29E6A5C8008EE9F7 /* SecureConversations.FileUploadListView.Mock.swift in Sources */, AF29810929E045CE0005BD55 /* TranscriptModelTests.swift in Sources */, 7512A57727BE8A6700319DF1 /* InteractorTests.swift in Sources */, diff --git a/GliaWidgets/Sources/Theme/Theme+Chat.swift b/GliaWidgets/Sources/Theme/Theme+Chat.swift index f6e325b82..5e3730be5 100644 --- a/GliaWidgets/Sources/Theme/Theme+Chat.swift +++ b/GliaWidgets/Sources/Theme/Theme+Chat.swift @@ -153,6 +153,7 @@ extension Theme { let visitorText = ChatTextContentStyle( textFont: font.bodyText, textColor: color.baseLight, + textStyle: .body, backgroundColor: color.primary, accessibility: .init(isFontScalingEnabled: true) ) @@ -177,6 +178,7 @@ extension Theme { let operatorText = ChatTextContentStyle( textFont: font.bodyText, textColor: color.baseDark, + textStyle: .body, backgroundColor: Color.lightGrey, accessibility: .init(isFontScalingEnabled: true) ) @@ -201,6 +203,7 @@ extension Theme { let choiceCardText = ChatTextContentStyle( textFont: font.bodyText, textColor: color.baseDark, + textStyle: .body, backgroundColor: color.baseLight, accessibility: .init(isFontScalingEnabled: true) ) @@ -215,6 +218,7 @@ extension Theme { let choiceCardOptionNormalState = ChoiceCardOptionStateStyle( textFont: font.bodyText, textColor: color.baseDark, + textStyle: .body, backgroundColor: Color.lightGrey, borderColor: nil, accessibility: .init( @@ -225,6 +229,7 @@ extension Theme { let choiceCardOptionSelectedState = ChoiceCardOptionStateStyle( textFont: font.bodyText, textColor: color.baseLight, + textStyle: .body, backgroundColor: color.primary, borderColor: nil, accessibility: .init( @@ -235,6 +240,7 @@ extension Theme { let choiceCardOptionDisabledState = ChoiceCardOptionStateStyle( textFont: font.bodyText, textColor: Color.grey, + textStyle: .body, backgroundColor: Color.lightGrey, borderColor: Color.baseShade, accessibility: .init( diff --git a/GliaWidgets/Sources/Theme/Theme+Gva.swift b/GliaWidgets/Sources/Theme/Theme+Gva.swift index 32ee6df3e..f1b4ffd99 100644 --- a/GliaWidgets/Sources/Theme/Theme+Gva.swift +++ b/GliaWidgets/Sources/Theme/Theme+Gva.swift @@ -8,7 +8,9 @@ extension Theme { title: .init( textFont: font.bodyText, textColor: .black, - backgroundColor: .clear + textStyle: .body, + backgroundColor: .clear, + accessibility: .init(isFontScalingEnabled: true) ), backgroundColor: .fill(color: color.lightGrey), cornerRadius: 10, @@ -20,7 +22,8 @@ extension Theme { backgroundColor: .fill(color: color.background), cornerRadius: 5, borderColor: .clear, - borderWidth: 0 + borderWidth: 0, + accessibility: .init(isFontScalingEnabled: true) ) ) diff --git a/GliaWidgets/Sources/View/Chat/ChatView.swift b/GliaWidgets/Sources/View/Chat/ChatView.swift index 879d261d4..529bae1b7 100644 --- a/GliaWidgets/Sources/View/Chat/ChatView.swift +++ b/GliaWidgets/Sources/View/Chat/ChatView.swift @@ -863,7 +863,7 @@ extension ChatView { private func gvaResponseTextView( _ message: ChatMessage, - text: NSAttributedString, + text: NSMutableAttributedString, showImage: Bool, imageUrl: String? ) -> GvaResponseTextView { diff --git a/GliaWidgets/Sources/View/Chat/GVA/GvaPersistentButtonOptionView.swift b/GliaWidgets/Sources/View/Chat/GVA/GvaPersistentButtonOptionView.swift index ef81289b7..8c46db412 100644 --- a/GliaWidgets/Sources/View/Chat/GVA/GvaPersistentButtonOptionView.swift +++ b/GliaWidgets/Sources/View/Chat/GVA/GvaPersistentButtonOptionView.swift @@ -27,6 +27,9 @@ class GvaPersistentButtonOptionView: BaseView { override func setup() { super.setup() + isAccessibilityElement = true + accessibilityLabel = text + accessibilityTraits = .button layer.cornerRadius = style.cornerRadius layer.borderWidth = style.borderWidth layer.borderColor = style.borderColor.cgColor @@ -38,6 +41,11 @@ class GvaPersistentButtonOptionView: BaseView { textLabel.numberOfLines = 0 textLabel.isAccessibilityElement = false + setFontScalingEnabled( + style.accessibility.isFontScalingEnabled, + for: textLabel + ) + choiceButton.addTarget(self, action: #selector(onTap), for: .touchUpInside) } @@ -45,7 +53,7 @@ class GvaPersistentButtonOptionView: BaseView { super.defineLayout() var constraints = [NSLayoutConstraint](); defer { constraints.activate() } - heightAnchor.constraint(equalToConstant: Self.height).isActive = true + heightAnchor.constraint(greaterThanOrEqualToConstant: 42).isActive = true addSubview(textLabel) textLabel.translatesAutoresizingMaskIntoConstraints = false constraints += textLabel.layoutInSuperview(insets: viewInsets) @@ -67,22 +75,6 @@ class GvaPersistentButtonOptionView: BaseView { } } - private func applyStyle(_ style: ChoiceCardOptionStateStyle) { - setFontScalingEnabled( - style.accessibility.isFontScalingEnabled, - for: textLabel - ) - - UIView.transition(with: textLabel, duration: 0.2, options: .transitionCrossDissolve) { - self.layer.backgroundColor = style.backgroundColor.cgColor - self.textLabel.textColor = style.textColor - if let borderColor = style.borderColor { - self.layer.borderColor = borderColor.cgColor - self.layer.borderWidth = style.borderWidth - } - } - } - @objc private func onTap() { tap?() } diff --git a/GliaWidgets/Sources/View/Chat/GVA/GvaPersistentButtonStyle.swift b/GliaWidgets/Sources/View/Chat/GVA/GvaPersistentButtonStyle.swift index cb127e962..ecde87d78 100644 --- a/GliaWidgets/Sources/View/Chat/GVA/GvaPersistentButtonStyle.swift +++ b/GliaWidgets/Sources/View/Chat/GVA/GvaPersistentButtonStyle.swift @@ -112,6 +112,9 @@ extension GvaPersistentButtonStyle { /// Border width of the button public var borderWidth: CGFloat + /// Accessibility + public var accessibility: Accessibility + init( textFont: UIFont, textColor: UIColor, @@ -119,7 +122,8 @@ extension GvaPersistentButtonStyle { backgroundColor: ColorType, cornerRadius: CGFloat, borderColor: UIColor, - borderWidth: CGFloat + borderWidth: CGFloat, + accessibility: Accessibility = .unsupported ) { self.textFont = textFont self.textColor = textColor @@ -128,6 +132,7 @@ extension GvaPersistentButtonStyle { self.cornerRadius = cornerRadius self.borderColor = borderColor self.borderWidth = borderWidth + self.accessibility = accessibility } mutating func apply( @@ -180,3 +185,32 @@ extension GvaPersistentButtonStyle { } } } + +extension GvaPersistentButtonStyle { + /// Accessibility properties for ChoiceCardOptionStateStyle. + public struct Accessibility: Equatable { + /// Accessibility value. + public var value: String + + /// Flag that provides font dynamic type by setting `adjustsFontForContentSizeCategory` for component that supports it. + public var isFontScalingEnabled: Bool + + /// + /// - Parameters: + /// - value: Accessibility value. + /// - isFontScalingEnabled: Flag that provides font dynamic type by setting `adjustsFontForContentSizeCategory` for component that supports it. + public init( + value: String = "", + isFontScalingEnabled: Bool + ) { + self.value = value + self.isFontScalingEnabled = isFontScalingEnabled + } + + /// Accessibility is not supported intentionally. + public static let unsupported = Self( + value: "", + isFontScalingEnabled: false + ) + } +} diff --git a/GliaWidgets/Sources/View/Chat/Message/Content/ChatMessageContent.swift b/GliaWidgets/Sources/View/Chat/Message/Content/ChatMessageContent.swift index e65386800..49d28c769 100644 --- a/GliaWidgets/Sources/View/Chat/Message/Content/ChatMessageContent.swift +++ b/GliaWidgets/Sources/View/Chat/Message/Content/ChatMessageContent.swift @@ -6,7 +6,7 @@ enum ChatMessageContent { case downloads([FileDownload], accessibility: ChatFileContentView.AccessibilityProperties) case choiceCard(ChoiceCard) case gvaPersistentButton(GvaButton) - case attributedText(NSAttributedString, accessibility: TextAccessibilityProperties) + case attributedText(NSMutableAttributedString, accessibility: TextAccessibilityProperties) struct TextAccessibilityProperties { let label: String diff --git a/GliaWidgets/Sources/View/Chat/Message/Content/ChoiceCard/ChoiceCardOptionStateStyle.swift b/GliaWidgets/Sources/View/Chat/Message/Content/ChoiceCard/ChoiceCardOptionStateStyle.swift index cd5598e15..c2d9495d3 100644 --- a/GliaWidgets/Sources/View/Chat/Message/Content/ChoiceCard/ChoiceCardOptionStateStyle.swift +++ b/GliaWidgets/Sources/View/Chat/Message/Content/ChoiceCard/ChoiceCardOptionStateStyle.swift @@ -18,6 +18,7 @@ public final class ChoiceCardOptionStateStyle: ChatTextContentStyle { public init( textFont: UIFont, textColor: UIColor, + textStyle: UIFont.TextStyle, backgroundColor: UIColor, borderColor: UIColor?, borderWidth: CGFloat = 1, @@ -28,6 +29,7 @@ public final class ChoiceCardOptionStateStyle: ChatTextContentStyle { super.init( textFont: textFont, textColor: textColor, + textStyle: textStyle, backgroundColor: backgroundColor, accessibility: accessibility ) diff --git a/GliaWidgets/Sources/View/Chat/Message/Content/Text/ChatTextContentStyle.swift b/GliaWidgets/Sources/View/Chat/Message/Content/Text/ChatTextContentStyle.swift index 931936652..3d105f4c4 100644 --- a/GliaWidgets/Sources/View/Chat/Message/Content/Text/ChatTextContentStyle.swift +++ b/GliaWidgets/Sources/View/Chat/Message/Content/Text/ChatTextContentStyle.swift @@ -24,17 +24,17 @@ public class ChatTextContentStyle { /// - Parameters: /// - textFont: Font of the message text. /// - textColor: Color of the message text. - /// - textStyle: Text style of the message text. + /// - textStyle: Text style of the message text. Necessary for attributed strings. /// - backgroundColor: Background color of the content view. /// - accessibility: Accessibility related properties. /// public init( textFont: UIFont, textColor: UIColor, - textStyle: UIFont.TextStyle = .body, + textStyle: UIFont.TextStyle, backgroundColor: UIColor, cornerRadius: CGFloat = 8.49, - accessibility: Accessibility = .unsupported + accessibility: Accessibility ) { self.textFont = textFont self.textColor = textColor diff --git a/GliaWidgets/Sources/View/Chat/Message/Content/Text/ChatTextContentView.swift b/GliaWidgets/Sources/View/Chat/Message/Content/Text/ChatTextContentView.swift index 2d9360aaf..335495147 100644 --- a/GliaWidgets/Sources/View/Chat/Message/Content/Text/ChatTextContentView.swift +++ b/GliaWidgets/Sources/View/Chat/Message/Content/Text/ChatTextContentView.swift @@ -6,8 +6,8 @@ class ChatTextContentView: BaseView { set { setText(newValue) } } - var attributedText: NSAttributedString? { - get { return textView.attributedText } + var attributedText: NSMutableAttributedString? { + get { return textView.attributedText as? NSMutableAttributedString } set { return setAttributedText(newValue) } } @@ -68,7 +68,6 @@ class ChatTextContentView: BaseView { textView.font = style.textFont textView.backgroundColor = .clear textView.textColor = style.textColor - textView.isAccessibilityElement = false setFontScalingEnabled( style.accessibility.isFontScalingEnabled, @@ -110,7 +109,7 @@ class ChatTextContentView: BaseView { textView.accessibilityIdentifier = text } - private func setAttributedText(_ text: NSAttributedString?) { + private func setAttributedText(_ text: NSMutableAttributedString?) { guard let text, !text.string.isEmpty else { textView.removeFromSuperview() return @@ -124,20 +123,19 @@ class ChatTextContentView: BaseView { } let attributes: [NSAttributedString.Key: Any] = [ - .font: style.textFont, + .font: UIFont.preferredFont(forTextStyle: style.textStyle), .foregroundColor: style.textColor ] - let attributedText = NSMutableAttributedString(attributedString: text) - attributedText.addAttributes( + text.addAttributes( attributes, range: NSRange( location: 0, - length: attributedText.length + length: text.length ) ) - textView.attributedText = attributedText + textView.attributedText = text textView.accessibilityIdentifier = text.string } } @@ -168,8 +166,9 @@ extension ChatTextContentView { with: ChatTextContentStyle( textFont: .systemFont(ofSize: 10), textColor: .black, + textStyle: .body, backgroundColor: .black, - accessibility: .unsupported + accessibility: .init(isFontScalingEnabled: true) ), contentAlignment: .left, insets: .zero diff --git a/GliaWidgets/Sources/ViewController/Chat/ChatViewController.Mock.swift b/GliaWidgets/Sources/ViewController/Chat/ChatViewController.Mock.swift index 7508e902d..d580e1c9f 100644 --- a/GliaWidgets/Sources/ViewController/Chat/ChatViewController.Mock.swift +++ b/GliaWidgets/Sources/ViewController/Chat/ChatViewController.Mock.swift @@ -1,6 +1,7 @@ #if DEBUG import Foundation import UIKit +import GliaCoreSDK // swiftlint:disable function_body_length extension ChatViewController { @@ -366,6 +367,55 @@ extension ChatViewController { return controller } + // MARK: - Glia Virtual Assistant Persistent Button State + + static func mockGvaPersistentButton() throws -> ChatViewController { + var chatViewModelEnv = ChatViewModel.Environment.mock + chatViewModelEnv.fileManager.urlsForDirectoryInDomainMask = { _, _ in [URL.mock] } + chatViewModelEnv.loadChatMessagesFromHistory = { true } + + let messageUuid = UUID.incrementing + let messageId = { messageUuid().uuidString } + let queueId = UUID.mock.uuidString + + let jsonData = mockGvaPersistentButtonJson() ?? Data() + let metadataContainer = try CoreSdkMessageMetadataContainer(jsonData: jsonData, jsonDecoder: .init()) + let metadata = Message.Metadata(container: metadataContainer.container) + + let messages: [ChatMessage] = [ + .mock( + id: messageId(), + queueID: queueId, + operator: .mock( + name: "Rasmus", + pictureUrl: "https://mock.mock/single_choice/567/image.png" + ), + sender: .operator, + content: "", + attachment: nil, + downloads: [], + metadata: metadata + ) + ] + + chatViewModelEnv.fetchChatHistory = { $0(.success(messages)) } + + var viewFactoryEnv = ViewFactory.Environment.mock + viewFactoryEnv.imageViewCache.getImageForKey = { _ in UIImage.mock } + + let chatViewModel = ChatViewModel.mock(environment: chatViewModelEnv) + let controller = ChatViewController.mock( + chatViewModel: chatViewModel, + viewFactory: .init( + with: .mock(), + messageRenderer: .mock, + environment: viewFactoryEnv + ) + ) + chatViewModel.action?(.setMessageText("Input Message Mock")) + return controller + } + // MARK: - Visitor File Download States static func mockVisitorFileDownloadStates(completion: ([ChatMessage]) -> Void) throws -> ChatViewController { var chatViewModelEnv = ChatViewModel.Environment.mock @@ -413,6 +463,32 @@ extension ChatViewController { completion(messages) return controller } + + static private func mockGvaPersistentButtonJson() -> Data? { + """ + { + "metadata": + { + "type" : "persistentButtons", + "content" : "This is a Glia Virutal Assistant Persistent button.", + "options" : [ + { + "value" : "I'm first button", + "text" : "First Button" + }, + { + "value" : "I'm second button", + "text" : "Second Button" + }, + { + "value" : "I'm third button", + "text" : "Third Button" + } + ] + } + } + """.data(using: .utf8) + } } // swiftlint:enable function_body_length #endif diff --git a/GliaWidgets/Sources/ViewModel/Chat/Data/ChatMessage.Mock.swift b/GliaWidgets/Sources/ViewModel/Chat/Data/ChatMessage.Mock.swift index 4380efbd4..6a1aa42ac 100644 --- a/GliaWidgets/Sources/ViewModel/Chat/Data/ChatMessage.Mock.swift +++ b/GliaWidgets/Sources/ViewModel/Chat/Data/ChatMessage.Mock.swift @@ -7,7 +7,8 @@ extension ChatMessage { sender: ChatMessageSender = .visitor, content: String = "", attachment: ChatAttachment? = nil, - downloads: [FileDownload] = [] + downloads: [FileDownload] = [], + metadata: MessageMetadata? = nil ) -> ChatMessage { .init( id: id, @@ -16,7 +17,8 @@ extension ChatMessage { sender: sender, content: content, attachment: attachment, - downloads: downloads + downloads: downloads, + metadata: metadata ) } } diff --git a/GliaWidgets/Sources/ViewModel/Chat/Data/Gva.swift b/GliaWidgets/Sources/ViewModel/Chat/Data/Gva.swift index dd50da910..cfa85ac8b 100644 --- a/GliaWidgets/Sources/ViewModel/Chat/Data/Gva.swift +++ b/GliaWidgets/Sources/ViewModel/Chat/Data/Gva.swift @@ -2,7 +2,7 @@ import Foundation struct GvaResponseText: Decodable, Equatable { let type: GvaCardType - let content: NSAttributedString + let content: NSMutableAttributedString enum CodingKeys: String, CodingKey { case type, content @@ -13,13 +13,13 @@ struct GvaResponseText: Decodable, Equatable { type = try container.decode(GvaCardType.self, forKey: .type) let contentString = try container.decode(String.self, forKey: .content) let modifiedString = contentString.replacingOccurrences(of: "\n", with: "
") - content = modifiedString.htmlToAttributedString ?? NSAttributedString(string: "") + content = modifiedString.htmlToAttributedString ?? NSMutableAttributedString(string: "") } } struct GvaButton: Decodable, Equatable { let type: GvaCardType - let content: NSAttributedString + let content: NSMutableAttributedString let options: [GvaOption] enum CodingKeys: String, CodingKey { @@ -31,9 +31,19 @@ struct GvaButton: Decodable, Equatable { type = try container.decode(GvaCardType.self, forKey: .type) let contentString = try container.decode(String.self, forKey: .content) let modifiedString = contentString.replacingOccurrences(of: "\n", with: "
") - content = modifiedString.htmlToAttributedString ?? NSAttributedString(string: "") + content = modifiedString.htmlToAttributedString ?? NSMutableAttributedString(string: "") options = try container.decode([GvaOption].self, forKey: .options) } + + init( + type: GvaCardType, + content: NSMutableAttributedString, + options: [GvaOption] + ) { + self.type = type + self.content = content + self.options = options + } } struct GvaGallery: Decodable, Equatable { @@ -48,7 +58,7 @@ struct GvaGalleryCard: Decodable, Equatable { let options: [GvaOption]? } -struct GvaOption: Decodable, Equatable { +struct GvaOption: Codable, Equatable { let text: String let value: String? let url: String? @@ -68,7 +78,7 @@ enum GvaUrlTarget: String, Decodable { } } -enum GvaCardType: String, Decodable { +enum GvaCardType: String, Codable { case persistentButtons case quickReplies case plainText @@ -76,7 +86,7 @@ enum GvaCardType: String, Decodable { } private extension StringProtocol { - var htmlToAttributedString: NSAttributedString? { + var htmlToAttributedString: NSMutableAttributedString? { Data(utf8).htmlToAttributedString } var htmlToString: String { @@ -85,21 +95,33 @@ private extension StringProtocol { } private extension Data { - var htmlToAttributedString: NSAttributedString? { - do { - let options: [NSAttributedString.DocumentReadingOptionKey: Any] = [ - .documentType: NSAttributedString.DocumentType.html, - .characterEncoding: String.Encoding.utf8.rawValue - ] - return try NSAttributedString( - data: self, - options: options, - documentAttributes: nil - ) - } catch { - debugPrint("HTML-string decoding failed with error:", error) - return nil + var htmlToAttributedString: NSMutableAttributedString? { + if let string = String(data: self, encoding: .utf8), containsHtml(string) { + do { + let options: [NSAttributedString.DocumentReadingOptionKey: Any] = [ + .documentType: NSAttributedString.DocumentType.html, + .characterEncoding: String.Encoding.utf8.rawValue + ] + return try NSMutableAttributedString( + data: self, + options: options, + documentAttributes: nil + ) + } catch { + debugPrint("HTML-string decoding failed with error:", error) + return NSMutableAttributedString(string: string) + } + } else { + return NSMutableAttributedString(string: String(data: self, encoding: .utf8) ?? "") + } + } + + func containsHtml(_ string: String) -> Bool { + if let _ = string.range(of: "<[^>]+>", options: .regularExpression) { + return true } + return false } + var htmlToString: String { htmlToAttributedString?.string ?? "" } } diff --git a/GliaWidgetsTests/ChatMessage/ChatMessageTests.swift b/GliaWidgetsTests/ChatMessage/ChatMessageTests.swift index 6c2d56cba..7712578e2 100644 --- a/GliaWidgetsTests/ChatMessage/ChatMessageTests.swift +++ b/GliaWidgetsTests/ChatMessage/ChatMessageTests.swift @@ -24,27 +24,3 @@ final class ChatMessageTests: XCTestCase { XCTAssertEqual(msg.cardType, .customCard) } } - -extension ChatMessage { - static func mock( - id: String = "mocked-message-id", - queueId: String? = "queue-id", - operator: ChatOperator? = .init(name: "XCTest Operator", pictureUrl: nil), - sender: ChatMessageSender = .`operator`, - content: String = "Hello unit test!", - attachment: ChatAttachment? = nil, - downloads: [FileDownload] = [], - metadata: MessageMetadata? = nil - ) -> ChatMessage { - ChatMessage( - id: id, - queueID: queueId, - operator: `operator`, - sender: sender, - content: content, - attachment: attachment, - downloads: downloads, - metadata: metadata - ) - } -} diff --git a/GliaWidgetsTests/CoreSdk/CoreSdk.swift b/GliaWidgetsTests/CoreSdk/CoreSdk.swift index f576558d7..089332b9c 100644 --- a/GliaWidgetsTests/CoreSdk/CoreSdk.swift +++ b/GliaWidgetsTests/CoreSdk/CoreSdk.swift @@ -8,7 +8,9 @@ struct CoreSdkMessageMetadataContainer: Decodable { let container: KeyedDecodingContainer init(from decoder: Decoder) throws { - self.container = try decoder.container(keyedBy: GliaCoreSDK.Message.Metadata.CodingKeys.self) + self.container = try decoder.container( + keyedBy: GliaCoreSDK.Message.Metadata.CodingKeys.self + ) } /// Creates instance with decoding container inside. diff --git a/SnapshotTests/ChatViewControllerLayoutTests.swift b/SnapshotTests/ChatViewControllerLayoutTests.swift index 75aedd062..ad1eb3c77 100644 --- a/SnapshotTests/ChatViewControllerLayoutTests.swift +++ b/SnapshotTests/ChatViewControllerLayoutTests.swift @@ -33,6 +33,16 @@ class ChatViewControllerLayoutTests: SnapshotTestCase { ) } + func test_gvaPersistentButton() throws { + let viewController = try ChatViewController.mockGvaPersistentButton() + viewController.view.frame = UIScreen.main.bounds + assertSnapshot( + matching: viewController, + as: .image, + named: nameForDevice() + ) + } + func test_visitorFileDownloadStates() throws { var chatMessages: [ChatMessage] = [] let viewController = try ChatViewController.mockVisitorFileDownloadStates { messages in diff --git a/SnapshotTests/ChatViewControllerVoiceOverTests.swift b/SnapshotTests/ChatViewControllerVoiceOverTests.swift index b3ff42f1b..ef1e73673 100644 --- a/SnapshotTests/ChatViewControllerVoiceOverTests.swift +++ b/SnapshotTests/ChatViewControllerVoiceOverTests.swift @@ -53,4 +53,14 @@ class ChatViewControllerVoiceOverTests: SnapshotTestCase { named: self.nameForDevice() ) } + + func test_gvaPersistentButton() throws { + let viewController = try ChatViewController.mockGvaPersistentButton() + viewController.view.frame = UIScreen.main.bounds + assertSnapshot( + matching: viewController, + as: .accessibilityImage(precision: Self.possiblePrecision), + named: nameForDevice() + ) + } }