From 46a41c1a884a78143978e06f2c41c401cf721d30 Mon Sep 17 00:00:00 2001 From: Yevhen Kyivskyi Date: Thu, 28 Nov 2024 16:44:53 +0100 Subject: [PATCH] Add Unit and Snapshot tests for top banner on Secure Conversations --- GliaWidgets.xcodeproj/project.pbxproj | 8 ++ ...onversations.ChatWithTranscriptModel.swift | 1 - .../Sources/EntryWidget/EntryWidget.swift | 9 +- .../EntryWidget/EntryWidgetViewModel.swift | 1 - GliaWidgets/Sources/View/Chat/ChatView.swift | 3 +- .../Chat/ChatViewController.Mock.swift | 22 +++++ ...sations.ChatWithTranscriptModelTests.swift | 15 ++++ .../EntryWidget/EntryWidget.Mock.swift | 26 ++++++ .../EntryWidget/EntryWidgetTests.swift | 83 +++++++++++++++++++ .../EntryWidgetViewModelTests.swift | 49 +++++++++++ .../ChatViewControllerLayoutTests.swift | 22 ++++- 11 files changed, 229 insertions(+), 10 deletions(-) create mode 100644 GliaWidgetsTests/Sources/EntryWidget/EntryWidget.Mock.swift create mode 100644 GliaWidgetsTests/Sources/EntryWidget/EntryWidgetViewModelTests.swift diff --git a/GliaWidgets.xcodeproj/project.pbxproj b/GliaWidgets.xcodeproj/project.pbxproj index e631d23d9..62b2625f8 100644 --- a/GliaWidgets.xcodeproj/project.pbxproj +++ b/GliaWidgets.xcodeproj/project.pbxproj @@ -180,6 +180,8 @@ 216D31052CF4D3590019CA9E /* ChatView.DefineLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 216D31042CF4D3590019CA9E /* ChatView.DefineLayout.swift */; }; 216D31072CF5DA050019CA9E /* ChatView.Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 216D31062CF5DA050019CA9E /* ChatView.Constants.swift */; }; 216D31092CF5DC080019CA9E /* OverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 216D31082CF5DC080019CA9E /* OverlayView.swift */; }; + 216D310B2CF790980019CA9E /* EntryWidget.Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 216D310A2CF790980019CA9E /* EntryWidget.Mock.swift */; }; + 216D310D2CF795380019CA9E /* EntryWidgetViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 216D310C2CF795380019CA9E /* EntryWidgetViewModelTests.swift */; }; 2188DED22CECC3D400FA3BEF /* SecureMessagingTopBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2188DED12CECC3D400FA3BEF /* SecureMessagingTopBannerView.swift */; }; 2188DED42CECE15800FA3BEF /* SecureMessagingTopBannerViewStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2188DED32CECE15800FA3BEF /* SecureMessagingTopBannerViewStyle.swift */; }; 2188DEDD2CEE2C3F00FA3BEF /* SecureMessagingBottomBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2188DED92CEE2C3F00FA3BEF /* SecureMessagingBottomBannerView.swift */; }; @@ -1264,6 +1266,8 @@ 216D31042CF4D3590019CA9E /* ChatView.DefineLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.DefineLayout.swift; sourceTree = ""; }; 216D31062CF5DA050019CA9E /* ChatView.Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.Constants.swift; sourceTree = ""; }; 216D31082CF5DC080019CA9E /* OverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayView.swift; sourceTree = ""; }; + 216D310A2CF790980019CA9E /* EntryWidget.Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntryWidget.Mock.swift; sourceTree = ""; }; + 216D310C2CF795380019CA9E /* EntryWidgetViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntryWidgetViewModelTests.swift; sourceTree = ""; }; 2188DED12CECC3D400FA3BEF /* SecureMessagingTopBannerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecureMessagingTopBannerView.swift; sourceTree = ""; }; 2188DED32CECE15800FA3BEF /* SecureMessagingTopBannerViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureMessagingTopBannerViewStyle.swift; sourceTree = ""; }; 2188DED92CEE2C3F00FA3BEF /* SecureMessagingBottomBannerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecureMessagingBottomBannerView.swift; sourceTree = ""; }; @@ -4812,6 +4816,8 @@ isa = PBXGroup; children = ( C06B6D4B2CCB917600A66D8A /* EntryWidgetTests.swift */, + 216D310C2CF795380019CA9E /* EntryWidgetViewModelTests.swift */, + 216D310A2CF790980019CA9E /* EntryWidget.Mock.swift */, ); path = EntryWidget; sourceTree = ""; @@ -6648,6 +6654,7 @@ 9ACC25D427B474E800BC5335 /* Glia.Environment.Failing.swift in Sources */, 9A8130C227D9095200220BBD /* FileDownload.Failing.swift in Sources */, 3189DD9429DEFAC600D68E9F /* SecureConversations.WelcomeViewModelSpec.swift in Sources */, + 216D310D2CF795380019CA9E /* EntryWidgetViewModelTests.swift in Sources */, AF4D821C29D6E572007763F8 /* TranscriptModel.DividedChatItemsForUnreadCountTests.swift in Sources */, 84520BF62B20936F00F97617 /* SnackBar.Failing.swift in Sources */, AF1C19802B14FE9F00F8810F /* ConditionalCompilationClient.Failing.swift in Sources */, @@ -6718,6 +6725,7 @@ 3115EFBA2BC960B500B24D5A /* (null) in Sources */, 31CCE3E62BCE8F3A00F92535 /* ScreenSharingCoordinatorTests.swift in Sources */, 846A5C3929D18D400049B29F /* ScreenShareHandlerTests.swift in Sources */, + 216D310B2CF790980019CA9E /* EntryWidget.Mock.swift in Sources */, 9AE05CB62805D2CB00871321 /* Interactor.Environment.Failing.swift in Sources */, 846429862A45DB4100943BD6 /* AlertViewController.Kind+Mock.swift in Sources */, AF29811229E42F3C0005BD55 /* AvailabilityTests.swift in Sources */, diff --git a/GliaWidgets/SecureConversations/ChatTranscript/SecureConversations.ChatWithTranscriptModel.swift b/GliaWidgets/SecureConversations/ChatTranscript/SecureConversations.ChatWithTranscriptModel.swift index 41f370348..b6356afb7 100644 --- a/GliaWidgets/SecureConversations/ChatTranscript/SecureConversations.ChatWithTranscriptModel.swift +++ b/GliaWidgets/SecureConversations/ChatTranscript/SecureConversations.ChatWithTranscriptModel.swift @@ -88,7 +88,6 @@ extension SecureConversations.ChatWithTranscriptModel { } } - // TODO: Unit test to be added in MOB-3840 var entryWidget: EntryWidget? { switch self { case .chat: diff --git a/GliaWidgets/Sources/EntryWidget/EntryWidget.swift b/GliaWidgets/Sources/EntryWidget/EntryWidget.swift index 87b44f2b0..5e0555c13 100644 --- a/GliaWidgets/Sources/EntryWidget/EntryWidget.swift +++ b/GliaWidgets/Sources/EntryWidget/EntryWidget.swift @@ -155,7 +155,6 @@ private extension EntryWidget { } } } - // TODO: Unit test to be added in MOB-3840 if !environment.isAuthenticated() || configuration.filterSecureConversation { availableMediaTypes.remove(.secureMessaging) } @@ -223,11 +222,9 @@ private extension EntryWidget { hostingController.view.clipsToBounds = true hostingController.view.translatesAutoresizingMaskIntoConstraints = false - let heightConstraint = hostingController.view.heightAnchor.constraint(equalTo: parentView.heightAnchor) - NSLayoutConstraint.activate([ hostingController.view.widthAnchor.constraint(equalTo: parentView.widthAnchor), - heightConstraint, + hostingController.view.heightAnchor.constraint(equalTo: parentView.heightAnchor), hostingController.view.centerXAnchor.constraint(equalTo: parentView.centerXAnchor), hostingController.view.centerYAnchor.constraint(equalTo: parentView.centerYAnchor) ]) @@ -344,8 +341,8 @@ extension EntryWidget: UIViewControllerTransitioningDelegate { #if DEBUG extension EntryWidget { - static func mock() -> Self { - .init(queueIds: [], configuration: .default, environment: .mock()) + static func mock(configuration: EntryWidget.Configuration? = nil) -> Self { + .init(queueIds: [], configuration: configuration ?? .default, environment: .mock()) } } diff --git a/GliaWidgets/Sources/EntryWidget/EntryWidgetViewModel.swift b/GliaWidgets/Sources/EntryWidget/EntryWidgetViewModel.swift index cfd5ce86a..f5b4f0915 100644 --- a/GliaWidgets/Sources/EntryWidget/EntryWidgetViewModel.swift +++ b/GliaWidgets/Sources/EntryWidget/EntryWidgetViewModel.swift @@ -12,7 +12,6 @@ extension EntryWidgetView { theme.entryWidget } - // TODO: Unit test to be added in MOB-3840 var showPoweredBy: Bool { theme.showsPoweredBy && configuration.showPoweredBy } diff --git a/GliaWidgets/Sources/View/Chat/ChatView.swift b/GliaWidgets/Sources/View/Chat/ChatView.swift index 4b3d3b02c..aa998e543 100644 --- a/GliaWidgets/Sources/View/Chat/ChatView.swift +++ b/GliaWidgets/Sources/View/Chat/ChatView.swift @@ -57,7 +57,8 @@ class ChatView: EngagementView { return CGRect(x: x, y: y, width: width, height: height) } - @Published private var isTopBannerExpanded = false + // Made internal for Snapshot test purposes + @Published var isTopBannerExpanded = false @Published private var isTopBannerHidden = true var props: Props { diff --git a/GliaWidgets/Sources/ViewController/Chat/ChatViewController.Mock.swift b/GliaWidgets/Sources/ViewController/Chat/ChatViewController.Mock.swift index 78e850995..024763fbb 100644 --- a/GliaWidgets/Sources/ViewController/Chat/ChatViewController.Mock.swift +++ b/GliaWidgets/Sources/ViewController/Chat/ChatViewController.Mock.swift @@ -655,6 +655,28 @@ extension ChatViewController { return .init(viewModel: .transcript(transcriptModel), environment: .mock()) } + + static func mockSecureMessagingTopAndBottomBannerView( + entryWidgetViewState: EntryWidget.ViewState = .loading + ) -> ChatViewController { + var chatViewModelEnv = ChatViewModel.Environment.mock + chatViewModelEnv.fileManager.urlsForDirectoryInDomainMask = { _, _ in [.mock] } + + let transcriptModel = SecureConversations.TranscriptModel.init( + isCustomCardSupported: false, + environment: .mock(createEntryWidget: { configuration in + let entryWidget = EntryWidget.mock(configuration: configuration) + entryWidget.viewState = entryWidgetViewState + return entryWidget + }), + availability: .mock(), + deliveredStatusText: "deliveredStatusText", + failedToDeliverStatusText: "failedToDeliverStatusText", + interactor: .mock() + ) + + return .init(viewModel: .transcript(transcriptModel), environment: .mock()) + } } /// Defines wrapper structure for getting decoding container. diff --git a/GliaWidgetsTests/SecureConversations/SecureConversations.ChatWithTranscriptModel/SecureConversations.ChatWithTranscriptModelTests.swift b/GliaWidgetsTests/SecureConversations/SecureConversations.ChatWithTranscriptModel/SecureConversations.ChatWithTranscriptModelTests.swift index 5d89e559c..7197b0587 100644 --- a/GliaWidgetsTests/SecureConversations/SecureConversations.ChatWithTranscriptModel/SecureConversations.ChatWithTranscriptModelTests.swift +++ b/GliaWidgetsTests/SecureConversations/SecureConversations.ChatWithTranscriptModel/SecureConversations.ChatWithTranscriptModelTests.swift @@ -70,4 +70,19 @@ final class SecureConversationsChatWithTranscriptModelTests: XCTestCase { XCTAssertEqual(contains, false) XCTAssertEqual(section.itemCount, 2) } + + func testEntryWidget() { + let chatViewModel = Model.chat(.mock()) + let transcriptViewModel = Model.transcript( + .mock( + availability: .mock, + deliveredStatusText: "Delivered", + failedToDeliverStatusText: "Failed to deliver", + interactor: .mock() + ) + ) + + XCTAssertNil(chatViewModel.entryWidget) + XCTAssertNotNil(transcriptViewModel.entryWidget) + } } diff --git a/GliaWidgetsTests/Sources/EntryWidget/EntryWidget.Mock.swift b/GliaWidgetsTests/Sources/EntryWidget/EntryWidget.Mock.swift new file mode 100644 index 000000000..a8fac5277 --- /dev/null +++ b/GliaWidgetsTests/Sources/EntryWidget/EntryWidget.Mock.swift @@ -0,0 +1,26 @@ +@testable import GliaWidgets + +extension EntryWidget.Configuration { + static func mock( + sizeConstraint: EntryWidget.SizeConstraints? = nil, + showPoweredBy: Bool? = nil, + filterSecureConversation: Bool? = nil, + mediaTypeSelected: Command? = nil + ) -> Self { + Self( + sizeConstraints: sizeConstraint ?? .init( + singleCellHeight: 72, + singleCellIconSize: 24, + poweredByContainerHeight: 40, + sheetHeaderHeight: 36, + sheetHeaderDraggerWidth: 32, + sheetHeaderDraggerHeight: 4, + dividerHeight: 1, + dividerHorizontalPadding: nil + ), + showPoweredBy: showPoweredBy ?? true, + filterSecureConversation: filterSecureConversation ?? false, + mediaTypeSelected: mediaTypeSelected + ) + } +} diff --git a/GliaWidgetsTests/Sources/EntryWidget/EntryWidgetTests.swift b/GliaWidgetsTests/Sources/EntryWidget/EntryWidgetTests.swift index 31f4ee6a2..0b8b934da 100644 --- a/GliaWidgetsTests/Sources/EntryWidget/EntryWidgetTests.swift +++ b/GliaWidgetsTests/Sources/EntryWidget/EntryWidgetTests.swift @@ -148,4 +148,87 @@ class EntryWidgetTests: XCTestCase { XCTAssertEqual(envCalls, [.start(.messaging(.welcome))]) } + + func test_availableEngagementTypesSortedWithNoFilters() { + let mockQueueId = "mockQueueId" + let mockQueue = Queue.mock(id: mockQueueId, media: [.messaging, .audio, .video, .text]) + var queueMonitorEnvironment: QueuesMonitor.Environment = .mock + queueMonitorEnvironment.listQueues = { completion in + completion([mockQueue], nil) + } + let queuesMonitor = QueuesMonitor(environment: queueMonitorEnvironment) + queuesMonitor.fetchAndMonitorQueues(queuesIds: [mockQueueId]) + + var environment = EntryWidget.Environment.mock() + environment.queuesMonitor = queuesMonitor + environment.observeSecureUnreadMessageCount = { result in + result(.success(5)) + return UUID.mock.uuidString + } + + + let entryWidget = EntryWidget( + queueIds: [mockQueueId], + configuration: .default, + environment: environment + ) + + XCTAssertEqual(entryWidget.availableEngagementTypes, [.video, .audio, .chat, .secureMessaging]) + } + + func test_availableEngagementTypesSortedAndFilteredIfUserNotAuthenticated() { + let mockQueueId = "mockQueueId" + let mockQueue = Queue.mock(id: mockQueueId, media: [.messaging, .audio, .video, .text]) + var queueMonitorEnvironment: QueuesMonitor.Environment = .mock + queueMonitorEnvironment.listQueues = { completion in + completion([mockQueue], nil) + } + let queuesMonitor = QueuesMonitor(environment: queueMonitorEnvironment) + queuesMonitor.fetchAndMonitorQueues(queuesIds: [mockQueueId]) + + var environment = EntryWidget.Environment.mock() + environment.queuesMonitor = queuesMonitor + environment.observeSecureUnreadMessageCount = { result in + result(.success(5)) + return UUID.mock.uuidString + } + environment.isAuthenticated = { + false + } + + + let entryWidget = EntryWidget( + queueIds: [mockQueueId], + configuration: .default, + environment: environment + ) + + XCTAssertEqual(entryWidget.availableEngagementTypes, [.video, .audio, .chat]) + } + + func test_availableEngagementTypesSortedAndFilteredSecureConversation() { + let mockQueueId = "mockQueueId" + let mockQueue = Queue.mock(id: mockQueueId, media: [.messaging, .audio, .video, .text]) + var queueMonitorEnvironment: QueuesMonitor.Environment = .mock + queueMonitorEnvironment.listQueues = { completion in + completion([mockQueue], nil) + } + let queuesMonitor = QueuesMonitor(environment: queueMonitorEnvironment) + queuesMonitor.fetchAndMonitorQueues(queuesIds: [mockQueueId]) + + var environment = EntryWidget.Environment.mock() + environment.queuesMonitor = queuesMonitor + environment.observeSecureUnreadMessageCount = { result in + result(.success(5)) + return UUID.mock.uuidString + } + var configuration = EntryWidget.Configuration.mock(filterSecureConversation: true) + let entryWidget = EntryWidget( + queueIds: [mockQueueId], + configuration: configuration, + environment: environment + ) + + XCTAssertEqual(entryWidget.availableEngagementTypes, [.video, .audio, .chat]) + } } diff --git a/GliaWidgetsTests/Sources/EntryWidget/EntryWidgetViewModelTests.swift b/GliaWidgetsTests/Sources/EntryWidget/EntryWidgetViewModelTests.swift new file mode 100644 index 000000000..b56247c89 --- /dev/null +++ b/GliaWidgetsTests/Sources/EntryWidget/EntryWidgetViewModelTests.swift @@ -0,0 +1,49 @@ +import XCTest +import Combine +import GliaCoreSDK + +@testable import GliaWidgets + +class EntryWidgetViewModelTests: XCTestCase { + func test_showPoweredBy() { + let entryWidget = EntryWidget.mock() + + let viewModel = EntryWidgetView.Model( + theme: .mock(showsPoweredBy: true), + showHeader: true, + configuration: .mock(showPoweredBy: true), + viewStatePublisher: entryWidget.$viewState, + mediaTypeSelected: { _ in } + ) + + XCTAssertTrue(viewModel.showPoweredBy) + } + + func test_doesNotShowPoweredByUsingThemeProperty() { + let entryWidget = EntryWidget.mock() + + let viewModel = EntryWidgetView.Model( + theme: .mock(showsPoweredBy: false), + showHeader: true, + configuration: .mock(showPoweredBy: true), + viewStatePublisher: entryWidget.$viewState, + mediaTypeSelected: { _ in } + ) + + XCTAssertFalse(viewModel.showPoweredBy) + } + + func test_doesNotShowPoweredByUsingConfigurationProperty() { + let entryWidget = EntryWidget.mock() + + let viewModel = EntryWidgetView.Model( + theme: .mock(showsPoweredBy: true), + showHeader: true, + configuration: .mock(showPoweredBy: false), + viewStatePublisher: entryWidget.$viewState, + mediaTypeSelected: { _ in } + ) + + XCTAssertFalse(viewModel.showPoweredBy) + } +} diff --git a/SnapshotTests/ChatViewControllerLayoutTests.swift b/SnapshotTests/ChatViewControllerLayoutTests.swift index b72756fcd..f6220229c 100644 --- a/SnapshotTests/ChatViewControllerLayoutTests.swift +++ b/SnapshotTests/ChatViewControllerLayoutTests.swift @@ -79,10 +79,30 @@ final class ChatViewControllerLayoutTests: SnapshotTestCase { viewController.assertSnapshot(as: .image, in: .landscape) } - func test_secureMessagingTopAndBottomBanner() { + func test_secureMessagingBottomAndCollapsedTopBanner() { let viewController = ChatViewController.mockSecureMessagingBottomBannerView() viewController.updateViewConstraints() viewController.assertSnapshot(as: .image, in: .portrait) viewController.assertSnapshot(as: .image, in: .landscape) } + + func test_secureMessagingBottomAndExpandedTopBanner() { + let mockEntryWidgetViewState = EntryWidget.ViewState.mediaTypes( + [.init(type: .chat), .init(type: .video), .init(type: .audio)] + ) + let viewController = ChatViewController.mockSecureMessagingTopAndBottomBannerView( + entryWidgetViewState: mockEntryWidgetViewState + ) + viewController.updateViewConstraints() + + (viewController.view as? ChatView)?.isTopBannerExpanded = true + viewController.updateViewConstraints() + + viewController.view.frame = UIScreen.main.bounds + viewController.view.setNeedsLayout() + viewController.view.layoutIfNeeded() + + viewController.assertSnapshot(as: .image, in: .portrait) + viewController.assertSnapshot(as: .image, in: .landscape) + } }