mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +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,38 +8,38 @@
|
||||
|
||||
import Combine
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
class UserSessionFlowCoordinatorTests: XCTestCase {
|
||||
var userSessionFlowCoordinator: UserSessionFlowCoordinator!
|
||||
var rootCoordinator: NavigationRootCoordinator!
|
||||
var userIndicatorController: UserIndicatorControllerMock!
|
||||
let stateMachineFactory = PublishedStateMachineFactory()
|
||||
@Suite
|
||||
struct UserSessionFlowCoordinatorTests {
|
||||
private var userSessionFlowCoordinator: UserSessionFlowCoordinator!
|
||||
private var rootCoordinator: NavigationRootCoordinator!
|
||||
private var userIndicatorController: UserIndicatorControllerMock!
|
||||
private let stateMachineFactory = PublishedStateMachineFactory()
|
||||
|
||||
let networkReachabilitySubject: CurrentValueSubject<NetworkMonitorReachability, Never> = .init(.reachable)
|
||||
let homeserverReachabilitySubject: CurrentValueSubject<NetworkMonitorReachability, Never> = .init(.reachable)
|
||||
var cancellables = Set<AnyCancellable>()
|
||||
private let networkReachabilitySubject: CurrentValueSubject<NetworkMonitorReachability, Never> = .init(.reachable)
|
||||
private let homeserverReachabilitySubject: CurrentValueSubject<NetworkMonitorReachability, Never> = .init(.reachable)
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
var tabCoordinator: NavigationTabCoordinator<UserSessionFlowCoordinator.HomeTab>? {
|
||||
private var tabCoordinator: NavigationTabCoordinator<UserSessionFlowCoordinator.HomeTab>? {
|
||||
rootCoordinator?.rootCoordinator as? NavigationTabCoordinator
|
||||
}
|
||||
|
||||
var chatsSplitCoordinator: NavigationSplitCoordinator? {
|
||||
|
||||
private var chatsSplitCoordinator: NavigationSplitCoordinator? {
|
||||
tabCoordinator?.tabCoordinators.first as? NavigationSplitCoordinator
|
||||
}
|
||||
|
||||
var detailCoordinator: CoordinatorProtocol? {
|
||||
|
||||
private var detailCoordinator: CoordinatorProtocol? {
|
||||
chatsSplitCoordinator?.detailCoordinator
|
||||
}
|
||||
|
||||
var detailNavigationStack: NavigationStackCoordinator? {
|
||||
|
||||
private var detailNavigationStack: NavigationStackCoordinator? {
|
||||
detailCoordinator as? NavigationStackCoordinator
|
||||
}
|
||||
|
||||
override func setUp() async throws {
|
||||
cancellables.removeAll()
|
||||
|
||||
init() async throws {
|
||||
rootCoordinator = NavigationRootCoordinator()
|
||||
|
||||
let clientProxy = ClientProxyMock(.init(userID: "hi@bob", roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))))
|
||||
@@ -76,156 +76,166 @@ class UserSessionFlowCoordinatorTests: XCTestCase {
|
||||
|
||||
// MARK: Navigation
|
||||
|
||||
func testInitialState() {
|
||||
XCTAssertNotNil(chatsSplitCoordinator)
|
||||
XCTAssertNil(detailCoordinator)
|
||||
@Test
|
||||
func initialState() {
|
||||
#expect(chatsSplitCoordinator != nil)
|
||||
#expect(detailCoordinator == nil)
|
||||
}
|
||||
|
||||
func testSettingsPresentation() async throws {
|
||||
@Test
|
||||
mutating func settingsPresentation() async throws {
|
||||
try await process(route: .settings, expectedUserSessionState: .settingsScreen)
|
||||
XCTAssertTrue((tabCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is SettingsScreenCoordinator)
|
||||
#expect((tabCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is SettingsScreenCoordinator)
|
||||
}
|
||||
|
||||
func testRoomPresentation() async throws {
|
||||
@Test
|
||||
mutating func roomPresentation() async throws {
|
||||
try await process(route: .room(roomID: "1", via: []), expectedChatsState: .roomList(detailState: .room(roomID: "1")))
|
||||
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertNotNil(detailCoordinator)
|
||||
#expect(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
#expect(detailCoordinator != nil)
|
||||
}
|
||||
|
||||
func testRoomPresentationClearsSettings() async throws {
|
||||
@Test
|
||||
mutating func roomPresentationClearsSettings() async throws {
|
||||
try await process(route: .settings, expectedUserSessionState: .settingsScreen)
|
||||
XCTAssertTrue((tabCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is SettingsScreenCoordinator)
|
||||
XCTAssertNil(detailCoordinator)
|
||||
#expect((tabCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is SettingsScreenCoordinator)
|
||||
#expect(detailCoordinator == nil)
|
||||
|
||||
try await process(route: .room(roomID: "1", via: []), expectedChatsState: .roomList(detailState: .room(roomID: "1")))
|
||||
XCTAssertNil((tabCoordinator?.sheetCoordinator))
|
||||
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertNotNil(detailCoordinator)
|
||||
#expect(tabCoordinator?.sheetCoordinator == nil)
|
||||
#expect(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
#expect(detailCoordinator != nil)
|
||||
}
|
||||
|
||||
func testChildRoomPresentation() async throws {
|
||||
@Test
|
||||
mutating func childRoomPresentation() async throws {
|
||||
try await process(route: .room(roomID: "1", via: []), expectedChatsState: .roomList(detailState: .room(roomID: "1")))
|
||||
let detailNavigationStack = try XCTUnwrap(detailNavigationStack, "There must be a navigation stack.")
|
||||
XCTAssertTrue(detailNavigationStack.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertNotNil(detailCoordinator)
|
||||
let detailNavigationStack = try #require(detailNavigationStack, "There must be a navigation stack.")
|
||||
#expect(detailNavigationStack.rootCoordinator is RoomScreenCoordinator)
|
||||
#expect(detailCoordinator != nil)
|
||||
|
||||
let deferred = deferFulfillment(detailNavigationStack.observe(\.stackCoordinators.count)) { $0 == 1 }
|
||||
try await process(route: .childRoom(roomID: "2", via: []))
|
||||
try await deferred.fulfill()
|
||||
XCTAssertTrue(detailNavigationStack.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertNotNil(detailCoordinator)
|
||||
XCTAssertEqual(detailNavigationStack.stackCoordinators.count, 1)
|
||||
XCTAssertTrue(detailNavigationStack.stackCoordinators.first is RoomScreenCoordinator)
|
||||
#expect(detailNavigationStack.rootCoordinator is RoomScreenCoordinator)
|
||||
#expect(detailCoordinator != nil)
|
||||
#expect(detailNavigationStack.stackCoordinators.count == 1)
|
||||
#expect(detailNavigationStack.stackCoordinators.first is RoomScreenCoordinator)
|
||||
}
|
||||
|
||||
func testShareMediaRouteWithoutRoom() async throws {
|
||||
@Test
|
||||
mutating func shareMediaRouteWithoutRoom() async throws {
|
||||
try await process(route: .settings, expectedUserSessionState: .settingsScreen)
|
||||
XCTAssertTrue((tabCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is SettingsScreenCoordinator)
|
||||
XCTAssertNil(chatsSplitCoordinator?.sheetCoordinator)
|
||||
|
||||
#expect((tabCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is SettingsScreenCoordinator)
|
||||
#expect(chatsSplitCoordinator?.sheetCoordinator == nil)
|
||||
|
||||
let sharePayload: ShareExtensionPayload = .mediaFiles(roomID: nil, mediaFiles: [.init(url: .picturesDirectory, suggestedName: nil)])
|
||||
try await process(route: .share(sharePayload),
|
||||
expectedUserSessionState: .tabBar,
|
||||
expectedChatsState: .shareExtensionRoomList(sharePayload: sharePayload))
|
||||
XCTAssertNil(tabCoordinator?.sheetCoordinator)
|
||||
XCTAssertTrue((chatsSplitCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is RoomSelectionScreenCoordinator)
|
||||
#expect(tabCoordinator?.sheetCoordinator == nil)
|
||||
#expect((chatsSplitCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is RoomSelectionScreenCoordinator)
|
||||
}
|
||||
|
||||
func testShareMediaRouteWithRoom() async throws {
|
||||
@Test
|
||||
mutating func shareMediaRouteWithRoom() async throws {
|
||||
try await process(route: .event(eventID: "1", roomID: "1", via: []), expectedChatsState: .roomList(detailState: .room(roomID: "1")))
|
||||
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertNil(tabCoordinator?.sheetCoordinator)
|
||||
XCTAssertNil(chatsSplitCoordinator?.sheetCoordinator)
|
||||
|
||||
#expect(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
#expect(tabCoordinator?.sheetCoordinator == nil)
|
||||
#expect(chatsSplitCoordinator?.sheetCoordinator == nil)
|
||||
|
||||
let sharePayload: ShareExtensionPayload = .mediaFiles(roomID: "2", mediaFiles: [.init(url: .picturesDirectory, suggestedName: nil)])
|
||||
try await process(route: .share(sharePayload),
|
||||
expectedChatsState: .roomList(detailState: .room(roomID: "2")))
|
||||
|
||||
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertNil(tabCoordinator?.sheetCoordinator)
|
||||
XCTAssertTrue((chatsSplitCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is MediaUploadPreviewScreenCoordinator)
|
||||
|
||||
#expect(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
#expect(tabCoordinator?.sheetCoordinator == nil)
|
||||
#expect((chatsSplitCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is MediaUploadPreviewScreenCoordinator)
|
||||
}
|
||||
|
||||
func testShareTextRouteWithoutRoom() async throws {
|
||||
@Test
|
||||
mutating func shareTextRouteWithoutRoom() async throws {
|
||||
try await process(route: .settings, expectedUserSessionState: .settingsScreen)
|
||||
XCTAssertTrue((tabCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is SettingsScreenCoordinator)
|
||||
XCTAssertNil(chatsSplitCoordinator?.sheetCoordinator)
|
||||
|
||||
#expect((tabCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is SettingsScreenCoordinator)
|
||||
#expect(chatsSplitCoordinator?.sheetCoordinator == nil)
|
||||
|
||||
let sharePayload: ShareExtensionPayload = .text(roomID: nil, text: "Important Text")
|
||||
try await process(route: .share(sharePayload),
|
||||
expectedUserSessionState: .tabBar,
|
||||
expectedChatsState: .shareExtensionRoomList(sharePayload: sharePayload))
|
||||
XCTAssertNil(tabCoordinator?.sheetCoordinator)
|
||||
XCTAssertTrue((chatsSplitCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is RoomSelectionScreenCoordinator)
|
||||
#expect(tabCoordinator?.sheetCoordinator == nil)
|
||||
#expect((chatsSplitCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is RoomSelectionScreenCoordinator)
|
||||
}
|
||||
|
||||
func testShareTextRouteWithRoom() async throws {
|
||||
@Test
|
||||
mutating func shareTextRouteWithRoom() async throws {
|
||||
try await process(route: .event(eventID: "1", roomID: "1", via: []), expectedChatsState: .roomList(detailState: .room(roomID: "1")))
|
||||
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertNil(tabCoordinator?.sheetCoordinator)
|
||||
XCTAssertNil(chatsSplitCoordinator?.sheetCoordinator)
|
||||
|
||||
#expect(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
#expect(tabCoordinator?.sheetCoordinator == nil)
|
||||
#expect(chatsSplitCoordinator?.sheetCoordinator == nil)
|
||||
|
||||
let sharePayload: ShareExtensionPayload = .text(roomID: "2", text: "Important text")
|
||||
try await process(route: .share(sharePayload),
|
||||
expectedChatsState: .roomList(detailState: .room(roomID: "2")))
|
||||
|
||||
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertNil(tabCoordinator?.sheetCoordinator)
|
||||
XCTAssertNil(chatsSplitCoordinator?.sheetCoordinator, "The media upload sheet shouldn't be shown when sharing text.")
|
||||
|
||||
#expect(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
#expect(tabCoordinator?.sheetCoordinator == nil)
|
||||
#expect(chatsSplitCoordinator?.sheetCoordinator == nil, "The media upload sheet shouldn't be shown when sharing text.")
|
||||
}
|
||||
|
||||
// MARK: Indicators
|
||||
|
||||
func testReachabilityIndicators() async throws {
|
||||
@Test
|
||||
func reachabilityIndicators() async throws {
|
||||
// Given a flow in its initial state.
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
|
||||
// Then no reachability indicators should be shown.
|
||||
XCTAssertFalse(userIndicatorController.submitIndicatorDelayCalled)
|
||||
XCTAssertEqual(retractReachabilityIndicatorCallsCount, 1) // The initial state removes the indicator.
|
||||
#expect(!userIndicatorController.submitIndicatorDelayCalled)
|
||||
#expect(retractReachabilityIndicatorCallsCount == 1) // The initial state removes the indicator.
|
||||
|
||||
// When the homeserver becomes unreachable.
|
||||
homeserverReachabilitySubject.send(.unreachable)
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
|
||||
// Then a server unreachable indicator should be shown.
|
||||
XCTAssertEqual(userIndicatorController.submitIndicatorDelayCallsCount, 1)
|
||||
XCTAssertEqual(userIndicatorController.submitIndicatorDelayReceivedArguments?.indicator.title, L10n.commonServerUnreachable)
|
||||
XCTAssertEqual(retractReachabilityIndicatorCallsCount, 1)
|
||||
#expect(userIndicatorController.submitIndicatorDelayCallsCount == 1)
|
||||
#expect(userIndicatorController.submitIndicatorDelayReceivedArguments?.indicator.title == L10n.commonServerUnreachable)
|
||||
#expect(retractReachabilityIndicatorCallsCount == 1)
|
||||
|
||||
// When the network also becomes unreachable.
|
||||
networkReachabilitySubject.send(.unreachable)
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
|
||||
// Then the server unreachable indicator should be replaced with an offline indicator.
|
||||
XCTAssertEqual(userIndicatorController.submitIndicatorDelayCallsCount, 2)
|
||||
XCTAssertEqual(userIndicatorController.submitIndicatorDelayReceivedArguments?.indicator.title, L10n.commonOffline)
|
||||
XCTAssertEqual(retractReachabilityIndicatorCallsCount, 1)
|
||||
#expect(userIndicatorController.submitIndicatorDelayCallsCount == 2)
|
||||
#expect(userIndicatorController.submitIndicatorDelayReceivedArguments?.indicator.title == L10n.commonOffline)
|
||||
#expect(retractReachabilityIndicatorCallsCount == 1)
|
||||
|
||||
// When the homeserver becomes reachable again.
|
||||
homeserverReachabilitySubject.send(.reachable)
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
|
||||
// Then there should still be an offline indicator (as we don't yet support air-gapped servers on iOS).
|
||||
XCTAssertEqual(userIndicatorController.submitIndicatorDelayCallsCount, 3)
|
||||
XCTAssertEqual(userIndicatorController.submitIndicatorDelayReceivedArguments?.indicator.title, L10n.commonOffline)
|
||||
XCTAssertEqual(retractReachabilityIndicatorCallsCount, 1)
|
||||
#expect(userIndicatorController.submitIndicatorDelayCallsCount == 3)
|
||||
#expect(userIndicatorController.submitIndicatorDelayReceivedArguments?.indicator.title == L10n.commonOffline)
|
||||
#expect(retractReachabilityIndicatorCallsCount == 1)
|
||||
|
||||
// When the network becomes reachable again.
|
||||
networkReachabilitySubject.send(.reachable)
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
|
||||
// Then the indicator should be hidden now as everything is back to normal
|
||||
XCTAssertEqual(userIndicatorController.submitIndicatorDelayCallsCount, 3)
|
||||
XCTAssertEqual(retractReachabilityIndicatorCallsCount, 2)
|
||||
#expect(userIndicatorController.submitIndicatorDelayCallsCount == 3)
|
||||
#expect(retractReachabilityIndicatorCallsCount == 2)
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func process(route: AppRoute,
|
||||
expectedUserSessionState: UserSessionFlowCoordinator.State? = nil,
|
||||
expectedChatsState: ChatsTabFlowCoordinatorStateMachine.State? = nil) async throws {
|
||||
let deferredUserSession: DeferredFulfillment? = if let expectedUserSessionState {
|
||||
private mutating func process(route: AppRoute,
|
||||
expectedUserSessionState: UserSessionFlowCoordinator.State? = nil,
|
||||
expectedChatsState: ChatsTabFlowCoordinatorStateMachine.State? = nil) async throws {
|
||||
let deferredUserSession: DeferredFulfillment<UserSessionFlowCoordinator.State>? = if let expectedUserSessionState {
|
||||
deferFulfillment(stateMachineFactory.userSessionFlowStatePublisher.delay(for: .milliseconds(100), scheduler: DispatchQueue.main)) {
|
||||
$0 == expectedUserSessionState
|
||||
}
|
||||
@@ -233,7 +243,7 @@ class UserSessionFlowCoordinatorTests: XCTestCase {
|
||||
nil
|
||||
}
|
||||
|
||||
let deferredChatsState: DeferredFulfillment? = if let expectedChatsState {
|
||||
let deferredChatsState: DeferredFulfillment<ChatsTabFlowCoordinatorStateMachine.State>? = if let expectedChatsState {
|
||||
deferFulfillment(stateMachineFactory.chatsTabFlowStatePublisher.delay(for: .milliseconds(100), scheduler: DispatchQueue.main)) {
|
||||
$0 == expectedChatsState
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user