From 163349b7c74875a1b79717a33ef630de337fe9b9 Mon Sep 17 00:00:00 2001 From: Nuno Vieira Date: Tue, 15 Oct 2024 16:35:56 +0100 Subject: [PATCH] Add Thread List View Model test coverage --- .../ChatThreadListViewModel.swift | 9 +- StreamChatSwiftUI.xcodeproj/project.pbxproj | 4 + .../ChatThreadListViewModel_Tests.swift | 185 ++++++++++++++++++ 3 files changed, 194 insertions(+), 4 deletions(-) create mode 100644 StreamChatSwiftUITests/Tests/ChatThreadList/ChatThreadListViewModel_Tests.swift diff --git a/Sources/StreamChatSwiftUI/ChatThreadList/ChatThreadListViewModel.swift b/Sources/StreamChatSwiftUI/ChatThreadList/ChatThreadListViewModel.swift index 5ffc412f..2fc1d216 100644 --- a/Sources/StreamChatSwiftUI/ChatThreadList/ChatThreadListViewModel.swift +++ b/Sources/StreamChatSwiftUI/ChatThreadList/ChatThreadListViewModel.swift @@ -89,12 +89,12 @@ open class ChatThreadListViewModel: ObservableObject, ChatThreadListControllerDe /// Re-fetches the threads. If the initial query failed, it will load the initial page. /// If on the other hand it was a new page that failed, it will re-fetch that page. public func retryLoadThreads() { - if failedToLoadThreads { - loadThreads() + if failedToLoadMoreThreads { + loadMoreThreads() return } - loadMoreThreads() + loadThreads() } /// Called when the view appears on screen. @@ -115,7 +115,8 @@ open class ChatThreadListViewModel: ObservableObject, ChatThreadListControllerDe /// Loads the initial page of threads. public func loadThreads() { - isLoading = threadListController.threads.isEmpty == true + let isEmpty = threadListController.threads.isEmpty + isLoading = isEmpty failedToLoadThreads = false isReloading = !isEmpty preselectThreadIfNeeded() diff --git a/StreamChatSwiftUI.xcodeproj/project.pbxproj b/StreamChatSwiftUI.xcodeproj/project.pbxproj index fe28193f..5680e756 100644 --- a/StreamChatSwiftUI.xcodeproj/project.pbxproj +++ b/StreamChatSwiftUI.xcodeproj/project.pbxproj @@ -520,6 +520,7 @@ ADE0F5622CB8556F0053B8B9 /* ChatThreadListFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADE0F5612CB8556F0053B8B9 /* ChatThreadListFooterView.swift */; }; ADE0F5642CB9609E0053B8B9 /* ChatThreadListHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADE0F5632CB9609E0053B8B9 /* ChatThreadListHeaderView.swift */; }; ADE0F5662CB962470053B8B9 /* ActionBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADE0F5652CB962470053B8B9 /* ActionBannerView.swift */; }; + ADE442EE2CBDAAAA0066CDF7 /* ChatThreadListViewModel_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADE442ED2CBDAAAA0066CDF7 /* ChatThreadListViewModel_Tests.swift */; }; ADE442F02CBDAAB70066CDF7 /* ChatThreadListView_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADE442EF2CBDAAB70066CDF7 /* ChatThreadListView_Tests.swift */; }; ADE442F22CBDAAC40066CDF7 /* ChatThreadListItemView_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADE442F12CBDAAC40066CDF7 /* ChatThreadListItemView_Tests.swift */; }; C14A465B284665B100EF498E /* SDKIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C14A465A284665B100EF498E /* SDKIdentifier.swift */; }; @@ -1111,6 +1112,7 @@ ADE0F5612CB8556F0053B8B9 /* ChatThreadListFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatThreadListFooterView.swift; sourceTree = ""; }; ADE0F5632CB9609E0053B8B9 /* ChatThreadListHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatThreadListHeaderView.swift; sourceTree = ""; }; ADE0F5652CB962470053B8B9 /* ActionBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionBannerView.swift; sourceTree = ""; }; + ADE442ED2CBDAAAA0066CDF7 /* ChatThreadListViewModel_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatThreadListViewModel_Tests.swift; sourceTree = ""; }; ADE442EF2CBDAAB70066CDF7 /* ChatThreadListView_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatThreadListView_Tests.swift; sourceTree = ""; }; ADE442F12CBDAAC40066CDF7 /* ChatThreadListItemView_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatThreadListItemView_Tests.swift; sourceTree = ""; }; C14A465A284665B100EF498E /* SDKIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDKIdentifier.swift; sourceTree = ""; }; @@ -2268,6 +2270,7 @@ ADE442EC2CBDAA320066CDF7 /* ChatThreadList */ = { isa = PBXGroup; children = ( + ADE442ED2CBDAAAA0066CDF7 /* ChatThreadListViewModel_Tests.swift */, ADE442EF2CBDAAB70066CDF7 /* ChatThreadListView_Tests.swift */, ADE442F12CBDAAC40066CDF7 /* ChatThreadListItemView_Tests.swift */, ); @@ -2966,6 +2969,7 @@ 84C94D0727578BF2007FE2B9 /* RandomDispatchQueue.swift in Sources */, 847110B628611033004A46D6 /* MessageActions_Tests.swift in Sources */, 84C94D1227578BF2007FE2B9 /* JSONEncoder+Extensions.swift in Sources */, + ADE442EE2CBDAAAA0066CDF7 /* ChatThreadListViewModel_Tests.swift in Sources */, 84E04797284A444E00BAFA17 /* WebSocketPingControllerMock.swift in Sources */, 8423C34C277DDD250092DCF1 /* MuteCommandHandler_Tests.swift in Sources */, 84C94D1127578BF2007FE2B9 /* ChannelId.swift in Sources */, diff --git a/StreamChatSwiftUITests/Tests/ChatThreadList/ChatThreadListViewModel_Tests.swift b/StreamChatSwiftUITests/Tests/ChatThreadList/ChatThreadListViewModel_Tests.swift new file mode 100644 index 00000000..0b3e98d0 --- /dev/null +++ b/StreamChatSwiftUITests/Tests/ChatThreadList/ChatThreadListViewModel_Tests.swift @@ -0,0 +1,185 @@ +// +// Copyright © 2024 Stream.io Inc. All rights reserved. +// + +@testable import StreamChat +@testable import StreamChatSwiftUI +@testable import StreamChatTestTools +import XCTest + +class ChatThreadListViewModel_Tests: StreamChatTestCase { + + func test_viewDidAppear_thenLoadsThreads() { + let mockThreadListController = ChatThreadListController_Mock.mock( + query: .init(watch: true) + ) + let viewModel = ChatThreadListViewModel( + threadListController: mockThreadListController + ) + + viewModel.viewDidAppear() + XCTAssertEqual(mockThreadListController.synchronize_callCount, 1) + } + + func test_viewDidAppear_whenAlreadyLoadedThreads_thenDoesNotLoadsThreads() { + let mockThreadListController = ChatThreadListController_Mock.mock( + query: .init(watch: true) + ) + let viewModel = ChatThreadListViewModel( + threadListController: mockThreadListController + ) + + viewModel.viewDidAppear() + mockThreadListController.synchronize_completion?(nil) + viewModel.viewDidAppear() + + XCTAssertEqual(mockThreadListController.synchronize_callCount, 1) + } + + func test_loadThreads_whenInitialEmptyData_whenSuccess() { + let mockThreadListController = ChatThreadListController_Mock.mock( + query: .init(watch: true) + ) + mockThreadListController.threads_mock = [] + let viewModel = ChatThreadListViewModel( + threadListController: mockThreadListController + ) + + viewModel.loadThreads() + + XCTAssertEqual(viewModel.isLoading, true) + XCTAssertEqual(viewModel.isReloading, false) + XCTAssertEqual(viewModel.failedToLoadThreads, false) + XCTAssertEqual(viewModel.hasLoadedThreads, false) + + mockThreadListController.threads_mock = [.mock()] + mockThreadListController.synchronize_completion?(nil) + + XCTAssertEqual(viewModel.isLoading, false) + XCTAssertEqual(viewModel.isReloading, false) + XCTAssertEqual(viewModel.failedToLoadThreads, false) + XCTAssertEqual(viewModel.hasLoadedThreads, true) + XCTAssertEqual(viewModel.isEmpty, false) + } + + func test_loadThreads_whenCacheAvailable_whenSuccess() { + let mockThreadListController = ChatThreadListController_Mock.mock( + query: .init(watch: true) + ) + mockThreadListController.threads_mock = [.mock()] + let viewModel = ChatThreadListViewModel( + threadListController: mockThreadListController + ) + + viewModel.loadThreads() + + XCTAssertEqual(viewModel.isLoading, false) + XCTAssertEqual(viewModel.isReloading, true) + XCTAssertEqual(viewModel.failedToLoadThreads, false) + XCTAssertEqual(viewModel.hasLoadedThreads, false) + + mockThreadListController.threads_mock = [.mock(), .mock()] + mockThreadListController.synchronize_completion?(nil) + + XCTAssertEqual(viewModel.isLoading, false) + XCTAssertEqual(viewModel.isReloading, false) + XCTAssertEqual(viewModel.failedToLoadThreads, false) + XCTAssertEqual(viewModel.hasLoadedThreads, true) + XCTAssertEqual(viewModel.isEmpty, false) + } + + func test_loadThreads_whenError() { + let mockThreadListController = ChatThreadListController_Mock.mock( + query: .init(watch: true) + ) + mockThreadListController.threads_mock = [] + let viewModel = ChatThreadListViewModel( + threadListController: mockThreadListController + ) + + viewModel.loadThreads() + mockThreadListController.threads_mock = [.mock()] + mockThreadListController.synchronize_completion?(ClientError("ERROR")) + + XCTAssertEqual(viewModel.isLoading, false) + XCTAssertEqual(viewModel.isReloading, false) + XCTAssertEqual(viewModel.failedToLoadThreads, true) + XCTAssertEqual(viewModel.failedToLoadMoreThreads, false) + XCTAssertEqual(viewModel.hasLoadedThreads, false) + } + + func test_didAppearThread_whenInsideThreshold_thenLoadMoreThreads() { + let mockThreadListController = ChatThreadListController_Mock.mock( + query: .init(watch: true) + ) + let viewModel = ChatThreadListViewModel( + threadListController: mockThreadListController + ) + let mockedThreads: [ChatThread] = [ + .mock(), .mock(), .mock(), .mock(), .mock(), .mock(), .mock() + ] + mockedThreads.forEach { thread in + viewModel.threads.append(thread) + } + + XCTAssertEqual(viewModel.isLoadingMoreThreads, false) + + viewModel.didAppearThread(at: 5) + + XCTAssertEqual(viewModel.isLoadingMoreThreads, true) + } + + func test_didAppearThread_whenNotInThreshold_thenDoNotLoadMoreThreads() { + let mockThreadListController = ChatThreadListController_Mock.mock( + query: .init(watch: true) + ) + let viewModel = ChatThreadListViewModel( + threadListController: mockThreadListController + ) + let mockedThreads: [ChatThread] = [ + .mock(), .mock(), .mock(), .mock(), .mock(), .mock(), .mock() + ] + mockedThreads.forEach { thread in + viewModel.threads.append(thread) + } + + XCTAssertEqual(viewModel.isLoadingMoreThreads, false) + + viewModel.didAppearThread(at: 0) + + XCTAssertEqual(viewModel.isLoadingMoreThreads, false) + } + + func test_didReceiveThreadMessageNewEvent() { + let mockThreadListController = ChatThreadListController_Mock.mock( + query: .init(watch: true) + ) + let viewModel = ChatThreadListViewModel( + threadListController: mockThreadListController + ) + let eventController = mockThreadListController.client.eventsController() + + // 2 Events + viewModel.eventsController( + eventController, + didReceiveEvent: ThreadMessageNewEvent( + message: .mock(parentMessageId: .unique), + channel: .mock(cid: .unique), + unreadCount: .noUnread, + createdAt: .unique + ) + ) + viewModel.eventsController( + eventController, + didReceiveEvent: ThreadMessageNewEvent( + message: .mock(parentMessageId: .unique), + channel: .mock(cid: .unique), + unreadCount: .noUnread, + createdAt: .unique + ) + ) + + XCTAssertEqual(viewModel.newThreadsCount, 2) + XCTAssertTrue(viewModel.hasNewThreads) + } +}