mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:33:26 +00:00
Swift Testing for Unit Tests PART 1 (#5119)
* migrated a lot of unit tests to Swift Testing and added a new implementation for deferred fulfillment more tests migration Cleaned the code manually to establish some good patterns more code improvements some more code improvements removed empty tests update project * more pr suggestions and cleanups * removed the TestSetup pattern * fixing claude not reusing tests * pr suggestion + added indent rule to swiftformat so that we can prevent AIs to change that
This commit is contained in:
@@ -8,10 +8,11 @@
|
||||
|
||||
import Combine
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
class HomeScreenViewModelTests: XCTestCase {
|
||||
@Suite
|
||||
final class HomeScreenViewModelTests {
|
||||
var viewModel: HomeScreenViewModelProtocol!
|
||||
var context: HomeScreenViewModelType.Context! {
|
||||
viewModel.context
|
||||
@@ -24,19 +25,18 @@ class HomeScreenViewModelTests: XCTestCase {
|
||||
|
||||
var cancellables = Set<AnyCancellable>()
|
||||
|
||||
override func setUp() {
|
||||
cancellables.removeAll()
|
||||
|
||||
init() {
|
||||
AppSettings.resetAllSettings()
|
||||
appSettings = AppSettings()
|
||||
ServiceLocator.shared.register(appSettings: appSettings)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
deinit {
|
||||
AppSettings.resetAllSettings()
|
||||
}
|
||||
|
||||
func testSelectRoom() async {
|
||||
@Test
|
||||
func selectRoom() async {
|
||||
setupViewModel()
|
||||
|
||||
let mockRoomID = "mock_room_id"
|
||||
@@ -57,11 +57,12 @@ class HomeScreenViewModelTests: XCTestCase {
|
||||
|
||||
context.send(viewAction: .selectRoom(roomIdentifier: mockRoomID))
|
||||
await Task.yield()
|
||||
XCTAssert(correctResult)
|
||||
XCTAssertEqual(mockRoomID, selectedRoomID)
|
||||
#expect(correctResult)
|
||||
#expect(mockRoomID == selectedRoomID)
|
||||
}
|
||||
|
||||
func testTapUserAvatar() async {
|
||||
|
||||
@Test
|
||||
func tapUserAvatar() async {
|
||||
setupViewModel()
|
||||
|
||||
var correctResult = false
|
||||
@@ -79,10 +80,11 @@ class HomeScreenViewModelTests: XCTestCase {
|
||||
|
||||
context.send(viewAction: .showSettings)
|
||||
await Task.yield()
|
||||
XCTAssert(correctResult)
|
||||
#expect(correctResult)
|
||||
}
|
||||
|
||||
func testLeaveRoomAlert() async throws {
|
||||
@Test
|
||||
func leaveRoomAlert() async throws {
|
||||
setupViewModel()
|
||||
|
||||
let mockRoomID = "1"
|
||||
@@ -97,10 +99,11 @@ class HomeScreenViewModelTests: XCTestCase {
|
||||
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertEqual(context.leaveRoomAlertItem?.roomID, mockRoomID)
|
||||
#expect(context.leaveRoomAlertItem?.roomID == mockRoomID)
|
||||
}
|
||||
|
||||
func testLeaveRoomError() async throws {
|
||||
@Test
|
||||
func leaveRoomError() async throws {
|
||||
setupViewModel()
|
||||
|
||||
let mockRoomID = "1"
|
||||
@@ -108,7 +111,7 @@ class HomeScreenViewModelTests: XCTestCase {
|
||||
room.leaveRoomClosure = { .failure(.sdkError(ClientProxyMockError.generic)) }
|
||||
|
||||
clientProxy.roomForIdentifierClosure = { _ in .joined(room) }
|
||||
|
||||
|
||||
let deferred = deferFulfillment(context.$viewState) { value in
|
||||
value.bindings.alertInfo != nil
|
||||
}
|
||||
@@ -116,39 +119,35 @@ class HomeScreenViewModelTests: XCTestCase {
|
||||
context.send(viewAction: .confirmLeaveRoom(roomIdentifier: mockRoomID))
|
||||
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertNotNil(context.alertInfo)
|
||||
|
||||
#expect(context.alertInfo != nil)
|
||||
}
|
||||
|
||||
func testLeaveRoomSuccess() async {
|
||||
@Test
|
||||
func leaveRoomSuccess() async throws {
|
||||
setupViewModel()
|
||||
|
||||
let mockRoomID = "1"
|
||||
var correctResult = false
|
||||
let expectation = expectation(description: #function)
|
||||
viewModel.actions
|
||||
.sink { action in
|
||||
switch action {
|
||||
case .roomLeft(let roomIdentifier):
|
||||
correctResult = roomIdentifier == mockRoomID
|
||||
default:
|
||||
break
|
||||
}
|
||||
expectation.fulfill()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
let room = JoinedRoomProxyMock(.init(id: mockRoomID, name: "Some room"))
|
||||
room.leaveRoomClosure = { .success(()) }
|
||||
|
||||
clientProxy.roomForIdentifierClosure = { _ in .joined(room) }
|
||||
|
||||
let deferred = deferFulfillment(viewModel.actions) { action in
|
||||
if case .roomLeft(let roomIdentifier) = action {
|
||||
return roomIdentifier == mockRoomID
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
context.send(viewAction: .confirmLeaveRoom(roomIdentifier: mockRoomID))
|
||||
await fulfillment(of: [expectation])
|
||||
XCTAssertNil(context.alertInfo)
|
||||
XCTAssertTrue(correctResult)
|
||||
try await deferred.fulfill()
|
||||
#expect(context.alertInfo == nil)
|
||||
}
|
||||
|
||||
func testShowRoomDetails() async {
|
||||
@Test
|
||||
func showRoomDetails() async {
|
||||
setupViewModel()
|
||||
|
||||
let mockRoomID = "1"
|
||||
@@ -165,45 +164,49 @@ class HomeScreenViewModelTests: XCTestCase {
|
||||
.store(in: &cancellables)
|
||||
context.send(viewAction: .showRoomDetails(roomIdentifier: mockRoomID))
|
||||
await Task.yield()
|
||||
XCTAssertNil(context.alertInfo)
|
||||
XCTAssertTrue(correctResult)
|
||||
#expect(context.alertInfo == nil)
|
||||
#expect(correctResult)
|
||||
}
|
||||
|
||||
func testFilters() async throws {
|
||||
@Test
|
||||
func filters() async throws {
|
||||
setupViewModel()
|
||||
|
||||
context.filtersState.activateFilter(.people)
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
XCTAssertEqual(roomSummaryProvider.roomListPublisher.value.count, 2)
|
||||
XCTAssertEqual(roomSummaryProvider.roomListPublisher.value.first?.name, "Foundation and Earth")
|
||||
#expect(roomSummaryProvider.roomListPublisher.value.count == 2)
|
||||
#expect(roomSummaryProvider.roomListPublisher.value.first?.name == "Foundation and Earth")
|
||||
}
|
||||
|
||||
func testSearch() async throws {
|
||||
@Test
|
||||
func search() async throws {
|
||||
setupViewModel()
|
||||
|
||||
context.isSearchFieldFocused = true
|
||||
context.searchQuery = "lude to Found"
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
XCTAssertEqual(roomSummaryProvider.roomListPublisher.value.first?.name, "Prelude to Foundation")
|
||||
XCTAssertEqual(roomSummaryProvider.roomListPublisher.value.count, 1)
|
||||
#expect(roomSummaryProvider.roomListPublisher.value.first?.name == "Prelude to Foundation")
|
||||
#expect(roomSummaryProvider.roomListPublisher.value.count == 1)
|
||||
}
|
||||
|
||||
func testFiltersEmptyState() async throws {
|
||||
@Test
|
||||
func filtersEmptyState() async throws {
|
||||
setupViewModel()
|
||||
|
||||
context.filtersState.activateFilter(.people)
|
||||
context.filtersState.activateFilter(.favourites)
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
XCTAssertTrue(context.viewState.shouldShowEmptyFilterState)
|
||||
#expect(context.viewState.shouldShowEmptyFilterState)
|
||||
context.isSearchFieldFocused = true
|
||||
XCTAssertFalse(context.viewState.shouldShowEmptyFilterState)
|
||||
#expect(!context.viewState.shouldShowEmptyFilterState)
|
||||
}
|
||||
|
||||
func testSetUpRecoveryBannerState() async throws {
|
||||
@Test
|
||||
func setUpRecoveryBannerState() async throws {
|
||||
// Given a view model without a visible security banner.
|
||||
let securityStateStateSubject = CurrentValueSubject<SessionSecurityState, Never>(.init(verificationState: .verified, recoveryState: .unknown))
|
||||
setupViewModel(securityStatePublisher: securityStateStateSubject.asCurrentValuePublisher())
|
||||
XCTAssertEqual(context.viewState.securityBannerMode, .none)
|
||||
#expect(context.viewState.securityBannerMode == .none)
|
||||
|
||||
// When the recovery state comes through as disabled.
|
||||
var deferred = deferFulfillment(context.$viewState) { $0.requiresExtraAccountSetup == true }
|
||||
@@ -211,7 +214,7 @@ class HomeScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the banner should be shown to set up recovery.
|
||||
XCTAssertEqual(context.viewState.securityBannerMode, .show(.setUpRecovery))
|
||||
#expect(context.viewState.securityBannerMode == .show(.setUpRecovery))
|
||||
|
||||
// When the recovery is enabled.
|
||||
deferred = deferFulfillment(context.$viewState) { $0.requiresExtraAccountSetup == false }
|
||||
@@ -219,10 +222,11 @@ class HomeScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the banner should no longer be shown.
|
||||
XCTAssertEqual(context.viewState.securityBannerMode, .none)
|
||||
#expect(context.viewState.securityBannerMode == .none)
|
||||
}
|
||||
|
||||
func testDismissSetUpRecoveryBannerState() async throws {
|
||||
@Test
|
||||
func dismissSetUpRecoveryBannerState() async throws {
|
||||
// Given a view model with the setup recovery banner shown.
|
||||
let securityStateStateSubject = CurrentValueSubject<SessionSecurityState, Never>(.init(verificationState: .verified, recoveryState: .unknown))
|
||||
setupViewModel(securityStatePublisher: securityStateStateSubject.asCurrentValuePublisher())
|
||||
@@ -238,16 +242,17 @@ class HomeScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// And when the recovery state comes through a second time the banner should still not be shown.
|
||||
let failure = deferFailure(context.$viewState, timeout: 1) { $0.securityBannerMode != .dismissed }
|
||||
let failure = deferFailure(context.$viewState, timeout: .seconds(1)) { $0.securityBannerMode != .dismissed }
|
||||
securityStateStateSubject.send(.init(verificationState: .verified, recoveryState: .disabled))
|
||||
try await failure.fulfill()
|
||||
}
|
||||
|
||||
func testOutOfSyncRecoveryBannerState() async throws {
|
||||
@Test
|
||||
func outOfSyncRecoveryBannerState() async throws {
|
||||
// Given a view model without a visible security banner.
|
||||
let securityStateStateSubject = CurrentValueSubject<SessionSecurityState, Never>(.init(verificationState: .verified, recoveryState: .unknown))
|
||||
setupViewModel(securityStatePublisher: securityStateStateSubject.asCurrentValuePublisher())
|
||||
XCTAssertEqual(context.viewState.securityBannerMode, .none)
|
||||
#expect(context.viewState.securityBannerMode == .none)
|
||||
|
||||
// When the recovery state comes through as incomplete.
|
||||
var deferred = deferFulfillment(context.$viewState) { $0.requiresExtraAccountSetup == true }
|
||||
@@ -255,7 +260,7 @@ class HomeScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the banner should be shown for out of sync recovery.
|
||||
XCTAssertEqual(context.viewState.securityBannerMode, .show(.recoveryOutOfSync))
|
||||
#expect(context.viewState.securityBannerMode == .show(.recoveryOutOfSync))
|
||||
|
||||
// When the recovery is enabled.
|
||||
deferred = deferFulfillment(context.$viewState) { $0.requiresExtraAccountSetup == false }
|
||||
@@ -263,16 +268,17 @@ class HomeScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the banner should no longer be shown.
|
||||
XCTAssertEqual(context.viewState.securityBannerMode, .none)
|
||||
#expect(context.viewState.securityBannerMode == .none)
|
||||
}
|
||||
|
||||
func testInviteUnreadBadge() async throws {
|
||||
@Test
|
||||
func inviteUnreadBadge() async throws {
|
||||
setupViewModel(invites: .rooms)
|
||||
var invites = context.viewState.rooms.invites
|
||||
XCTAssertEqual(invites.count, 2)
|
||||
#expect(invites.count == 2)
|
||||
|
||||
for invite in invites {
|
||||
XCTAssertTrue(invite.badges.isDotShown)
|
||||
#expect(invite.badges.isDotShown)
|
||||
}
|
||||
|
||||
let deferred = deferFulfillment(context.$viewState) { state in
|
||||
@@ -285,31 +291,33 @@ class HomeScreenViewModelTests: XCTestCase {
|
||||
invites = context.viewState.rooms.invites
|
||||
|
||||
for invite in invites {
|
||||
XCTAssertFalse(invite.badges.isDotShown)
|
||||
#expect(!invite.badges.isDotShown)
|
||||
}
|
||||
}
|
||||
|
||||
func testAcceptInvite() async throws {
|
||||
@Test
|
||||
func acceptInvite() async throws {
|
||||
setupViewModel(invites: .rooms)
|
||||
|
||||
let invitedRoomIDs = context.viewState.rooms.invites.compactMap(\.roomID)
|
||||
appSettings.seenInvites = Set(invitedRoomIDs)
|
||||
XCTAssertEqual(invitedRoomIDs.count, 2)
|
||||
#expect(invitedRoomIDs.count == 2)
|
||||
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0 == .presentRoom(roomIdentifier: invitedRoomIDs[0]) }
|
||||
context.send(viewAction: .acceptInvite(roomIdentifier: invitedRoomIDs[0]))
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertEqual(appSettings.seenInvites, [invitedRoomIDs[1]])
|
||||
XCTAssertFalse(notificationManager.removeDeliveredMessageNotificationsForCalled, "The notification will be dismissed when opening the room.")
|
||||
#expect(appSettings.seenInvites == [invitedRoomIDs[1]])
|
||||
#expect(!notificationManager.removeDeliveredMessageNotificationsForCalled, "The notification will be dismissed when opening the room.")
|
||||
}
|
||||
|
||||
func testAcceptSpaceInvite() async throws {
|
||||
@Test
|
||||
func acceptSpaceInvite() async throws {
|
||||
setupViewModel(invites: .spaces)
|
||||
|
||||
let invitedRoomIDs = context.viewState.rooms.invites.compactMap(\.roomID)
|
||||
appSettings.seenInvites = Set(invitedRoomIDs)
|
||||
XCTAssertEqual(invitedRoomIDs.count, 2)
|
||||
#expect(invitedRoomIDs.count == 2)
|
||||
|
||||
let deferred = deferFulfillment(viewModel.actions) {
|
||||
$0 == .presentSpace(SpaceRoomListProxyMock(.init(spaceServiceRoom: SpaceServiceRoom.mock(id: invitedRoomIDs[0], isSpace: true))))
|
||||
@@ -317,43 +325,48 @@ class HomeScreenViewModelTests: XCTestCase {
|
||||
context.send(viewAction: .acceptInvite(roomIdentifier: invitedRoomIDs[0]))
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertEqual(appSettings.seenInvites, [invitedRoomIDs[1]])
|
||||
XCTAssertFalse(notificationManager.removeDeliveredMessageNotificationsForCalled, "The notification will be dismissed when opening the room.")
|
||||
#expect(appSettings.seenInvites == [invitedRoomIDs[1]])
|
||||
#expect(!notificationManager.removeDeliveredMessageNotificationsForCalled, "The notification will be dismissed when opening the room.")
|
||||
}
|
||||
|
||||
func testDeclineInvite() async throws {
|
||||
@Test
|
||||
func declineInvite() async throws {
|
||||
setupViewModel(invites: .rooms)
|
||||
let invitedRoomIDs = context.viewState.rooms.invites.compactMap(\.roomID)
|
||||
appSettings.seenInvites = Set(invitedRoomIDs)
|
||||
XCTAssertEqual(invitedRoomIDs.count, 2)
|
||||
#expect(invitedRoomIDs.count == 2)
|
||||
|
||||
let deferred = deferFulfillment(context.$viewState) { $0.bindings.alertInfo != nil }
|
||||
context.send(viewAction: .declineInvite(roomIdentifier: invitedRoomIDs[0]))
|
||||
try await deferred.fulfill()
|
||||
|
||||
let rejectExpectation = expectation(description: "Expected rejectInvitation to be called.")
|
||||
var rejectCalled = false
|
||||
clientProxy.roomForIdentifierClosure = { _ in
|
||||
let roomProxy = InvitedRoomProxyMock(.init())
|
||||
roomProxy.rejectInvitationClosure = {
|
||||
rejectExpectation.fulfill()
|
||||
rejectCalled = true
|
||||
return .success(())
|
||||
}
|
||||
|
||||
return .invited(roomProxy)
|
||||
}
|
||||
context.viewState.bindings.alertInfo?.verticalButtons?[0].action?()
|
||||
await fulfillment(of: [rejectExpectation], timeout: 1.0)
|
||||
|
||||
XCTAssertEqual(appSettings.seenInvites, [invitedRoomIDs[1]])
|
||||
XCTAssertTrue(notificationManager.removeDeliveredMessageNotificationsForCalled)
|
||||
XCTAssertEqual(notificationManager.removeDeliveredMessageNotificationsForReceivedInvocations, [invitedRoomIDs[0]])
|
||||
// Wait for the async action to complete
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
#expect(rejectCalled)
|
||||
|
||||
#expect(appSettings.seenInvites == [invitedRoomIDs[1]])
|
||||
#expect(notificationManager.removeDeliveredMessageNotificationsForCalled)
|
||||
#expect(notificationManager.removeDeliveredMessageNotificationsForReceivedInvocations == [invitedRoomIDs[0]])
|
||||
}
|
||||
|
||||
func testDeclineAndBlockInvite() async throws {
|
||||
@Test
|
||||
func declineAndBlockInvite() async throws {
|
||||
setupViewModel(invites: .rooms)
|
||||
let invitedRoomIDs = context.viewState.rooms.invites.compactMap(\.roomID)
|
||||
appSettings.seenInvites = Set(invitedRoomIDs)
|
||||
XCTAssertEqual(invitedRoomIDs.count, 2)
|
||||
#expect(invitedRoomIDs.count == 2)
|
||||
|
||||
let deferred = deferFulfillment(context.$viewState) { $0.bindings.alertInfo != nil }
|
||||
context.send(viewAction: .declineInvite(roomIdentifier: invitedRoomIDs[0]))
|
||||
@@ -364,17 +377,18 @@ class HomeScreenViewModelTests: XCTestCase {
|
||||
try await deferredAction.fulfill()
|
||||
}
|
||||
|
||||
func testNewSoundBanner() {
|
||||
@Test
|
||||
func newSoundBanner() {
|
||||
appSettings.hasSeenNewSoundBanner = false
|
||||
|
||||
setupViewModel()
|
||||
XCTAssertTrue(context.viewState.shouldShowBanner)
|
||||
XCTAssertTrue(context.viewState.shouldShowNewSoundBanner)
|
||||
#expect(context.viewState.shouldShowBanner)
|
||||
#expect(context.viewState.shouldShowNewSoundBanner)
|
||||
|
||||
context.send(viewAction: .dismissNewSoundBanner)
|
||||
XCTAssertFalse(context.viewState.shouldShowBanner)
|
||||
XCTAssertFalse(context.viewState.shouldShowNewSoundBanner)
|
||||
XCTAssertTrue(appSettings.hasSeenNewSoundBanner)
|
||||
#expect(!context.viewState.shouldShowBanner)
|
||||
#expect(!context.viewState.shouldShowNewSoundBanner)
|
||||
#expect(appSettings.hasSeenNewSoundBanner)
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
@@ -382,6 +396,8 @@ class HomeScreenViewModelTests: XCTestCase {
|
||||
enum InviteType { case rooms, spaces }
|
||||
|
||||
private func setupViewModel(securityStatePublisher: CurrentValuePublisher<SessionSecurityState, Never>? = nil, invites: InviteType? = nil) {
|
||||
cancellables.removeAll()
|
||||
|
||||
var rooms: [RoomSummary] = .mockRooms
|
||||
|
||||
switch invites {
|
||||
|
||||
Reference in New Issue
Block a user