Finish migration of UnitTests target from XCTestCase to Swift Testing (#5129)
* Initial plan * Migrate 3 test files from XCTest to Swift Testing - MediaUploadPreviewScreenViewModelTests: @MainActor @Suite struct with init(), BundleFinder class for Bundle(for:), mutating test/setup functions, [self] capture replacing [weak self] in closures - NotificationManagerTests: @MainActor @Suite final class with init()/deinit, expectation/fulfillment(of:) replaced with confirmation(...), test_ prefix stripped - NotificationSettingsScreenViewModelTests: @MainActor @Suite struct with init() throws, non-optional stored properties, test prefix stripped Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Migrate 3 XCTest files to Swift Testing - NotificationSettingsEditScreenViewModelTests: @MainActor @Suite struct with init() throws, mutating test methods - TimelineViewModelTests: @MainActor @Suite final class with init() async throws + deinit - AttributedStringBuilderTests: @Suite struct with init() async throws All XCT assertions replaced with #expect/#require/Issue.record Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Migrate 4 test files from XCTest to Swift Testing - TimelineMediaPreviewViewModelTests: @Suite struct, mutating @Test funcs, testLoadingItem renamed to loadingItem (called internally by other tests) - ServerConfirmationScreenViewModelTests: @Suite final class with init()/deinit - CompletionSuggestionServiceTests: @Suite struct with init() - RoomFlowCoordinatorTests: @Suite final class with deinit Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Migrate 4 test files from XCTest to Swift Testing - VoiceMessageRecorderTests: @Suite struct with init() async throws, added BundleFinder class for Bundle lookup, migrated all assertions - SpaceScreenViewModelTests: @Suite struct, private mutating setupViewModel, all test funcs mutating, XCTestExpectation → confirmation - RoomNotificationSettingsScreenViewModelTests: @Suite struct with init() throws, cancellable tests marked mutating - JoinRoomScreenViewModelTests: @Suite final class with init()/deinit, XCTestExpectation → confirmation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Migrate 6 test files from XCTestCase to Swift Testing Co-authored-by: pixlwave <6060466+pixlwave@users.noreply.github.com> * Fix trailing blank line in RoomPollsHistoryScreenViewModelTests Co-authored-by: pixlwave <6060466+pixlwave@users.noreply.github.com> * Migrate 3 test files from XCTest to Swift Testing - MediaUploadingPreprocessorTests: @Suite final class with init()/deinit, removed executionTimeAllowance, XCTAssertEqual(accuracy:) → abs(Double) - SecurityAndPrivacyScreenViewModelTests: @MainActor @Suite final class, 5 expectation+fulfillment → await confirmation(...) - CreateRoomViewModelTests: @MainActor @Suite final class, 4 expectation+fulfillment → await confirmation(...) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Migrate RoomScreenViewModelTests and RoomDetailsScreenViewModelTests to Swift Testing - Replace XCTest with Testing framework - RoomScreenViewModelTests: final class with init() async throws + deinit - RoomDetailsScreenViewModelTests: struct with init() and mutating funcs - Convert XCT assertions to #expect / Issue.record - Convert XCTestExpectation patterns to confirmation { confirm in } - Strip 'test' prefix from all test function names Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Migrate ComposerToolbarViewModelTests from XCTest to Swift Testing - Replace import XCTest with import Testing - Convert XCTestCase class to @MainActor @Suite final class - Replace setUp()/tearDown() with init()/deinit - Strip 'test' prefix from all 41 test method names and add @Test - Replace XCTAssert* with #expect()/#require() - Replace try XCTUnwrap() with try #require() - Convert expectation+wait patterns to deferFulfillment with PassthroughSubject - Convert isInverted expectation to boolean flag checked after await - Use deferFulfillment on $viewState for state-transition tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address comments with Copilot. * Fix the failing tests. * Fixed flaky tests (#5137) resolved flaky tests * Tweaks and fixes. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: pixlwave <6060466+pixlwave@users.noreply.github.com> Co-authored-by: Doug <douglase@element.io> Co-authored-by: Mauro <34335419+Velin92@users.noreply.github.com>
This commit is contained in:
@@ -26,9 +26,12 @@ extension SessionVerificationControllerProxyMock {
|
||||
|
||||
mock.acknowledgeVerificationRequestDetailsReturnValue = .success(())
|
||||
|
||||
mock.requestDeviceVerificationClosure = { [unowned mock] in
|
||||
mock.requestDeviceVerificationClosure = { [weak mock] in
|
||||
Task.detached {
|
||||
guard let mock else { return }
|
||||
|
||||
try await Task.sleep(for: requestDelay)
|
||||
|
||||
mock.actions.send(.acceptedVerificationRequest)
|
||||
|
||||
if otherDeviceStartsSasVerification {
|
||||
@@ -42,8 +45,10 @@ extension SessionVerificationControllerProxyMock {
|
||||
return .success(())
|
||||
}
|
||||
|
||||
mock.startSasVerificationClosure = { [unowned mock] in
|
||||
mock.startSasVerificationClosure = { [weak mock] in
|
||||
Task.detached {
|
||||
guard let mock else { return }
|
||||
|
||||
try await Task.sleep(for: requestDelay)
|
||||
mock.actions.send(.startedSasVerification)
|
||||
|
||||
@@ -56,8 +61,10 @@ extension SessionVerificationControllerProxyMock {
|
||||
return .success(())
|
||||
}
|
||||
|
||||
mock.approveVerificationClosure = { [unowned mock] in
|
||||
mock.approveVerificationClosure = { [weak mock] in
|
||||
Task.detached {
|
||||
guard let mock else { return }
|
||||
|
||||
try await Task.sleep(for: requestDelay)
|
||||
mock.actions.send(.finished)
|
||||
}
|
||||
@@ -65,8 +72,10 @@ extension SessionVerificationControllerProxyMock {
|
||||
return .success(())
|
||||
}
|
||||
|
||||
mock.declineVerificationClosure = { [unowned mock] in
|
||||
mock.declineVerificationClosure = { [weak mock] in
|
||||
Task.detached {
|
||||
guard let mock else { return }
|
||||
|
||||
try await Task.sleep(for: requestDelay)
|
||||
mock.actions.send(.cancelled)
|
||||
}
|
||||
@@ -74,8 +83,10 @@ extension SessionVerificationControllerProxyMock {
|
||||
return .success(())
|
||||
}
|
||||
|
||||
mock.cancelVerificationClosure = { [unowned mock] in
|
||||
mock.cancelVerificationClosure = { [weak mock] in
|
||||
Task.detached {
|
||||
guard let mock else { return }
|
||||
|
||||
try await Task.sleep(for: requestDelay)
|
||||
mock.actions.send(.cancelled)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
typealias RoomNotificationSettingsScreenViewModelType = StateStoreViewModel<RoomNotificationSettingsScreenViewState, RoomNotificationSettingsScreenViewAction>
|
||||
typealias RoomNotificationSettingsScreenViewModelType = StateStoreViewModelV2<RoomNotificationSettingsScreenViewState, RoomNotificationSettingsScreenViewAction>
|
||||
|
||||
class RoomNotificationSettingsScreenViewModel: RoomNotificationSettingsScreenViewModelType, RoomNotificationSettingsScreenViewModelProtocol {
|
||||
private let actionsSubject: PassthroughSubject<RoomNotificationSettingsScreenViewModelAction, Never> = .init()
|
||||
@@ -115,7 +115,9 @@ class RoomNotificationSettingsScreenViewModel: RoomNotificationSettingsScreenVie
|
||||
} catch {
|
||||
displayError(.restoreDefaultFailed)
|
||||
}
|
||||
state.isRestoringDefaultSetting = false
|
||||
await MainActor.run {
|
||||
state.isRestoringDefaultSetting = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +136,9 @@ class RoomNotificationSettingsScreenViewModel: RoomNotificationSettingsScreenVie
|
||||
} catch {
|
||||
displayError(.setModeFailed)
|
||||
}
|
||||
state.pendingCustomMode = nil
|
||||
await MainActor.run {
|
||||
state.pendingCustomMode = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import Compound
|
||||
import SwiftUI
|
||||
|
||||
struct RoomNotificationSettingsCustomSectionView: View {
|
||||
@ObservedObject var context: RoomNotificationSettingsScreenViewModel.Context
|
||||
@Bindable var context: RoomNotificationSettingsScreenViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
Section {
|
||||
|
||||
@@ -10,7 +10,7 @@ import Compound
|
||||
import SwiftUI
|
||||
|
||||
struct RoomNotificationSettingsScreen: View {
|
||||
@ObservedObject var context: RoomNotificationSettingsScreenViewModel.Context
|
||||
@Bindable var context: RoomNotificationSettingsScreenViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
|
||||
@@ -10,7 +10,7 @@ import Compound
|
||||
import SwiftUI
|
||||
|
||||
struct RoomNotificationSettingsUserDefinedScreen: View {
|
||||
@ObservedObject var context: RoomNotificationSettingsScreenViewModel.Context
|
||||
@Bindable var context: RoomNotificationSettingsScreenViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
enum RoomNotificationModeProxy: String, CaseIterable {
|
||||
enum RoomNotificationModeProxy: String, CaseIterable, Equatable {
|
||||
case allMessages
|
||||
case mentionsAndKeywordsOnly
|
||||
case mute
|
||||
|
||||
@@ -20,7 +20,7 @@ enum SessionVerificationControllerProxyError: Error {
|
||||
case failedCancellingVerification
|
||||
}
|
||||
|
||||
enum SessionVerificationControllerProxyAction {
|
||||
enum SessionVerificationControllerProxyAction: Equatable {
|
||||
case receivedVerificationRequest(details: SessionVerificationRequestDetails)
|
||||
case acceptedVerificationRequest
|
||||
case startedSasVerification
|
||||
@@ -30,7 +30,7 @@ enum SessionVerificationControllerProxyAction {
|
||||
case failed
|
||||
}
|
||||
|
||||
struct SessionVerificationRequestDetails {
|
||||
struct SessionVerificationRequestDetails: Equatable {
|
||||
let senderProfile: UserProfileProxy
|
||||
let flowID: String
|
||||
let deviceID: String
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -54,6 +54,7 @@ final class BugReportServiceTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@MainActor
|
||||
func initialStateWithRealService() {
|
||||
let urlPublisher: CurrentValueSubject<RageshakeConfiguration, Never> = .init(.url("https://example.com/submit"))
|
||||
let service = BugReportService(rageshakeURLPublisher: urlPublisher.asCurrentValuePublisher(),
|
||||
|
||||
@@ -8,10 +8,12 @@
|
||||
|
||||
import Combine
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
@Suite
|
||||
@MainActor
|
||||
class ChatsTabFlowCoordinatorTests: XCTestCase {
|
||||
struct ChatsTabFlowCoordinatorTests {
|
||||
var clientProxy: ClientProxyMock!
|
||||
var timelineControllerFactory: TimelineControllerFactoryMock!
|
||||
var chatsTabFlowCoordinator: ChatsTabFlowCoordinator!
|
||||
@@ -22,15 +24,14 @@ class ChatsTabFlowCoordinatorTests: XCTestCase {
|
||||
var cancellables = Set<AnyCancellable>()
|
||||
|
||||
var detailCoordinator: CoordinatorProtocol? {
|
||||
splitCoordinator?.detailCoordinator
|
||||
splitCoordinator.detailCoordinator
|
||||
}
|
||||
|
||||
var detailNavigationStack: NavigationStackCoordinator? {
|
||||
detailCoordinator as? NavigationStackCoordinator
|
||||
}
|
||||
|
||||
override func setUp() async throws {
|
||||
cancellables.removeAll()
|
||||
init() async throws {
|
||||
clientProxy = ClientProxyMock(.init(userID: "hi@bob", roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))))
|
||||
timelineControllerFactory = TimelineControllerFactoryMock(.init())
|
||||
|
||||
@@ -60,224 +61,234 @@ class ChatsTabFlowCoordinatorTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testRoomPresentation() async throws {
|
||||
@Test
|
||||
mutating func roomPresentation() async throws {
|
||||
try await process(route: .room(roomID: "1", via: []), expectedState: .roomList(detailState: .room(roomID: "1")))
|
||||
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertNotNil(detailCoordinator)
|
||||
#expect(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
#expect(detailCoordinator != nil)
|
||||
|
||||
try await process(route: .roomList, expectedState: .roomList(detailState: nil))
|
||||
XCTAssertNil(detailNavigationStack?.rootCoordinator)
|
||||
XCTAssertNil(detailCoordinator)
|
||||
#expect(detailNavigationStack?.rootCoordinator == nil)
|
||||
#expect(detailCoordinator == nil)
|
||||
|
||||
try await process(route: .room(roomID: "1", via: []), expectedState: .roomList(detailState: .room(roomID: "1")))
|
||||
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertNotNil(detailCoordinator)
|
||||
#expect(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
#expect(detailCoordinator != nil)
|
||||
|
||||
try await process(route: .room(roomID: "2", via: []), expectedState: .roomList(detailState: .room(roomID: "2")))
|
||||
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertNotNil(detailCoordinator)
|
||||
#expect(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
#expect(detailCoordinator != nil)
|
||||
|
||||
try await process(route: .roomList, expectedState: .roomList(detailState: nil))
|
||||
XCTAssertNil(detailNavigationStack?.rootCoordinator)
|
||||
XCTAssertNil(detailCoordinator)
|
||||
#expect(detailNavigationStack?.rootCoordinator == nil)
|
||||
#expect(detailCoordinator == nil)
|
||||
|
||||
XCTAssertEqual(notificationManager.removeDeliveredMessageNotificationsForReceivedInvocations, ["1", "1", "2"])
|
||||
#expect(notificationManager.removeDeliveredMessageNotificationsForReceivedInvocations == ["1", "1", "2"])
|
||||
}
|
||||
|
||||
func testRoomAliasPresentation() async throws {
|
||||
@Test
|
||||
mutating func roomAliasPresentation() async throws {
|
||||
clientProxy.resolveRoomAliasReturnValue = .success(.init(roomId: "1", servers: []))
|
||||
|
||||
try await process(route: .roomAlias("#alias:matrix.org"), expectedState: .roomList(detailState: .room(roomID: "1")))
|
||||
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertNotNil(detailCoordinator)
|
||||
#expect(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
#expect(detailCoordinator != nil)
|
||||
|
||||
try await process(route: .roomList, expectedState: .roomList(detailState: nil))
|
||||
XCTAssertNil(detailNavigationStack?.rootCoordinator)
|
||||
XCTAssertNil(detailCoordinator)
|
||||
#expect(detailNavigationStack?.rootCoordinator == nil)
|
||||
#expect(detailCoordinator == nil)
|
||||
|
||||
try await process(route: .room(roomID: "1", via: []), expectedState: .roomList(detailState: .room(roomID: "1")))
|
||||
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertNotNil(detailCoordinator)
|
||||
#expect(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
#expect(detailCoordinator != nil)
|
||||
|
||||
clientProxy.resolveRoomAliasReturnValue = .success(.init(roomId: "2", servers: []))
|
||||
|
||||
try await process(route: .room(roomID: "2", via: []), expectedState: .roomList(detailState: .room(roomID: "2")))
|
||||
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertNotNil(detailCoordinator)
|
||||
#expect(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
#expect(detailCoordinator != nil)
|
||||
|
||||
try await process(route: .roomList, expectedState: .roomList(detailState: nil))
|
||||
XCTAssertNil(detailNavigationStack?.rootCoordinator)
|
||||
XCTAssertNil(detailCoordinator)
|
||||
#expect(detailNavigationStack?.rootCoordinator == nil)
|
||||
#expect(detailCoordinator == nil)
|
||||
|
||||
XCTAssertEqual(notificationManager.removeDeliveredMessageNotificationsForReceivedInvocations, ["1", "1", "2"])
|
||||
#expect(notificationManager.removeDeliveredMessageNotificationsForReceivedInvocations == ["1", "1", "2"])
|
||||
}
|
||||
|
||||
func testRoomDetailsPresentation() async throws {
|
||||
@Test
|
||||
mutating func roomDetailsPresentation() async throws {
|
||||
try await process(route: .roomDetails(roomID: "1"), expectedState: .roomList(detailState: .room(roomID: "1")))
|
||||
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomDetailsScreenCoordinator)
|
||||
XCTAssertNotNil(detailCoordinator)
|
||||
#expect(detailNavigationStack?.rootCoordinator is RoomDetailsScreenCoordinator)
|
||||
#expect(detailCoordinator != nil)
|
||||
|
||||
try await process(route: .roomList, expectedState: .roomList(detailState: nil))
|
||||
XCTAssertNil(detailNavigationStack?.rootCoordinator)
|
||||
XCTAssertNil(detailCoordinator)
|
||||
#expect(detailNavigationStack?.rootCoordinator == nil)
|
||||
#expect(detailCoordinator == nil)
|
||||
}
|
||||
|
||||
func testStackUnwinding() async throws {
|
||||
@Test
|
||||
mutating func stackUnwinding() async throws {
|
||||
try await process(route: .roomDetails(roomID: "1"), expectedState: .roomList(detailState: .room(roomID: "1")))
|
||||
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomDetailsScreenCoordinator)
|
||||
XCTAssertNotNil(detailCoordinator)
|
||||
#expect(detailNavigationStack?.rootCoordinator is RoomDetailsScreenCoordinator)
|
||||
#expect(detailCoordinator != nil)
|
||||
|
||||
try await process(route: .room(roomID: "2", via: []), expectedState: .roomList(detailState: .room(roomID: "2")))
|
||||
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertNotNil(detailCoordinator)
|
||||
#expect(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
#expect(detailCoordinator != nil)
|
||||
}
|
||||
|
||||
func testNoOp() async throws {
|
||||
@Test
|
||||
mutating func noOp() async throws {
|
||||
try await process(route: .roomDetails(roomID: "1"), expectedState: .roomList(detailState: .room(roomID: "1")))
|
||||
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomDetailsScreenCoordinator)
|
||||
XCTAssertNotNil(detailCoordinator)
|
||||
#expect(detailNavigationStack?.rootCoordinator is RoomDetailsScreenCoordinator)
|
||||
#expect(detailCoordinator != nil)
|
||||
|
||||
let unexpectedFulfillment = deferFailure(stateMachineFactory.chatsTabFlowStatePublisher, timeout: 1) { _ in true }
|
||||
let unexpectedFulfillment = deferFailure(stateMachineFactory.chatsTabFlowStatePublisher, timeout: .seconds(1)) { _ in true }
|
||||
chatsTabFlowCoordinator.handleAppRoute(.roomDetails(roomID: "1"), animated: true)
|
||||
try await unexpectedFulfillment.fulfill()
|
||||
|
||||
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomDetailsScreenCoordinator)
|
||||
XCTAssertNotNil(detailCoordinator)
|
||||
#expect(detailNavigationStack?.rootCoordinator is RoomDetailsScreenCoordinator)
|
||||
#expect(detailCoordinator != nil)
|
||||
}
|
||||
|
||||
func testSwitchToDifferentDetails() async throws {
|
||||
@Test
|
||||
mutating func switchToDifferentDetails() async throws {
|
||||
try await process(route: .roomDetails(roomID: "1"), expectedState: .roomList(detailState: .room(roomID: "1")))
|
||||
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomDetailsScreenCoordinator)
|
||||
XCTAssertNotNil(detailCoordinator)
|
||||
#expect(detailNavigationStack?.rootCoordinator is RoomDetailsScreenCoordinator)
|
||||
#expect(detailCoordinator != nil)
|
||||
|
||||
try await process(route: .roomDetails(roomID: "2"), expectedState: .roomList(detailState: .room(roomID: "2")))
|
||||
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomDetailsScreenCoordinator)
|
||||
XCTAssertNotNil(detailCoordinator)
|
||||
#expect(detailNavigationStack?.rootCoordinator is RoomDetailsScreenCoordinator)
|
||||
#expect(detailCoordinator != nil)
|
||||
}
|
||||
|
||||
func testPushDetails() async throws {
|
||||
@Test
|
||||
mutating func pushDetails() async throws {
|
||||
try await process(route: .room(roomID: "1", via: []), expectedState: .roomList(detailState: .room(roomID: "1")))
|
||||
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertNotNil(detailCoordinator)
|
||||
#expect(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
#expect(detailCoordinator != nil)
|
||||
|
||||
let unexpectedFulfillment = deferFailure(stateMachineFactory.chatsTabFlowStatePublisher, timeout: 1) { _ in true }
|
||||
let unexpectedFulfillment = deferFailure(stateMachineFactory.chatsTabFlowStatePublisher, timeout: .seconds(1)) { _ in true }
|
||||
chatsTabFlowCoordinator.handleAppRoute(.roomDetails(roomID: "1"), animated: true)
|
||||
try await unexpectedFulfillment.fulfill()
|
||||
|
||||
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertEqual(detailNavigationStack?.stackCoordinators.count, 1)
|
||||
XCTAssertTrue(detailNavigationStack?.stackCoordinators.first is RoomDetailsScreenCoordinator)
|
||||
XCTAssertNotNil(detailCoordinator)
|
||||
#expect(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
#expect(detailNavigationStack?.stackCoordinators.count == 1)
|
||||
#expect(detailNavigationStack?.stackCoordinators.first is RoomDetailsScreenCoordinator)
|
||||
#expect(detailCoordinator != nil)
|
||||
}
|
||||
|
||||
func testReplaceDetailsWithTimeline() async throws {
|
||||
@Test
|
||||
mutating func replaceDetailsWithTimeline() async throws {
|
||||
try await process(route: .roomDetails(roomID: "1"), expectedState: .roomList(detailState: .room(roomID: "1")))
|
||||
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomDetailsScreenCoordinator)
|
||||
XCTAssertNotNil(detailCoordinator)
|
||||
#expect(detailNavigationStack?.rootCoordinator is RoomDetailsScreenCoordinator)
|
||||
#expect(detailCoordinator != nil)
|
||||
|
||||
try await process(route: .room(roomID: "1", via: []), expectedState: .roomList(detailState: .room(roomID: "1")))
|
||||
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertNotNil(detailCoordinator)
|
||||
#expect(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
#expect(detailCoordinator != nil)
|
||||
}
|
||||
|
||||
func testUserProfileClearsStack() async throws {
|
||||
@Test
|
||||
mutating func userProfileClearsStack() async throws {
|
||||
try await process(route: .roomDetails(roomID: "1"), expectedState: .roomList(detailState: .room(roomID: "1")))
|
||||
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomDetailsScreenCoordinator)
|
||||
XCTAssertNotNil(detailCoordinator)
|
||||
XCTAssertNil(splitCoordinator?.sheetCoordinator)
|
||||
#expect(detailNavigationStack?.rootCoordinator is RoomDetailsScreenCoordinator)
|
||||
#expect(detailCoordinator != nil)
|
||||
#expect(splitCoordinator.sheetCoordinator == nil)
|
||||
|
||||
try await process(route: .userProfile(userID: "alice"), expectedState: .userProfileScreen)
|
||||
XCTAssertNil(detailNavigationStack?.rootCoordinator)
|
||||
guard let sheetStackCoordinator = splitCoordinator?.sheetCoordinator as? NavigationStackCoordinator else {
|
||||
XCTFail("There should be a navigation stack presented as a sheet.")
|
||||
return
|
||||
}
|
||||
XCTAssertTrue(sheetStackCoordinator.rootCoordinator is UserProfileScreenCoordinator)
|
||||
#expect(detailNavigationStack?.rootCoordinator == nil)
|
||||
let sheetStackCoordinator = try #require(splitCoordinator.sheetCoordinator as? NavigationStackCoordinator, "There should be a navigation stack presented as a sheet.")
|
||||
#expect(sheetStackCoordinator.rootCoordinator is UserProfileScreenCoordinator)
|
||||
}
|
||||
|
||||
func testRoomClearsStack() async throws {
|
||||
@Test
|
||||
mutating func roomClearsStack() async throws {
|
||||
try await process(route: .room(roomID: "1", via: []), expectedState: .roomList(detailState: .room(roomID: "1")))
|
||||
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertEqual(detailNavigationStack?.stackCoordinators.count, 0)
|
||||
XCTAssertNotNil(detailCoordinator)
|
||||
#expect(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
#expect(detailNavigationStack?.stackCoordinators.count == 0)
|
||||
#expect(detailCoordinator != nil)
|
||||
|
||||
chatsTabFlowCoordinator.handleAppRoute(.childRoom(roomID: "2", via: []), animated: true)
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertEqual(detailNavigationStack?.stackCoordinators.count, 1)
|
||||
XCTAssertTrue(detailNavigationStack?.stackCoordinators.first is RoomScreenCoordinator)
|
||||
XCTAssertNotNil(detailCoordinator)
|
||||
#expect(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
#expect(detailNavigationStack?.stackCoordinators.count == 1)
|
||||
#expect(detailNavigationStack?.stackCoordinators.first is RoomScreenCoordinator)
|
||||
#expect(detailCoordinator != nil)
|
||||
|
||||
try await process(route: .room(roomID: "3", via: []), expectedState: .roomList(detailState: .room(roomID: "3")))
|
||||
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertEqual(detailNavigationStack?.stackCoordinators.count, 0)
|
||||
XCTAssertNotNil(detailCoordinator)
|
||||
#expect(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
#expect(detailNavigationStack?.stackCoordinators.count == 0)
|
||||
#expect(detailCoordinator != nil)
|
||||
}
|
||||
|
||||
func testEventRoutes() async throws {
|
||||
@Test
|
||||
mutating func eventRoutes() async throws {
|
||||
// A regular event route should set its room as the root of the stack and focus on the event.
|
||||
try await process(route: .event(eventID: "1", roomID: "1", via: []), expectedState: .roomList(detailState: .room(roomID: "1")))
|
||||
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertEqual(detailNavigationStack?.stackCoordinators.count, 0)
|
||||
XCTAssertNotNil(detailCoordinator)
|
||||
XCTAssertEqual(timelineControllerFactory.buildTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderCallsCount, 1)
|
||||
XCTAssertEqual(timelineControllerFactory.buildTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderReceivedArguments?.initialFocussedEventID, "1")
|
||||
#expect(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
#expect(detailNavigationStack?.stackCoordinators.count == 0)
|
||||
#expect(detailCoordinator != nil)
|
||||
#expect(timelineControllerFactory.buildTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderCallsCount == 1)
|
||||
#expect(timelineControllerFactory.buildTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderReceivedArguments?.initialFocussedEventID == "1")
|
||||
|
||||
// A child event route should push a new room screen onto the stack and focus on the event.
|
||||
chatsTabFlowCoordinator.handleAppRoute(.childEvent(eventID: "2", roomID: "2", via: []), animated: true)
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertEqual(detailNavigationStack?.stackCoordinators.count, 1)
|
||||
XCTAssertTrue(detailNavigationStack?.stackCoordinators.first is RoomScreenCoordinator)
|
||||
XCTAssertNotNil(detailCoordinator)
|
||||
XCTAssertEqual(timelineControllerFactory.buildTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderCallsCount, 2)
|
||||
XCTAssertEqual(timelineControllerFactory.buildTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderReceivedArguments?.initialFocussedEventID, "2")
|
||||
#expect(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
#expect(detailNavigationStack?.stackCoordinators.count == 1)
|
||||
#expect(detailNavigationStack?.stackCoordinators.first is RoomScreenCoordinator)
|
||||
#expect(detailCoordinator != nil)
|
||||
#expect(timelineControllerFactory.buildTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderCallsCount == 2)
|
||||
#expect(timelineControllerFactory.buildTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderReceivedArguments?.initialFocussedEventID == "2")
|
||||
|
||||
// A subsequent regular event route should clear the stack and set the new room as the root of the stack.
|
||||
try await process(route: .event(eventID: "3", roomID: "3", via: []), expectedState: .roomList(detailState: .room(roomID: "3")))
|
||||
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertEqual(detailNavigationStack?.stackCoordinators.count, 0)
|
||||
XCTAssertNotNil(detailCoordinator)
|
||||
XCTAssertEqual(timelineControllerFactory.buildTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderCallsCount, 3)
|
||||
XCTAssertEqual(timelineControllerFactory.buildTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderReceivedArguments?.initialFocussedEventID, "3")
|
||||
#expect(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
#expect(detailNavigationStack?.stackCoordinators.count == 0)
|
||||
#expect(detailCoordinator != nil)
|
||||
#expect(timelineControllerFactory.buildTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderCallsCount == 3)
|
||||
#expect(timelineControllerFactory.buildTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderReceivedArguments?.initialFocussedEventID == "3")
|
||||
|
||||
// A regular event route for the same room should set a new instance of the room as the root of the stack.
|
||||
try await process(route: .event(eventID: "4", roomID: "3", via: []), expectedState: .roomList(detailState: .room(roomID: "3")))
|
||||
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertEqual(detailNavigationStack?.stackCoordinators.count, 0)
|
||||
XCTAssertNotNil(detailCoordinator)
|
||||
XCTAssertEqual(timelineControllerFactory.buildTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderCallsCount, 4)
|
||||
XCTAssertEqual(timelineControllerFactory.buildTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderReceivedArguments?.initialFocussedEventID, "4",
|
||||
"A new timeline should be created for the same room ID, so that the screen isn't stale while loading.")
|
||||
#expect(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
#expect(detailNavigationStack?.stackCoordinators.count == 0)
|
||||
#expect(detailCoordinator != nil)
|
||||
#expect(timelineControllerFactory.buildTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderCallsCount == 4)
|
||||
#expect(timelineControllerFactory.buildTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderReceivedArguments?.initialFocussedEventID == "4",
|
||||
"A new timeline should be created for the same room ID, so that the screen isn't stale while loading.")
|
||||
}
|
||||
|
||||
func testShareMediaRouteWithRoom() async throws {
|
||||
@Test
|
||||
mutating func shareMediaRouteWithRoom() async throws {
|
||||
try await process(route: .event(eventID: "1", roomID: "1", via: []), expectedState: .roomList(detailState: .room(roomID: "1")))
|
||||
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
#expect(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
|
||||
let sharePayload: ShareExtensionPayload = .mediaFiles(roomID: "2", mediaFiles: [.init(url: .picturesDirectory, suggestedName: nil)])
|
||||
try await process(route: .share(sharePayload),
|
||||
expectedState: .roomList(detailState: .room(roomID: "2")))
|
||||
|
||||
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertTrue((splitCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is MediaUploadPreviewScreenCoordinator)
|
||||
#expect(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
#expect((splitCoordinator.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is MediaUploadPreviewScreenCoordinator)
|
||||
}
|
||||
|
||||
func testShareTextRouteWithRoom() async throws {
|
||||
@Test
|
||||
mutating func shareTextRouteWithRoom() async throws {
|
||||
try await process(route: .event(eventID: "1", roomID: "1", via: []), expectedState: .roomList(detailState: .room(roomID: "1")))
|
||||
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
#expect(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
|
||||
let sharePayload: ShareExtensionPayload = .text(roomID: "2", text: "Important text")
|
||||
try await process(route: .share(sharePayload),
|
||||
expectedState: .roomList(detailState: .room(roomID: "2")))
|
||||
|
||||
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertNil(splitCoordinator?.sheetCoordinator, "The media upload sheet shouldn't be shown when sharing text.")
|
||||
#expect(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
#expect(splitCoordinator.sheetCoordinator == nil, "The media upload sheet shouldn't be shown when sharing text.")
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func process(route: AppRoute, expectedState: ChatsTabFlowCoordinatorStateMachine.State) async throws {
|
||||
private mutating func process(route: AppRoute, expectedState: ChatsTabFlowCoordinatorStateMachine.State) async throws {
|
||||
// Sometimes the state machine's state changes before the coordinators have updated the stack.
|
||||
let delayedPublisher = stateMachineFactory.chatsTabFlowStatePublisher.delay(for: .milliseconds(100), scheduler: DispatchQueue.main)
|
||||
|
||||
|
||||
@@ -8,17 +8,13 @@
|
||||
|
||||
import Combine
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@Suite
|
||||
@MainActor
|
||||
final class CompletionSuggestionServiceTests: XCTestCase {
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
override func setUp() {
|
||||
cancellables.removeAll()
|
||||
}
|
||||
|
||||
func testUserSuggestions() async throws {
|
||||
struct CompletionSuggestionServiceTests {
|
||||
@Test
|
||||
func userSuggestions() async throws {
|
||||
let alice: RoomMemberProxyMock = .mockAlice
|
||||
let members: [RoomMemberProxyMock] = [alice, .mockBob, .mockCharlie, .mockMe]
|
||||
let roomProxyMock = JoinedRoomProxyMock(.init(id: "roomID", name: "test", members: members))
|
||||
@@ -57,7 +53,8 @@ final class CompletionSuggestionServiceTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testUserSuggestionsIncludingAllUsers() async throws {
|
||||
@Test
|
||||
func userSuggestionsIncludingAllUsers() async throws {
|
||||
let alice: RoomMemberProxyMock = .mockAlice
|
||||
let members: [RoomMemberProxyMock] = [alice, .mockBob, .mockCharlie, .mockMe]
|
||||
let roomProxyMock = JoinedRoomProxyMock(.init(id: "roomID",
|
||||
@@ -88,7 +85,8 @@ final class CompletionSuggestionServiceTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testUserSuggestionsWithEmptyText() async throws {
|
||||
@Test
|
||||
func userSuggestionsWithEmptyText() async throws {
|
||||
let alice: RoomMemberProxyMock = .mockAlice
|
||||
let bob: RoomMemberProxyMock = .mockBob
|
||||
let members: [RoomMemberProxyMock] = [alice, bob, .mockMe]
|
||||
@@ -124,7 +122,8 @@ final class CompletionSuggestionServiceTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testUserSuggestionInDifferentMessagePositions() async throws {
|
||||
@Test
|
||||
func userSuggestionInDifferentMessagePositions() async throws {
|
||||
let alice: RoomMemberProxyMock = .mockAlice
|
||||
let members: [RoomMemberProxyMock] = [alice, .mockBob, .mockCharlie, .mockMe]
|
||||
let roomProxyMock = JoinedRoomProxyMock(.init(name: "test", members: members))
|
||||
@@ -151,7 +150,8 @@ final class CompletionSuggestionServiceTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testUserSuggestionWithMultipleMentionSymbol() async throws {
|
||||
@Test
|
||||
func userSuggestionWithMultipleMentionSymbol() async throws {
|
||||
let alice: RoomMemberProxyMock = .mockAlice
|
||||
let bob: RoomMemberProxyMock = .mockBob
|
||||
let members: [RoomMemberProxyMock] = [alice, bob, .mockCharlie, .mockMe]
|
||||
@@ -179,7 +179,8 @@ final class CompletionSuggestionServiceTests: XCTestCase {
|
||||
try await deffered.fulfill()
|
||||
}
|
||||
|
||||
func testRoomSuggestions() async throws {
|
||||
@Test
|
||||
func roomSuggestions() async throws {
|
||||
let alice: RoomMemberProxyMock = .mockAlice
|
||||
// We keep the users in the tests since they should not appear in the suggestions when using the room trigger
|
||||
let members: [RoomMemberProxyMock] = [alice, .mockBob, .mockCharlie, .mockMe]
|
||||
@@ -252,7 +253,8 @@ final class CompletionSuggestionServiceTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testRoomSuggestionInDifferentMessagePositions() async throws {
|
||||
@Test
|
||||
func roomSuggestionInDifferentMessagePositions() async throws {
|
||||
let alice: RoomMemberProxyMock = .mockAlice
|
||||
// We keep the users in the tests since they should not appear in the suggestions when using the room trigger
|
||||
let members: [RoomMemberProxyMock] = [alice, .mockBob, .mockCharlie, .mockMe]
|
||||
@@ -301,7 +303,8 @@ final class CompletionSuggestionServiceTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testRoomSuggestionWithMultipleMentionSymbol() async throws {
|
||||
@Test
|
||||
func roomSuggestionWithMultipleMentionSymbol() async throws {
|
||||
let alice: RoomMemberProxyMock = .mockAlice
|
||||
// We keep the users in the tests since they should not appear in the suggestions when using the room trigger
|
||||
let members: [RoomMemberProxyMock] = [alice, .mockBob, .mockCharlie, .mockMe]
|
||||
@@ -351,7 +354,8 @@ final class CompletionSuggestionServiceTests: XCTestCase {
|
||||
try await deffered.fulfill()
|
||||
}
|
||||
|
||||
func testSuggestionsWithMultipleDifferentTriggers() async throws {
|
||||
@Test
|
||||
func suggestionsWithMultipleDifferentTriggers() async throws {
|
||||
let alice: RoomMemberProxyMock = .mockAlice
|
||||
// We keep the users in the tests since they should not appear in the suggestions when using the room trigger
|
||||
let members: [RoomMemberProxyMock] = [alice, .mockBob, .mockCharlie, .mockMe]
|
||||
@@ -380,7 +384,8 @@ final class CompletionSuggestionServiceTests: XCTestCase {
|
||||
try await deffered.fulfill()
|
||||
}
|
||||
|
||||
func testSuggestionsContainingNonAlphanumericCharacters() async throws {
|
||||
@Test
|
||||
func suggestionsContainingNonAlphanumericCharacters() async throws {
|
||||
let alice: RoomMemberProxyMock = .mockAlice
|
||||
// We keep the users in the tests since they should not appear in the suggestions when using the room trigger
|
||||
let members: [RoomMemberProxyMock] = [alice, .mockBob, .mockCharlie, .mockMe]
|
||||
|
||||
@@ -8,89 +8,86 @@
|
||||
|
||||
import Combine
|
||||
@testable import ElementX
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
import Testing
|
||||
import WysiwygComposer
|
||||
import XCTest
|
||||
|
||||
@Suite
|
||||
@MainActor
|
||||
class ComposerToolbarViewModelTests: XCTestCase {
|
||||
final class ComposerToolbarViewModelTests {
|
||||
private var appSettings: AppSettings!
|
||||
private var wysiwygViewModel: WysiwygComposerViewModel!
|
||||
private var viewModel: ComposerToolbarViewModel!
|
||||
private var completionSuggestionServiceMock: CompletionSuggestionServiceMock!
|
||||
private var draftServiceMock: ComposerDraftServiceMock!
|
||||
|
||||
override func setUp() {
|
||||
init() {
|
||||
AppSettings.resetAllSettings()
|
||||
appSettings = AppSettings()
|
||||
ServiceLocator.shared.register(appSettings: appSettings)
|
||||
setUpViewModel()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
deinit {
|
||||
AppSettings.resetAllSettings()
|
||||
}
|
||||
|
||||
func testComposerFocus() {
|
||||
@Test
|
||||
func composerFocus() {
|
||||
viewModel.process(timelineAction: .setMode(mode: .edit(originalEventOrTransactionID: .eventID("mock"), type: .default)))
|
||||
XCTAssertTrue(viewModel.state.bindings.composerFocused)
|
||||
#expect(viewModel.state.bindings.composerFocused)
|
||||
viewModel.process(timelineAction: .removeFocus)
|
||||
XCTAssertFalse(viewModel.state.bindings.composerFocused)
|
||||
#expect(!viewModel.state.bindings.composerFocused)
|
||||
}
|
||||
|
||||
func testComposerMode() {
|
||||
@Test
|
||||
func composerMode() {
|
||||
let mode: ComposerMode = .edit(originalEventOrTransactionID: .eventID("mock"), type: .default)
|
||||
viewModel.process(timelineAction: .setMode(mode: mode))
|
||||
XCTAssertEqual(viewModel.state.composerMode, mode)
|
||||
#expect(viewModel.state.composerMode == mode)
|
||||
viewModel.process(timelineAction: .clear)
|
||||
XCTAssertEqual(viewModel.state.composerMode, .default)
|
||||
#expect(viewModel.state.composerMode == .default)
|
||||
}
|
||||
|
||||
func testComposerModeIsPublished() {
|
||||
@Test
|
||||
func composerModeIsPublished() async throws {
|
||||
let mode: ComposerMode = .edit(originalEventOrTransactionID: .eventID("mock"), type: .default)
|
||||
let expectation = expectation(description: "Composer mode is published")
|
||||
let cancellable = viewModel
|
||||
.context
|
||||
.$viewState
|
||||
.map(\.composerMode)
|
||||
.removeDuplicates()
|
||||
.dropFirst()
|
||||
.sink { composerMode in
|
||||
XCTAssertEqual(composerMode, mode)
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
let deferred = deferFulfillment(viewModel.context.$viewState.map(\.composerMode).removeDuplicates().dropFirst()) { $0 == mode }
|
||||
viewModel.process(timelineAction: .setMode(mode: mode))
|
||||
|
||||
wait(for: [expectation], timeout: 2.0)
|
||||
cancellable.cancel()
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testHandleKeyCommand() {
|
||||
XCTAssertTrue(viewModel.context.viewState.keyCommands.count == 1)
|
||||
@Test
|
||||
func handleKeyCommand() {
|
||||
#expect(viewModel.context.viewState.keyCommands.count == 1)
|
||||
}
|
||||
|
||||
func testComposerFocusAfterEnablingRTE() {
|
||||
@Test
|
||||
func composerFocusAfterEnablingRTE() {
|
||||
viewModel.process(viewAction: .enableTextFormatting)
|
||||
XCTAssertTrue(viewModel.state.bindings.composerFocused)
|
||||
#expect(viewModel.state.bindings.composerFocused)
|
||||
}
|
||||
|
||||
func testRTEEnabledAfterSendingMessage() {
|
||||
@Test
|
||||
func rteEnabledAfterSendingMessage() {
|
||||
viewModel.process(viewAction: .enableTextFormatting)
|
||||
XCTAssertTrue(viewModel.state.bindings.composerFocused)
|
||||
#expect(viewModel.state.bindings.composerFocused)
|
||||
viewModel.state.composerEmpty = false
|
||||
viewModel.process(viewAction: .sendMessage)
|
||||
XCTAssertTrue(viewModel.state.bindings.composerFormattingEnabled)
|
||||
#expect(viewModel.state.bindings.composerFormattingEnabled)
|
||||
}
|
||||
|
||||
func testAlertIsShownAfterLinkAction() {
|
||||
XCTAssertNil(viewModel.state.bindings.alertInfo)
|
||||
@Test
|
||||
func alertIsShownAfterLinkAction() {
|
||||
#expect(viewModel.state.bindings.alertInfo == nil)
|
||||
viewModel.process(viewAction: .enableTextFormatting)
|
||||
viewModel.process(viewAction: .composerAction(action: .link))
|
||||
XCTAssertNotNil(viewModel.state.bindings.alertInfo)
|
||||
#expect(viewModel.state.bindings.alertInfo != nil)
|
||||
}
|
||||
|
||||
func testSuggestions() {
|
||||
@Test
|
||||
func suggestions() {
|
||||
let suggestions: [SuggestionItem] = [.init(suggestionType: .user(.init(id: "@user_mention_1:matrix.org", displayName: "User 1", avatarURL: nil)), range: .init(), rawSuggestionText: ""),
|
||||
.init(suggestionType: .user(.init(id: "@user_mention_2:matrix.org", displayName: "User 2", avatarURL: nil)), range: .init(), rawSuggestionText: "")]
|
||||
let mockCompletionSuggestionService = CompletionSuggestionServiceMock(configuration: .init(suggestions: suggestions))
|
||||
@@ -104,31 +101,34 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
composerDraftService: draftServiceMock)
|
||||
|
||||
XCTAssertEqual(viewModel.state.suggestions, suggestions)
|
||||
#expect(viewModel.state.suggestions == suggestions)
|
||||
}
|
||||
|
||||
func testSuggestionTrigger() async throws {
|
||||
@Test
|
||||
func suggestionTrigger() async throws {
|
||||
let deferred = deferFulfillment(wysiwygViewModel.$attributedContent) { $0.plainText == "#room-alias-test" }
|
||||
wysiwygViewModel.setMarkdownContent("@user-test")
|
||||
wysiwygViewModel.setMarkdownContent("#room-alias-test")
|
||||
try await deferred.fulfill()
|
||||
|
||||
// The first one is nil because when initialised the view model is empty
|
||||
XCTAssertEqual(completionSuggestionServiceMock.setSuggestionTriggerReceivedInvocations, [nil,
|
||||
.init(type: .user, text: "user-test", range: .init(location: 0, length: 10)),
|
||||
.init(type: .room, text: "room-alias-test",
|
||||
range: .init(location: 0, length: 16))])
|
||||
#expect(completionSuggestionServiceMock.setSuggestionTriggerReceivedInvocations == [nil,
|
||||
.init(type: .user, text: "user-test", range: .init(location: 0, length: 10)),
|
||||
.init(type: .room, text: "room-alias-test",
|
||||
range: .init(location: 0, length: 16))])
|
||||
}
|
||||
|
||||
func testSelectedUserSuggestion() {
|
||||
@Test
|
||||
func selectedUserSuggestion() {
|
||||
let suggestion = SuggestionItem(suggestionType: .user(.init(id: "@test:matrix.org", displayName: "Test", avatarURL: nil)), range: .init(), rawSuggestionText: "")
|
||||
viewModel.context.send(viewAction: .selectedSuggestion(suggestion))
|
||||
|
||||
// The display name can be used for HTML injection in the rich text editor and it's useless anyway as the clients don't use it when resolving display names
|
||||
XCTAssertEqual(wysiwygViewModel.content.html, "<a href=\"https://matrix.to/#/@test:matrix.org\">@test:matrix.org</a> ")
|
||||
#expect(wysiwygViewModel.content.html == "<a href=\"https://matrix.to/#/@test:matrix.org\">@test:matrix.org</a> ")
|
||||
}
|
||||
|
||||
func testSelectedRoomSuggestion() {
|
||||
@Test
|
||||
func selectedRoomSuggestion() {
|
||||
let suggestion = SuggestionItem(suggestionType: .room(.init(id: "!room:matrix.org",
|
||||
canonicalAlias: "#room-alias:matrix.org",
|
||||
name: "Room",
|
||||
@@ -140,19 +140,21 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
||||
|
||||
// The display name can be used for HTML injection in the rich text editor and it's useless anyway as the clients don't use it when resolving display names
|
||||
|
||||
XCTAssertEqual(wysiwygViewModel.content.html, "<a href=\"https://matrix.to/#/%23room-alias:matrix.org\">#room-alias:matrix.org</a> ")
|
||||
#expect(wysiwygViewModel.content.html == "<a href=\"https://matrix.to/#/%23room-alias:matrix.org\">#room-alias:matrix.org</a> ")
|
||||
}
|
||||
|
||||
func testAllUsersSuggestion() throws {
|
||||
@Test
|
||||
func allUsersSuggestion() throws {
|
||||
let suggestion = SuggestionItem(suggestionType: .allUsers(.room(id: "", name: nil, avatarURL: nil)), range: .init(), rawSuggestionText: "")
|
||||
viewModel.context.send(viewAction: .selectedSuggestion(suggestion))
|
||||
|
||||
var string = "@room"
|
||||
try string.unicodeScalars.append(XCTUnwrap(UnicodeScalar(String.nbsp)))
|
||||
XCTAssertEqual(wysiwygViewModel.content.html, string)
|
||||
try string.unicodeScalars.append(#require(UnicodeScalar(String.nbsp)))
|
||||
#expect(wysiwygViewModel.content.html == string)
|
||||
}
|
||||
|
||||
func testUserMentionPillInRTE() async {
|
||||
@Test
|
||||
func userMentionPillInRTE() async {
|
||||
viewModel.context.send(viewAction: .composerAppeared)
|
||||
await Task.yield()
|
||||
let userID = "@test:matrix.org"
|
||||
@@ -160,10 +162,11 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
||||
viewModel.context.send(viewAction: .selectedSuggestion(suggestion))
|
||||
|
||||
let attachment = wysiwygViewModel.textView.attributedText.attribute(.attachment, at: 0, effectiveRange: nil) as? PillTextAttachment
|
||||
XCTAssertEqual(attachment?.pillData?.type, .user(userID: userID))
|
||||
#expect(attachment?.pillData?.type == .user(userID: userID))
|
||||
}
|
||||
|
||||
func testRoomMentionPillInRTE() async {
|
||||
@Test
|
||||
func roomMentionPillInRTE() async {
|
||||
viewModel.context.send(viewAction: .composerAppeared)
|
||||
await Task.yield()
|
||||
let roomAlias = "#test:matrix.org"
|
||||
@@ -171,20 +174,22 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
||||
viewModel.context.send(viewAction: .selectedSuggestion(suggestion))
|
||||
|
||||
let attachment = wysiwygViewModel.textView.attributedText.attribute(.attachment, at: 0, effectiveRange: nil) as? PillTextAttachment
|
||||
XCTAssertEqual(attachment?.pillData?.type, .roomAlias(roomAlias))
|
||||
#expect(attachment?.pillData?.type == .roomAlias(roomAlias))
|
||||
}
|
||||
|
||||
func testAllUsersMentionPillInRTE() async {
|
||||
@Test
|
||||
func allUsersMentionPillInRTE() async {
|
||||
viewModel.context.send(viewAction: .composerAppeared)
|
||||
await Task.yield()
|
||||
let suggestion = SuggestionItem(suggestionType: .allUsers(.room(id: "", name: nil, avatarURL: nil)), range: .init(), rawSuggestionText: "")
|
||||
viewModel.context.send(viewAction: .selectedSuggestion(suggestion))
|
||||
|
||||
let attachment = wysiwygViewModel.textView.attributedText.attribute(.attachment, at: 0, effectiveRange: nil) as? PillTextAttachment
|
||||
XCTAssertEqual(attachment?.pillData?.type, .allUsers)
|
||||
#expect(attachment?.pillData?.type == .allUsers)
|
||||
}
|
||||
|
||||
func testIntentionalMentions() async throws {
|
||||
@Test
|
||||
func intentionalMentions() async throws {
|
||||
wysiwygViewModel.setHtmlContent("""
|
||||
<p>Hello @room \
|
||||
and especially hello to <a href=\"https://matrix.to/#/@test:matrix.org\">Test</a></p>
|
||||
@@ -205,77 +210,81 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
||||
|
||||
// MARK: - Draft
|
||||
|
||||
func testSaveDraftPlainText() async {
|
||||
let expectation = expectation(description: "Wait for draft to be saved")
|
||||
draftServiceMock.saveDraftClosure = { draft in
|
||||
XCTAssertEqual(draft.plainText, "Hello world!")
|
||||
XCTAssertNil(draft.htmlText)
|
||||
XCTAssertEqual(draft.draftType, .newMessage)
|
||||
defer { expectation.fulfill() }
|
||||
return .success(())
|
||||
}
|
||||
|
||||
@Test
|
||||
func saveDraftPlainText() async throws {
|
||||
viewModel.context.composerFormattingEnabled = false
|
||||
viewModel.context.plainComposerText = .init(string: "Hello world!")
|
||||
viewModel.saveDraft()
|
||||
|
||||
await fulfillment(of: [expectation], timeout: 10)
|
||||
XCTAssertEqual(draftServiceMock.saveDraftCallsCount, 1)
|
||||
XCTAssertFalse(draftServiceMock.clearDraftCalled)
|
||||
XCTAssertFalse(draftServiceMock.loadDraftCalled)
|
||||
}
|
||||
|
||||
func testSaveDraftFormattedText() async {
|
||||
let expectation = expectation(description: "Wait for draft to be saved")
|
||||
draftServiceMock.saveDraftClosure = { draft in
|
||||
XCTAssertEqual(draft.plainText, "__Hello__ world!")
|
||||
XCTAssertEqual(draft.htmlText, "<strong>Hello</strong> world!")
|
||||
XCTAssertEqual(draft.draftType, .newMessage)
|
||||
defer { expectation.fulfill() }
|
||||
return .success(())
|
||||
var capturedDraft: ComposerDraftProxy?
|
||||
await waitForConfirmation("Save draft") { confirmation in
|
||||
draftServiceMock.saveDraftClosure = { draft in
|
||||
capturedDraft = draft
|
||||
confirmation()
|
||||
return .success(())
|
||||
}
|
||||
viewModel.saveDraft()
|
||||
}
|
||||
|
||||
let draft = try #require(capturedDraft)
|
||||
#expect(draft.plainText == "Hello world!")
|
||||
#expect(draft.htmlText == nil)
|
||||
#expect(draft.draftType == .newMessage)
|
||||
#expect(draftServiceMock.saveDraftCallsCount == 1)
|
||||
#expect(!draftServiceMock.clearDraftCalled)
|
||||
#expect(!draftServiceMock.loadDraftCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
func saveDraftFormattedText() async throws {
|
||||
viewModel.context.composerFormattingEnabled = true
|
||||
wysiwygViewModel.setHtmlContent("<strong>Hello</strong> world!")
|
||||
viewModel.saveDraft()
|
||||
|
||||
await fulfillment(of: [expectation], timeout: 10)
|
||||
XCTAssertEqual(draftServiceMock.saveDraftCallsCount, 1)
|
||||
XCTAssertFalse(draftServiceMock.clearDraftCalled)
|
||||
XCTAssertFalse(draftServiceMock.loadDraftCalled)
|
||||
}
|
||||
|
||||
func testSaveDraftEdit() async {
|
||||
let expectation = expectation(description: "Wait for draft to be saved")
|
||||
draftServiceMock.saveDraftClosure = { draft in
|
||||
XCTAssertEqual(draft.plainText, "Hello world!")
|
||||
XCTAssertNil(draft.htmlText)
|
||||
XCTAssertEqual(draft.draftType, .edit(eventID: "testID"))
|
||||
defer { expectation.fulfill() }
|
||||
return .success(())
|
||||
var capturedDraft: ComposerDraftProxy?
|
||||
await waitForConfirmation("Save draft") { confirmation in
|
||||
draftServiceMock.saveDraftClosure = { draft in
|
||||
capturedDraft = draft
|
||||
confirmation()
|
||||
return .success(())
|
||||
}
|
||||
viewModel.saveDraft()
|
||||
}
|
||||
|
||||
let draft = try #require(capturedDraft)
|
||||
#expect(draft.plainText == "__Hello__ world!")
|
||||
#expect(draft.htmlText == "<strong>Hello</strong> world!")
|
||||
#expect(draft.draftType == .newMessage)
|
||||
#expect(draftServiceMock.saveDraftCallsCount == 1)
|
||||
#expect(!draftServiceMock.clearDraftCalled)
|
||||
#expect(!draftServiceMock.loadDraftCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
func saveDraftEdit() async throws {
|
||||
viewModel.context.composerFormattingEnabled = false
|
||||
viewModel.process(timelineAction: .setMode(mode: .edit(originalEventOrTransactionID: .eventID("testID"), type: .default)))
|
||||
viewModel.context.plainComposerText = .init(string: "Hello world!")
|
||||
viewModel.saveDraft()
|
||||
|
||||
await fulfillment(of: [expectation], timeout: 10)
|
||||
XCTAssertEqual(draftServiceMock.saveDraftCallsCount, 1)
|
||||
XCTAssertFalse(draftServiceMock.clearDraftCalled)
|
||||
XCTAssertFalse(draftServiceMock.loadDraftCalled)
|
||||
}
|
||||
|
||||
func testSaveDraftReply() async {
|
||||
let expectation = expectation(description: "Wait for draft to be saved")
|
||||
draftServiceMock.saveDraftClosure = { draft in
|
||||
XCTAssertEqual(draft.plainText, "Hello world!")
|
||||
XCTAssertNil(draft.htmlText)
|
||||
XCTAssertEqual(draft.draftType, .reply(eventID: "testID"))
|
||||
defer { expectation.fulfill() }
|
||||
return .success(())
|
||||
var capturedDraft: ComposerDraftProxy?
|
||||
await waitForConfirmation("Save draft") { confirmation in
|
||||
draftServiceMock.saveDraftClosure = { draft in
|
||||
capturedDraft = draft
|
||||
confirmation()
|
||||
return .success(())
|
||||
}
|
||||
viewModel.saveDraft()
|
||||
}
|
||||
|
||||
let draft = try #require(capturedDraft)
|
||||
#expect(draft.plainText == "Hello world!")
|
||||
#expect(draft.htmlText == nil)
|
||||
#expect(draft.draftType == .edit(eventID: "testID"))
|
||||
#expect(draftServiceMock.saveDraftCallsCount == 1)
|
||||
#expect(!draftServiceMock.clearDraftCalled)
|
||||
#expect(!draftServiceMock.loadDraftCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
func saveDraftReply() async throws {
|
||||
viewModel.context.composerFormattingEnabled = false
|
||||
viewModel.process(timelineAction: .setMode(mode: .reply(eventID: "testID",
|
||||
replyDetails: .loaded(sender: .init(id: ""),
|
||||
@@ -283,143 +292,161 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
||||
eventContent: .message(.text(.init(body: "reply text")))),
|
||||
isThread: false)))
|
||||
viewModel.context.plainComposerText = .init(string: "Hello world!")
|
||||
viewModel.saveDraft()
|
||||
|
||||
await fulfillment(of: [expectation], timeout: 10)
|
||||
XCTAssertEqual(draftServiceMock.saveDraftCallsCount, 1)
|
||||
XCTAssertFalse(draftServiceMock.clearDraftCalled)
|
||||
XCTAssertFalse(draftServiceMock.loadDraftCalled)
|
||||
}
|
||||
|
||||
func testSaveDraftWhenEmptyReply() async {
|
||||
let expectation = expectation(description: "Wait for draft to be saved")
|
||||
draftServiceMock.saveDraftClosure = { draft in
|
||||
XCTAssertEqual(draft.plainText, "")
|
||||
XCTAssertNil(draft.htmlText)
|
||||
XCTAssertEqual(draft.draftType, .reply(eventID: "testID"))
|
||||
defer { expectation.fulfill() }
|
||||
return .success(())
|
||||
var capturedDraft: ComposerDraftProxy?
|
||||
await waitForConfirmation("Save draft") { confirmation in
|
||||
draftServiceMock.saveDraftClosure = { draft in
|
||||
capturedDraft = draft
|
||||
confirmation()
|
||||
return .success(())
|
||||
}
|
||||
viewModel.saveDraft()
|
||||
}
|
||||
|
||||
let draft = try #require(capturedDraft)
|
||||
#expect(draft.plainText == "Hello world!")
|
||||
#expect(draft.htmlText == nil)
|
||||
#expect(draft.draftType == .reply(eventID: "testID"))
|
||||
#expect(draftServiceMock.saveDraftCallsCount == 1)
|
||||
#expect(!draftServiceMock.clearDraftCalled)
|
||||
#expect(!draftServiceMock.loadDraftCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
func saveDraftWhenEmptyReply() async throws {
|
||||
viewModel.context.composerFormattingEnabled = false
|
||||
viewModel.process(timelineAction: .setMode(mode: .reply(eventID: "testID",
|
||||
replyDetails: .loaded(sender: .init(id: ""),
|
||||
eventID: "testID",
|
||||
eventContent: .message(.text(.init(body: "reply text")))),
|
||||
isThread: false)))
|
||||
viewModel.saveDraft()
|
||||
|
||||
await fulfillment(of: [expectation], timeout: 10)
|
||||
XCTAssertEqual(draftServiceMock.saveDraftCallsCount, 1)
|
||||
XCTAssertFalse(draftServiceMock.clearDraftCalled)
|
||||
XCTAssertFalse(draftServiceMock.loadDraftCalled)
|
||||
}
|
||||
|
||||
func testClearDraftWhenEmptyNormalMessage() async {
|
||||
let expectation = expectation(description: "Wait for draft to be cleared")
|
||||
draftServiceMock.clearDraftClosure = {
|
||||
defer { expectation.fulfill() }
|
||||
return .success(())
|
||||
var capturedDraft: ComposerDraftProxy?
|
||||
await waitForConfirmation("Save draft") { confirmation in
|
||||
draftServiceMock.saveDraftClosure = { draft in
|
||||
capturedDraft = draft
|
||||
confirmation()
|
||||
return .success(())
|
||||
}
|
||||
viewModel.saveDraft()
|
||||
}
|
||||
|
||||
let draft = try #require(capturedDraft)
|
||||
#expect(draft.plainText == "")
|
||||
#expect(draft.htmlText == nil)
|
||||
#expect(draft.draftType == .reply(eventID: "testID"))
|
||||
#expect(draftServiceMock.saveDraftCallsCount == 1)
|
||||
#expect(!draftServiceMock.clearDraftCalled)
|
||||
#expect(!draftServiceMock.loadDraftCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
func clearDraftWhenEmptyNormalMessage() async {
|
||||
viewModel.context.composerFormattingEnabled = false
|
||||
viewModel.saveDraft()
|
||||
|
||||
await fulfillment(of: [expectation], timeout: 10)
|
||||
XCTAssertFalse(draftServiceMock.saveDraftCalled)
|
||||
XCTAssertEqual(draftServiceMock.clearDraftCallsCount, 1)
|
||||
XCTAssertFalse(draftServiceMock.loadDraftCalled)
|
||||
}
|
||||
|
||||
func testClearDraftForNonTextMode() async {
|
||||
let expectation = expectation(description: "Wait for draft to be cleared")
|
||||
draftServiceMock.clearDraftClosure = {
|
||||
defer { expectation.fulfill() }
|
||||
return .success(())
|
||||
await waitForConfirmation("Clear draft") { confirmation in
|
||||
draftServiceMock.clearDraftClosure = {
|
||||
confirmation()
|
||||
return .success(())
|
||||
}
|
||||
viewModel.saveDraft()
|
||||
}
|
||||
|
||||
#expect(!draftServiceMock.saveDraftCalled)
|
||||
#expect(draftServiceMock.clearDraftCallsCount == 1)
|
||||
#expect(!draftServiceMock.loadDraftCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
func clearDraftForNonTextMode() async {
|
||||
viewModel.context.composerFormattingEnabled = false
|
||||
let waveformData: [Float] = Array(repeating: 1.0, count: 1000)
|
||||
viewModel.context.plainComposerText = .init(string: "Hello world!")
|
||||
viewModel.process(timelineAction: .setMode(mode: .previewVoiceMessage(state: AudioPlayerState(id: .recorderPreview, title: "", duration: 10.0),
|
||||
waveform: .data(waveformData),
|
||||
isUploading: false)))
|
||||
viewModel.saveDraft()
|
||||
|
||||
await fulfillment(of: [expectation], timeout: 10)
|
||||
XCTAssertFalse(draftServiceMock.saveDraftCalled)
|
||||
XCTAssertEqual(draftServiceMock.clearDraftCallsCount, 1)
|
||||
XCTAssertFalse(draftServiceMock.loadDraftCalled)
|
||||
await waitForConfirmation("Clear draft") { confirmation in
|
||||
draftServiceMock.clearDraftClosure = {
|
||||
confirmation()
|
||||
return .success(())
|
||||
}
|
||||
viewModel.saveDraft()
|
||||
}
|
||||
|
||||
#expect(!draftServiceMock.saveDraftCalled)
|
||||
#expect(draftServiceMock.clearDraftCallsCount == 1)
|
||||
#expect(!draftServiceMock.loadDraftCalled)
|
||||
}
|
||||
|
||||
func testNothingToRestore() async {
|
||||
@Test
|
||||
func nothingToRestore() async {
|
||||
viewModel.context.composerFormattingEnabled = false
|
||||
let expectation = expectation(description: "Wait for draft to be restored")
|
||||
draftServiceMock.loadDraftClosure = {
|
||||
defer { expectation.fulfill() }
|
||||
return .success(nil)
|
||||
.success(nil)
|
||||
}
|
||||
|
||||
await viewModel.loadDraft()
|
||||
await fulfillment(of: [expectation], timeout: 10)
|
||||
XCTAssertFalse(viewModel.context.composerFormattingEnabled)
|
||||
XCTAssertTrue(viewModel.state.composerEmpty)
|
||||
XCTAssertEqual(viewModel.state.composerMode, .default)
|
||||
#expect(!viewModel.context.composerFormattingEnabled)
|
||||
#expect(viewModel.state.composerEmpty)
|
||||
#expect(viewModel.state.composerMode == .default)
|
||||
}
|
||||
|
||||
func testRestoreNormalPlainTextMessage() async {
|
||||
@Test
|
||||
func restoreNormalPlainTextMessage() async {
|
||||
viewModel.context.composerFormattingEnabled = false
|
||||
let expectation = expectation(description: "Wait for draft to be restored")
|
||||
draftServiceMock.loadDraftClosure = {
|
||||
defer { expectation.fulfill() }
|
||||
return .success(.init(plainText: "Hello world!",
|
||||
htmlText: nil,
|
||||
draftType: .newMessage))
|
||||
.success(.init(plainText: "Hello world!",
|
||||
htmlText: nil,
|
||||
draftType: .newMessage))
|
||||
}
|
||||
await viewModel.loadDraft()
|
||||
|
||||
await fulfillment(of: [expectation], timeout: 10)
|
||||
XCTAssertFalse(viewModel.context.composerFormattingEnabled)
|
||||
XCTAssertEqual(viewModel.state.composerMode, .default)
|
||||
XCTAssertEqual(viewModel.context.plainComposerText, NSAttributedString(string: "Hello world!"))
|
||||
#expect(!viewModel.context.composerFormattingEnabled)
|
||||
#expect(viewModel.state.composerMode == .default)
|
||||
#expect(viewModel.context.plainComposerText == NSAttributedString(string: "Hello world!"))
|
||||
}
|
||||
|
||||
func testRestoreNormalFormattedTextMessage() async {
|
||||
@Test
|
||||
func restoreNormalFormattedTextMessage() async throws {
|
||||
viewModel.context.composerFormattingEnabled = false
|
||||
|
||||
try await confirmation { confirmation in
|
||||
draftServiceMock.loadDraftClosure = {
|
||||
defer { confirmation() }
|
||||
return .success(.init(plainText: "__Hello__ world!",
|
||||
htmlText: "<strong>Hello</strong> world!",
|
||||
draftType: .newMessage))
|
||||
}
|
||||
|
||||
let deferred = deferFulfillment(wysiwygViewModel.$isContentEmpty) { !$0 }
|
||||
await viewModel.loadDraft()
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
#expect(viewModel.context.composerFormattingEnabled)
|
||||
#expect(viewModel.state.composerMode == .default)
|
||||
#expect(wysiwygViewModel.content.html == "<strong>Hello</strong> world!")
|
||||
#expect(wysiwygViewModel.content.markdown == "__Hello__ world!")
|
||||
}
|
||||
|
||||
@Test
|
||||
func restoreEdit() async {
|
||||
viewModel.context.composerFormattingEnabled = false
|
||||
let expectation = expectation(description: "Wait for draft to be restored")
|
||||
draftServiceMock.loadDraftClosure = {
|
||||
defer { expectation.fulfill() }
|
||||
return .success(.init(plainText: "__Hello__ world!",
|
||||
htmlText: "<strong>Hello</strong> world!",
|
||||
draftType: .newMessage))
|
||||
.success(.init(plainText: "Hello world!",
|
||||
htmlText: nil,
|
||||
draftType: .edit(eventID: "testID")))
|
||||
}
|
||||
await viewModel.loadDraft()
|
||||
|
||||
await fulfillment(of: [expectation], timeout: 10)
|
||||
XCTAssertTrue(viewModel.context.composerFormattingEnabled)
|
||||
XCTAssertEqual(viewModel.state.composerMode, .default)
|
||||
XCTAssertEqual(wysiwygViewModel.content.html, "<strong>Hello</strong> world!")
|
||||
XCTAssertEqual(wysiwygViewModel.content.markdown, "__Hello__ world!")
|
||||
#expect(!viewModel.context.composerFormattingEnabled)
|
||||
#expect(viewModel.state.composerMode == .edit(originalEventOrTransactionID: .eventID("testID"), type: .default))
|
||||
#expect(viewModel.context.plainComposerText == NSAttributedString(string: "Hello world!"))
|
||||
}
|
||||
|
||||
func testRestoreEdit() async {
|
||||
viewModel.context.composerFormattingEnabled = false
|
||||
let expectation = expectation(description: "Wait for draft to be restored")
|
||||
draftServiceMock.loadDraftClosure = {
|
||||
defer { expectation.fulfill() }
|
||||
return .success(.init(plainText: "Hello world!",
|
||||
htmlText: nil,
|
||||
draftType: .edit(eventID: "testID")))
|
||||
}
|
||||
await viewModel.loadDraft()
|
||||
|
||||
await fulfillment(of: [expectation], timeout: 10)
|
||||
XCTAssertFalse(viewModel.context.composerFormattingEnabled)
|
||||
XCTAssertEqual(viewModel.state.composerMode, .edit(originalEventOrTransactionID: .eventID("testID"), type: .default))
|
||||
XCTAssertEqual(viewModel.context.plainComposerText, NSAttributedString(string: "Hello world!"))
|
||||
}
|
||||
|
||||
func testRestoreReply() async {
|
||||
@Test
|
||||
func restoreReply() async throws {
|
||||
let testEventID = "testID"
|
||||
let text = "Hello world!"
|
||||
let loadedReply = TimelineItemReplyDetails.loaded(sender: .init(id: "userID",
|
||||
@@ -428,39 +455,38 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
||||
eventContent: .message(.text(.init(body: "Reply text"))))
|
||||
|
||||
viewModel.context.composerFormattingEnabled = false
|
||||
let draftExpectation = expectation(description: "Wait for draft to be restored")
|
||||
draftServiceMock.loadDraftClosure = {
|
||||
defer { draftExpectation.fulfill() }
|
||||
return .success(.init(plainText: text,
|
||||
htmlText: nil,
|
||||
draftType: .reply(eventID: testEventID)))
|
||||
.success(.init(plainText: text,
|
||||
htmlText: nil,
|
||||
draftType: .reply(eventID: testEventID)))
|
||||
}
|
||||
|
||||
let loadReplyExpectation = expectation(description: "Wait for reply to be loaded")
|
||||
let deferredReplyLoaded = deferFulfillment(viewModel.context.$viewState) {
|
||||
$0.composerMode == .reply(eventID: testEventID, replyDetails: loadedReply, isThread: true)
|
||||
}
|
||||
draftServiceMock.getReplyEventIDClosure = { eventID in
|
||||
defer { loadReplyExpectation.fulfill() }
|
||||
XCTAssertEqual(eventID, testEventID)
|
||||
#expect(eventID == testEventID)
|
||||
try? await Task.sleep(for: .seconds(1))
|
||||
return .success(.init(details: loadedReply,
|
||||
isThreaded: true))
|
||||
}
|
||||
await viewModel.loadDraft()
|
||||
|
||||
await fulfillment(of: [draftExpectation], timeout: 10)
|
||||
XCTAssertFalse(viewModel.context.composerFormattingEnabled)
|
||||
#expect(!viewModel.context.composerFormattingEnabled)
|
||||
// Testing the loading state first
|
||||
XCTAssertEqual(viewModel.state.composerMode, .reply(eventID: testEventID,
|
||||
replyDetails: .loading(eventID: testEventID),
|
||||
isThread: false))
|
||||
XCTAssertEqual(viewModel.context.plainComposerText, NSAttributedString(string: text))
|
||||
#expect(viewModel.state.composerMode == .reply(eventID: testEventID,
|
||||
replyDetails: .loading(eventID: testEventID),
|
||||
isThread: false))
|
||||
#expect(viewModel.context.plainComposerText == NSAttributedString(string: text))
|
||||
|
||||
await fulfillment(of: [loadReplyExpectation], timeout: 10)
|
||||
XCTAssertEqual(viewModel.state.composerMode, .reply(eventID: testEventID,
|
||||
replyDetails: loadedReply,
|
||||
isThread: true))
|
||||
try await deferredReplyLoaded.fulfill()
|
||||
#expect(viewModel.state.composerMode == .reply(eventID: testEventID,
|
||||
replyDetails: loadedReply,
|
||||
isThread: true))
|
||||
}
|
||||
|
||||
func testRestoreReplyAndCancelReplyMode() async {
|
||||
@Test
|
||||
func restoreReplyAndCancelReplyMode() async throws {
|
||||
let testEventID = "testID"
|
||||
let text = "Hello world!"
|
||||
let loadedReply = TimelineItemReplyDetails.loaded(sender: .init(id: "userID", displayName: "Username"),
|
||||
@@ -468,103 +494,105 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
||||
eventContent: .message(.text(.init(body: "Reply text"))))
|
||||
|
||||
viewModel.context.composerFormattingEnabled = false
|
||||
let draftExpectation = expectation(description: "Wait for draft to be restored")
|
||||
draftServiceMock.loadDraftClosure = {
|
||||
defer { draftExpectation.fulfill() }
|
||||
return .success(.init(plainText: text,
|
||||
htmlText: nil,
|
||||
draftType: .reply(eventID: testEventID)))
|
||||
.success(.init(plainText: text,
|
||||
htmlText: nil,
|
||||
draftType: .reply(eventID: testEventID)))
|
||||
}
|
||||
|
||||
let loadReplyExpectation = expectation(description: "Wait for reply to be loaded")
|
||||
let replyLoadedSubject = PassthroughSubject<Void, Never>()
|
||||
let deferredReplyLoaded = deferFulfillment(replyLoadedSubject) { _ in true }
|
||||
draftServiceMock.getReplyEventIDClosure = { eventID in
|
||||
defer { loadReplyExpectation.fulfill() }
|
||||
XCTAssertEqual(eventID, testEventID)
|
||||
defer { replyLoadedSubject.send(()) }
|
||||
#expect(eventID == testEventID)
|
||||
try? await Task.sleep(for: .seconds(1))
|
||||
return .success(.init(details: loadedReply,
|
||||
isThreaded: true))
|
||||
}
|
||||
await viewModel.loadDraft()
|
||||
|
||||
await fulfillment(of: [draftExpectation], timeout: 10)
|
||||
XCTAssertFalse(viewModel.context.composerFormattingEnabled)
|
||||
#expect(!viewModel.context.composerFormattingEnabled)
|
||||
// Testing the loading state first
|
||||
XCTAssertEqual(viewModel.state.composerMode, .reply(eventID: testEventID,
|
||||
replyDetails: .loading(eventID: testEventID),
|
||||
isThread: false))
|
||||
XCTAssertEqual(viewModel.context.plainComposerText, NSAttributedString(string: text))
|
||||
#expect(viewModel.state.composerMode == .reply(eventID: testEventID,
|
||||
replyDetails: .loading(eventID: testEventID),
|
||||
isThread: false))
|
||||
#expect(viewModel.context.plainComposerText == NSAttributedString(string: text))
|
||||
|
||||
// Now we change the state to cancel the reply mode update
|
||||
viewModel.process(viewAction: .cancelReply)
|
||||
await fulfillment(of: [loadReplyExpectation], timeout: 10)
|
||||
XCTAssertEqual(viewModel.state.composerMode, .default)
|
||||
try await deferredReplyLoaded.fulfill()
|
||||
#expect(viewModel.state.composerMode == .default)
|
||||
}
|
||||
|
||||
func testSaveVolatileDraftWhenEditing() {
|
||||
@Test
|
||||
func saveVolatileDraftWhenEditing() {
|
||||
viewModel.context.composerFormattingEnabled = false
|
||||
viewModel.context.plainComposerText = .init(string: "Hello world!")
|
||||
viewModel.process(timelineAction: .setMode(mode: .edit(originalEventOrTransactionID: .eventID(UUID().uuidString), type: .default)))
|
||||
|
||||
let draft = draftServiceMock.saveVolatileDraftReceivedDraft
|
||||
XCTAssertNotNil(draft)
|
||||
XCTAssertEqual(draft?.plainText, "Hello world!")
|
||||
XCTAssertNil(draft?.htmlText)
|
||||
XCTAssertEqual(draft?.draftType, .newMessage)
|
||||
#expect(draft != nil)
|
||||
#expect(draft?.plainText == "Hello world!")
|
||||
#expect(draft?.htmlText == nil)
|
||||
#expect(draft?.draftType == .newMessage)
|
||||
}
|
||||
|
||||
func testRestoreVolatileDraftWhenCancellingEdit() async {
|
||||
let expectation = expectation(description: "Wait for draft to be restored")
|
||||
draftServiceMock.loadVolatileDraftClosure = {
|
||||
defer { expectation.fulfill() }
|
||||
return .init(plainText: "Hello world",
|
||||
htmlText: nil,
|
||||
draftType: .newMessage)
|
||||
@Test
|
||||
func restoreVolatileDraftWhenCancellingEdit() async {
|
||||
await waitForConfirmation("Volatile draft loaded") { confirmation in
|
||||
draftServiceMock.loadVolatileDraftClosure = {
|
||||
defer { confirmation() }
|
||||
return .init(plainText: "Hello world",
|
||||
htmlText: nil,
|
||||
draftType: .newMessage)
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.viewModel.process(viewAction: .cancelEdit)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.process(viewAction: .cancelEdit)
|
||||
await fulfillment(of: [expectation])
|
||||
XCTAssertEqual(viewModel.context.plainComposerText, NSAttributedString(string: "Hello world"))
|
||||
#expect(viewModel.context.plainComposerText == NSAttributedString(string: "Hello world"))
|
||||
}
|
||||
|
||||
func testRestoreVolatileDraftWhenClearing() async {
|
||||
let expectation1 = expectation(description: "Wait for draft to be restored")
|
||||
draftServiceMock.loadVolatileDraftClosure = {
|
||||
defer { expectation1.fulfill() }
|
||||
return .init(plainText: "Hello world",
|
||||
htmlText: nil,
|
||||
draftType: .newMessage)
|
||||
@Test
|
||||
func restoreVolatileDraftWhenClearing() async {
|
||||
await waitForConfirmation("Volatile draft loaded and cleared", expectedCount: 2) { confirmation in
|
||||
draftServiceMock.loadVolatileDraftClosure = {
|
||||
defer { confirmation() }
|
||||
return .init(plainText: "Hello world",
|
||||
htmlText: nil,
|
||||
draftType: .newMessage)
|
||||
}
|
||||
draftServiceMock.clearVolatileDraftClosure = {
|
||||
confirmation()
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.viewModel.process(timelineAction: .clear)
|
||||
}
|
||||
}
|
||||
|
||||
let expectation2 = expectation(description: "The draft should also be cleared after being loaded")
|
||||
draftServiceMock.clearVolatileDraftClosure = {
|
||||
expectation2.fulfill()
|
||||
}
|
||||
|
||||
viewModel.process(timelineAction: .clear)
|
||||
await fulfillment(of: [expectation1, expectation2])
|
||||
XCTAssertEqual(viewModel.context.plainComposerText, NSAttributedString(string: "Hello world"))
|
||||
#expect(viewModel.context.plainComposerText == NSAttributedString(string: "Hello world"))
|
||||
}
|
||||
|
||||
func testRestoreVolatileDraftDoubleClear() async {
|
||||
let expectation1 = expectation(description: "Wait for draft to be restored")
|
||||
draftServiceMock.loadVolatileDraftClosure = {
|
||||
defer { expectation1.fulfill() }
|
||||
return .init(plainText: "Hello world",
|
||||
htmlText: nil,
|
||||
draftType: .newMessage)
|
||||
@Test
|
||||
func restoreVolatileDraftDoubleClear() async {
|
||||
await waitForConfirmation("Volatile draft loaded and cleared", expectedCount: 2) { confirmation in
|
||||
draftServiceMock.loadVolatileDraftClosure = {
|
||||
defer { confirmation() }
|
||||
return .init(plainText: "Hello world",
|
||||
htmlText: nil,
|
||||
draftType: .newMessage)
|
||||
}
|
||||
draftServiceMock.clearVolatileDraftClosure = {
|
||||
confirmation()
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.viewModel.process(timelineAction: .clear)
|
||||
}
|
||||
}
|
||||
|
||||
let expectation2 = expectation(description: "The draft should also be cleared after being loaded")
|
||||
draftServiceMock.clearVolatileDraftClosure = {
|
||||
expectation2.fulfill()
|
||||
}
|
||||
|
||||
viewModel.process(timelineAction: .clear)
|
||||
await fulfillment(of: [expectation1, expectation2])
|
||||
XCTAssertEqual(viewModel.context.plainComposerText, NSAttributedString(string: "Hello world"))
|
||||
#expect(viewModel.context.plainComposerText == NSAttributedString(string: "Hello world"))
|
||||
}
|
||||
|
||||
func testRestoreUserMentionInPlainText() async throws {
|
||||
@Test
|
||||
func restoreUserMentionInPlainText() async throws {
|
||||
viewModel.context.composerFormattingEnabled = false
|
||||
let text = "Hello [TestName](https://matrix.to/#/@test:matrix.org)!"
|
||||
viewModel.process(timelineAction: .setText(plainText: text, htmlText: nil))
|
||||
@@ -584,7 +612,8 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testRestoreAllUsersMentionInPlainText() async throws {
|
||||
@Test
|
||||
func restoreAllUsersMentionInPlainText() async throws {
|
||||
viewModel.context.composerFormattingEnabled = false
|
||||
let text = "Hello @room"
|
||||
viewModel.process(timelineAction: .setText(plainText: text, htmlText: nil))
|
||||
@@ -603,7 +632,8 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testRestoreMixedMentionsInPlainText() async throws {
|
||||
@Test
|
||||
func restoreMixedMentionsInPlainText() async throws {
|
||||
viewModel.context.composerFormattingEnabled = false
|
||||
let text = "Hello [User1](https://matrix.to/#/@user1:matrix.org), [User2](https://matrix.to/#/@user2:matrix.org) and @room"
|
||||
viewModel.process(timelineAction: .setText(plainText: text, htmlText: nil))
|
||||
@@ -623,7 +653,8 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testRestoreAmbiguousMention() async throws {
|
||||
@Test
|
||||
func restoreAmbiguousMention() async throws {
|
||||
viewModel.context.composerFormattingEnabled = false
|
||||
let text = "Hello [User1](https://matrix.to/#/@roomuser:matrix.org)"
|
||||
viewModel.process(timelineAction: .setText(plainText: text, htmlText: nil))
|
||||
@@ -643,12 +674,12 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testRestoreDoesntOverwriteInitialText() async {
|
||||
@Test
|
||||
func restoreDoesntOverwriteInitialText() async {
|
||||
let sharedText = "Some shared text"
|
||||
let expectation = expectation(description: "Wait for draft to be restored")
|
||||
expectation.isInverted = true
|
||||
var draftLoadCalled = false
|
||||
setUpViewModel(initialText: sharedText) {
|
||||
defer { expectation.fulfill() }
|
||||
draftLoadCalled = true
|
||||
return .success(.init(plainText: "Hello world!",
|
||||
htmlText: nil,
|
||||
draftType: .newMessage))
|
||||
@@ -656,15 +687,16 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
||||
viewModel.context.composerFormattingEnabled = false
|
||||
await viewModel.loadDraft()
|
||||
|
||||
await fulfillment(of: [expectation], timeout: 1)
|
||||
XCTAssertFalse(viewModel.context.composerFormattingEnabled)
|
||||
XCTAssertEqual(viewModel.state.composerMode, .default)
|
||||
XCTAssertEqual(viewModel.context.plainComposerText, NSAttributedString(string: sharedText))
|
||||
#expect(!draftLoadCalled)
|
||||
#expect(!viewModel.context.composerFormattingEnabled)
|
||||
#expect(viewModel.state.composerMode == .default)
|
||||
#expect(viewModel.context.plainComposerText == NSAttributedString(string: sharedText))
|
||||
}
|
||||
|
||||
// MARK: - Identity Violation
|
||||
|
||||
func testVerificationViolationDisablesComposer() async throws {
|
||||
@Test
|
||||
func verificationViolationDisablesComposer() async throws {
|
||||
let mockCompletionSuggestionService = CompletionSuggestionServiceMock(configuration: .init())
|
||||
|
||||
let roomProxyMock = JoinedRoomProxyMock(.init(name: "Test"))
|
||||
@@ -695,7 +727,8 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
||||
try await fulfillment.fulfill()
|
||||
}
|
||||
|
||||
func testMultipleViolation() async throws {
|
||||
@Test
|
||||
func multipleViolation() async throws {
|
||||
let mockCompletionSuggestionService = CompletionSuggestionServiceMock(configuration: .init())
|
||||
|
||||
let roomProxyMock = JoinedRoomProxyMock(.init(name: "Test"))
|
||||
@@ -743,7 +776,8 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
||||
try await fulfillment.fulfill()
|
||||
}
|
||||
|
||||
func testPinViolationDoesNotDisableComposer() {
|
||||
@Test
|
||||
func pinViolationDoesNotDisableComposer() async throws {
|
||||
let mockCompletionSuggestionService = CompletionSuggestionServiceMock(configuration: .init())
|
||||
|
||||
let roomProxyMock = JoinedRoomProxyMock(.init(name: "Test"))
|
||||
@@ -764,19 +798,8 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
composerDraftService: draftServiceMock)
|
||||
|
||||
let expectation = expectation(description: "Composer should be enabled")
|
||||
let cancellable = viewModel
|
||||
.context
|
||||
.$viewState
|
||||
.map(\.canSend)
|
||||
.sink { canSend in
|
||||
if canSend {
|
||||
expectation.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
wait(for: [expectation], timeout: 2.0)
|
||||
cancellable.cancel()
|
||||
let deferred = deferFulfillment(viewModel.context.$viewState, message: "Composer should be enabled") { $0.canSend == true }
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
@@ -8,10 +8,11 @@
|
||||
|
||||
import Combine
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@Suite
|
||||
@MainActor
|
||||
class CreateRoomScreenViewModelTests: XCTestCase {
|
||||
final class CreateRoomScreenViewModelTests {
|
||||
var viewModel: CreateRoomScreenViewModelProtocol!
|
||||
var clientProxy: ClientProxyMock!
|
||||
var spaceService: SpaceServiceProxyMock!
|
||||
@@ -23,7 +24,7 @@ class CreateRoomScreenViewModelTests: XCTestCase {
|
||||
viewModel.context
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
deinit {
|
||||
AppSettings.resetAllSettings()
|
||||
viewModel = nil
|
||||
clientProxy = nil
|
||||
@@ -31,28 +32,31 @@ class CreateRoomScreenViewModelTests: XCTestCase {
|
||||
userSession = nil
|
||||
}
|
||||
|
||||
func testDefaultState() {
|
||||
@Test
|
||||
func defaultState() {
|
||||
setup()
|
||||
XCTAssertEqual(context.viewState.bindings.selectedAccessType, .private)
|
||||
XCTAssertNil(context.selectedSpace)
|
||||
XCTAssertEqual(context.viewState.availableAccessTypes, [.public, .askToJoin, .private])
|
||||
XCTAssertTrue(context.viewState.canSelectSpace)
|
||||
#expect(context.viewState.bindings.selectedAccessType == .private)
|
||||
#expect(context.selectedSpace == nil)
|
||||
#expect(context.viewState.availableAccessTypes == [.public, .askToJoin, .private])
|
||||
#expect(context.viewState.canSelectSpace)
|
||||
}
|
||||
|
||||
func testCreateRoomRequirements() {
|
||||
@Test
|
||||
func createRoomRequirements() {
|
||||
setup()
|
||||
XCTAssertFalse(context.viewState.canCreateRoom)
|
||||
#expect(!context.viewState.canCreateRoom)
|
||||
context.send(viewAction: .updateRoomName("A"))
|
||||
XCTAssertTrue(context.viewState.canCreateRoom)
|
||||
#expect(context.viewState.canCreateRoom)
|
||||
}
|
||||
|
||||
func testCreateRoom() async throws {
|
||||
@Test
|
||||
func createRoom() async throws {
|
||||
setup()
|
||||
// Given a form with a blank topic.
|
||||
context.send(viewAction: .updateRoomName("A"))
|
||||
context.roomTopic = ""
|
||||
context.selectedAccessType = .private
|
||||
XCTAssertTrue(context.viewState.canCreateRoom)
|
||||
#expect(context.viewState.canCreateRoom)
|
||||
|
||||
// When creating the room.
|
||||
clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReturnValue = .success("1")
|
||||
@@ -64,14 +68,15 @@ class CreateRoomScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the room should be created and a topic should not be set.
|
||||
XCTAssertTrue(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartCalled)
|
||||
XCTAssertEqual(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReceivedArguments?.name, "A")
|
||||
XCTAssertNil(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReceivedArguments?.topic,
|
||||
"The topic should be sent as nil when it is empty.")
|
||||
XCTAssertEqual(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReceivedArguments?.accessType, .private)
|
||||
#expect(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartCalled)
|
||||
#expect(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReceivedArguments?.name == "A")
|
||||
#expect(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReceivedArguments?.topic == nil,
|
||||
"The topic should be sent as nil when it is empty.")
|
||||
#expect(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReceivedArguments?.accessType == .private)
|
||||
}
|
||||
|
||||
func testCreateSpace() async throws {
|
||||
@Test
|
||||
func createSpace() async throws {
|
||||
setup(isSpace: true)
|
||||
clientProxy = ClientProxyMock(.init(userIDServerName: "matrix.org",
|
||||
userID: "@a:b.com",
|
||||
@@ -92,7 +97,7 @@ class CreateRoomScreenViewModelTests: XCTestCase {
|
||||
context.send(viewAction: .updateRoomName("A"))
|
||||
context.roomTopic = ""
|
||||
context.selectedAccessType = .private
|
||||
XCTAssertTrue(context.viewState.canCreateRoom)
|
||||
#expect(context.viewState.canCreateRoom)
|
||||
|
||||
// When creating the room.
|
||||
clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReturnValue = .success("1")
|
||||
@@ -106,14 +111,15 @@ class CreateRoomScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the room should be created and a topic should not be set.
|
||||
XCTAssertTrue(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartCalled)
|
||||
XCTAssertEqual(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReceivedArguments?.name, "A")
|
||||
XCTAssertNil(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReceivedArguments?.topic,
|
||||
"The topic should be sent as nil when it is empty.")
|
||||
XCTAssertEqual(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReceivedArguments?.accessType, .private)
|
||||
#expect(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartCalled)
|
||||
#expect(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReceivedArguments?.name == "A")
|
||||
#expect(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReceivedArguments?.topic == nil,
|
||||
"The topic should be sent as nil when it is empty.")
|
||||
#expect(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReceivedArguments?.accessType == .private)
|
||||
}
|
||||
|
||||
func testCreateKnockingRoom() async {
|
||||
@Test
|
||||
func createKnockingRoom() async {
|
||||
setup()
|
||||
context.send(viewAction: .updateRoomName("A"))
|
||||
context.roomTopic = "B"
|
||||
@@ -121,20 +127,21 @@ class CreateRoomScreenViewModelTests: XCTestCase {
|
||||
// When setting the room as private we always reset the knocking state to the default value of false
|
||||
// so we need to wait a main actor cycle to ensure the view state is updated
|
||||
await Task.yield()
|
||||
XCTAssertTrue(context.viewState.canCreateRoom)
|
||||
#expect(context.viewState.canCreateRoom)
|
||||
|
||||
let expectation = expectation(description: "Wait for the room to be created")
|
||||
clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartClosure = { _, _, accessType, _, _, _, localAliasPart in
|
||||
XCTAssertEqual(accessType, .askToJoin)
|
||||
XCTAssertEqual(localAliasPart, "a")
|
||||
defer { expectation.fulfill() }
|
||||
return .success("")
|
||||
await waitForConfirmation("Wait for the room to be created") { confirmation in
|
||||
clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartClosure = { _, _, accessType, _, _, _, localAliasPart in
|
||||
#expect(accessType == .askToJoin)
|
||||
#expect(localAliasPart == "a")
|
||||
defer { confirmation() }
|
||||
return .success("")
|
||||
}
|
||||
context.send(viewAction: .createRoom)
|
||||
}
|
||||
context.send(viewAction: .createRoom)
|
||||
await fulfillment(of: [expectation])
|
||||
}
|
||||
|
||||
func testCreatePublicRoomFailsForInvalidAlias() async throws {
|
||||
@Test
|
||||
func createPublicRoomFailsForInvalidAlias() async throws {
|
||||
setup()
|
||||
context.send(viewAction: .updateRoomName("A"))
|
||||
context.roomTopic = "B"
|
||||
@@ -154,10 +161,11 @@ class CreateRoomScreenViewModelTests: XCTestCase {
|
||||
// blocked it
|
||||
context.send(viewAction: .createRoom)
|
||||
await Task.yield()
|
||||
XCTAssertFalse(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartCalled)
|
||||
#expect(!clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartCalled)
|
||||
}
|
||||
|
||||
func testCreatePublicRoomFailsForExistingAlias() async throws {
|
||||
@Test
|
||||
func createPublicRoomFailsForExistingAlias() async throws {
|
||||
setup()
|
||||
clientProxy.isAliasAvailableReturnValue = .success(false)
|
||||
context.send(viewAction: .updateRoomName("A"))
|
||||
@@ -176,61 +184,61 @@ class CreateRoomScreenViewModelTests: XCTestCase {
|
||||
|
||||
// We also want to force the room creation in case the user may tap the button before the debounce
|
||||
// blocked it
|
||||
let expectation = expectation(description: "Wait for the alias to be checked again")
|
||||
clientProxy.isAliasAvailableClosure = { _ in
|
||||
defer {
|
||||
expectation.fulfill()
|
||||
await waitForConfirmation("Wait for the alias to be checked again") { confirmation in
|
||||
clientProxy.isAliasAvailableClosure = { _ in
|
||||
defer { confirmation() }
|
||||
return .success(false)
|
||||
}
|
||||
return .success(false)
|
||||
context.send(viewAction: .createRoom)
|
||||
}
|
||||
context.send(viewAction: .createRoom)
|
||||
await fulfillment(of: [expectation])
|
||||
XCTAssertEqual(clientProxy.isAliasAvailableCallsCount, 2)
|
||||
XCTAssertFalse(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartCalled)
|
||||
#expect(clientProxy.isAliasAvailableCallsCount == 2)
|
||||
#expect(!clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartCalled)
|
||||
}
|
||||
|
||||
func testNameAndAddressSync() async {
|
||||
@Test
|
||||
func nameAndAddressSync() async {
|
||||
setup()
|
||||
context.selectedAccessType = .private
|
||||
await Task.yield()
|
||||
context.send(viewAction: .updateRoomName("abc"))
|
||||
XCTAssertEqual(context.viewState.aliasLocalPart, "abc")
|
||||
XCTAssertEqual(context.viewState.roomName, "abc")
|
||||
#expect(context.viewState.aliasLocalPart == "abc")
|
||||
#expect(context.viewState.roomName == "abc")
|
||||
context.send(viewAction: .updateRoomName("DEF"))
|
||||
XCTAssertEqual(context.viewState.roomName, "DEF")
|
||||
XCTAssertEqual(context.viewState.aliasLocalPart, "def")
|
||||
#expect(context.viewState.roomName == "DEF")
|
||||
#expect(context.viewState.aliasLocalPart == "def")
|
||||
context.send(viewAction: .updateRoomName("a b c"))
|
||||
XCTAssertEqual(context.viewState.aliasLocalPart, "a-b-c")
|
||||
XCTAssertEqual(context.viewState.roomName, "a b c")
|
||||
#expect(context.viewState.aliasLocalPart == "a-b-c")
|
||||
#expect(context.viewState.roomName == "a b c")
|
||||
context.send(viewAction: .updateAliasLocalPart("hello-world"))
|
||||
// This removes the sync
|
||||
XCTAssertEqual(context.viewState.aliasLocalPart, "hello-world")
|
||||
XCTAssertEqual(context.viewState.roomName, "a b c")
|
||||
#expect(context.viewState.aliasLocalPart == "hello-world")
|
||||
#expect(context.viewState.roomName == "a b c")
|
||||
|
||||
context.send(viewAction: .updateRoomName("Hello Matrix!"))
|
||||
XCTAssertEqual(context.viewState.aliasLocalPart, "hello-world")
|
||||
XCTAssertEqual(context.viewState.roomName, "Hello Matrix!")
|
||||
#expect(context.viewState.aliasLocalPart == "hello-world")
|
||||
#expect(context.viewState.roomName == "Hello Matrix!")
|
||||
|
||||
// Deleting the whole name will restore the sync
|
||||
context.send(viewAction: .updateRoomName(""))
|
||||
XCTAssertEqual(context.viewState.aliasLocalPart, "")
|
||||
XCTAssertEqual(context.viewState.roomName, "")
|
||||
#expect(context.viewState.aliasLocalPart == "")
|
||||
#expect(context.viewState.roomName == "")
|
||||
|
||||
context.send(viewAction: .updateRoomName("Hello# Matrix!"))
|
||||
XCTAssertEqual(context.viewState.aliasLocalPart, "hello-matrix!")
|
||||
XCTAssertEqual(context.viewState.roomName, "Hello# Matrix!")
|
||||
#expect(context.viewState.aliasLocalPart == "hello-matrix!")
|
||||
#expect(context.viewState.roomName == "Hello# Matrix!")
|
||||
}
|
||||
|
||||
func testCreateRoomInASelectedSpaceFromTheList() async throws {
|
||||
@Test
|
||||
func createRoomInASelectedSpaceFromTheList() async throws {
|
||||
let spaces = [SpaceServiceRoom].mockJoinedSpaces2
|
||||
setup()
|
||||
|
||||
context.send(viewAction: .updateRoomName("A"))
|
||||
context.selectedAccessType = .public
|
||||
XCTAssertTrue(context.viewState.canCreateRoom)
|
||||
XCTAssertNil(context.selectedSpace)
|
||||
XCTAssertEqual(context.viewState.availableAccessTypes, [.public, .askToJoin, .private])
|
||||
XCTAssertTrue(context.viewState.canSelectSpace)
|
||||
#expect(context.viewState.canCreateRoom)
|
||||
#expect(context.selectedSpace == nil)
|
||||
#expect(context.viewState.availableAccessTypes == [.public, .askToJoin, .private])
|
||||
#expect(context.viewState.canSelectSpace)
|
||||
|
||||
var deferred = deferFulfillment(context.$viewState) { viewState in
|
||||
viewState.editableSpaces.map(\.id) == spaces.map(\.id)
|
||||
@@ -248,64 +256,62 @@ class CreateRoomScreenViewModelTests: XCTestCase {
|
||||
|
||||
// When creating the room.
|
||||
clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReturnValue = .success("1")
|
||||
let expectation = expectation(description: "Wait for the addChild function to be called")
|
||||
spaceService.addChildToClosure = { roomID, spaceID in
|
||||
defer { expectation.fulfill() }
|
||||
XCTAssertEqual(roomID, "1")
|
||||
XCTAssertEqual(spaceID, spaces[0].id)
|
||||
return .success(())
|
||||
try await confirmation("Wait for the addChild function to be called") { confirm in
|
||||
let deferredAction = deferFulfillment(viewModel.actions) { action in
|
||||
guard case .createdRoom(let roomProxy, nil) = action, roomProxy.id == "1" else { return false }
|
||||
return true
|
||||
}
|
||||
spaceService.addChildToClosure = { roomID, spaceID in
|
||||
defer { confirm() }
|
||||
#expect(roomID == "1")
|
||||
#expect(spaceID == spaces[0].id)
|
||||
return .success(())
|
||||
}
|
||||
context.send(viewAction: .createRoom)
|
||||
try await deferredAction.fulfill()
|
||||
}
|
||||
|
||||
let deferredAction = deferFulfillment(viewModel.actions) { action in
|
||||
guard case .createdRoom(let roomProxy, nil) = action, roomProxy.id == "1" else { return false }
|
||||
return true
|
||||
}
|
||||
context.send(viewAction: .createRoom)
|
||||
|
||||
await fulfillment(of: [expectation])
|
||||
try await deferredAction.fulfill()
|
||||
|
||||
XCTAssertTrue(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartCalled)
|
||||
XCTAssertEqual(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReceivedArguments?.name, "A")
|
||||
XCTAssertEqual(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReceivedArguments?.accessType, .private)
|
||||
#expect(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartCalled)
|
||||
#expect(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReceivedArguments?.name == "A")
|
||||
#expect(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReceivedArguments?.accessType == .private)
|
||||
}
|
||||
|
||||
func testCreateRoomInAnAlreadySelectedSpace() async throws {
|
||||
@Test
|
||||
func createRoomInAnAlreadySelectedSpace() async throws {
|
||||
let space = SpaceServiceRoom.mock(isSpace: true, joinRule: .invite)
|
||||
setup(spacesSelectionMode: .editableSpacesList(preSelectedSpace: space))
|
||||
|
||||
context.send(viewAction: .updateRoomName("A"))
|
||||
context.selectedAccessType = .spaceMembers
|
||||
XCTAssertTrue(context.viewState.canCreateRoom)
|
||||
XCTAssertEqual(context.selectedSpace?.id, space.id)
|
||||
XCTAssertEqual(context.viewState.availableAccessTypes, [.spaceMembers, .askToJoinWithSpaceMembers, .private])
|
||||
XCTAssertTrue(context.viewState.canSelectSpace)
|
||||
#expect(context.viewState.canCreateRoom)
|
||||
#expect(context.selectedSpace?.id == space.id)
|
||||
#expect(context.viewState.availableAccessTypes == [.spaceMembers, .askToJoinWithSpaceMembers, .private])
|
||||
#expect(context.viewState.canSelectSpace)
|
||||
|
||||
// When creating the room.
|
||||
clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReturnValue = .success("1")
|
||||
let expectation = expectation(description: "Wait for the addChild function to be called")
|
||||
spaceService.addChildToClosure = { roomID, spaceID in
|
||||
defer { expectation.fulfill() }
|
||||
XCTAssertEqual(roomID, "1")
|
||||
XCTAssertEqual(spaceID, space.id)
|
||||
return .success(())
|
||||
try await confirmation("Wait for the addChild function to be called") { confirm in
|
||||
let deferredAction = deferFulfillment(viewModel.actions) { action in
|
||||
guard case .createdRoom(let roomProxy, nil) = action, roomProxy.id == "1" else { return false }
|
||||
return true
|
||||
}
|
||||
spaceService.addChildToClosure = { roomID, spaceID in
|
||||
defer { confirm() }
|
||||
#expect(roomID == "1")
|
||||
#expect(spaceID == space.id)
|
||||
return .success(())
|
||||
}
|
||||
context.send(viewAction: .createRoom)
|
||||
try await deferredAction.fulfill()
|
||||
}
|
||||
|
||||
let deferredAction = deferFulfillment(viewModel.actions) { action in
|
||||
guard case .createdRoom(let roomProxy, nil) = action, roomProxy.id == "1" else { return false }
|
||||
return true
|
||||
}
|
||||
context.send(viewAction: .createRoom)
|
||||
|
||||
await fulfillment(of: [expectation])
|
||||
try await deferredAction.fulfill()
|
||||
|
||||
XCTAssertTrue(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartCalled)
|
||||
XCTAssertEqual(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReceivedArguments?.name, "A")
|
||||
XCTAssertEqual(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReceivedArguments?.accessType, .spaceMembers(spaceID: space.id))
|
||||
#expect(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartCalled)
|
||||
#expect(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReceivedArguments?.name == "A")
|
||||
#expect(clientProxy.createRoomNameTopicAccessTypeIsSpaceUserIDsAvatarURLAliasLocalPartReceivedArguments?.accessType == .spaceMembers(spaceID: space.id))
|
||||
}
|
||||
|
||||
func testCreateRoomInAnPublicSpaceAvailableTypes() {
|
||||
@Test
|
||||
func createRoomInAnPublicSpaceAvailableTypes() {
|
||||
let space = SpaceServiceRoom.mock(isSpace: true, joinRule: .public)
|
||||
setup(spacesSelectionMode: .editableSpacesList(preSelectedSpace: space))
|
||||
|
||||
@@ -313,10 +319,10 @@ class CreateRoomScreenViewModelTests: XCTestCase {
|
||||
context.send(viewAction: .updateRoomName("A"))
|
||||
context.roomTopic = ""
|
||||
context.selectedAccessType = .spaceMembers
|
||||
XCTAssertTrue(context.viewState.canCreateRoom)
|
||||
XCTAssertEqual(context.selectedSpace?.id, space.id)
|
||||
XCTAssertEqual(context.viewState.availableAccessTypes, [.public, .askToJoin, .private])
|
||||
XCTAssertTrue(context.viewState.canSelectSpace)
|
||||
#expect(context.viewState.canCreateRoom)
|
||||
#expect(context.selectedSpace?.id == space.id)
|
||||
#expect(context.viewState.availableAccessTypes == [.public, .askToJoin, .private])
|
||||
#expect(context.viewState.canSelectSpace)
|
||||
}
|
||||
|
||||
private func setup(isSpace: Bool = false, spacesSelectionMode: CreateRoomScreenSpaceSelectionMode = .editableSpacesList(preSelectedSpace: nil)) {
|
||||
|
||||
@@ -7,17 +7,19 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@Suite
|
||||
@MainActor
|
||||
class EditRoomAddressScreenViewModelTests: XCTestCase {
|
||||
struct EditRoomAddressScreenViewModelTests {
|
||||
var viewModel: EditRoomAddressScreenViewModelProtocol!
|
||||
|
||||
var context: EditRoomAddressScreenViewModelType.Context {
|
||||
viewModel.context
|
||||
}
|
||||
|
||||
func testCanonicalAliasChosen() async throws {
|
||||
@Test
|
||||
mutating func canonicalAliasChosen() async throws {
|
||||
let roomProxy = JoinedRoomProxyMock(.init(name: "Room Name", canonicalAlias: "#room-name:matrix.org",
|
||||
alternativeAliases: ["#beta:homeserver.io",
|
||||
"#alternative-room-name:matrix.org"]))
|
||||
@@ -34,7 +36,8 @@ class EditRoomAddressScreenViewModelTests: XCTestCase {
|
||||
}
|
||||
|
||||
/// Priority should be given to aliases from the current user's homeserver as they can edit those.
|
||||
func testAlternativeAliasChosen() async throws {
|
||||
@Test
|
||||
mutating func alternativeAliasChosen() async throws {
|
||||
let roomProxy = JoinedRoomProxyMock(.init(name: "Room Name", canonicalAlias: "#alpha:homeserver.io",
|
||||
alternativeAliases: ["#beta:homeserver.io",
|
||||
"#room-name:matrix.org",
|
||||
@@ -51,7 +54,8 @@ class EditRoomAddressScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testBuildAliasFromDisplayName() async throws {
|
||||
@Test
|
||||
mutating func buildAliasFromDisplayName() async throws {
|
||||
let roomProxy = JoinedRoomProxyMock(.init(name: "Room Name"))
|
||||
|
||||
viewModel = EditRoomAddressScreenViewModel(roomProxy: roomProxy,
|
||||
@@ -65,7 +69,8 @@ class EditRoomAddressScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testCorrectMethodsCalledOnSaveWhenNoAliasExists() async {
|
||||
@Test
|
||||
mutating func correctMethodsCalledOnSaveWhenNoAliasExists() async {
|
||||
let clientProxy = ClientProxyMock(.init(userIDServerName: "matrix.org"))
|
||||
clientProxy.isAliasAvailableReturnValue = .success(true)
|
||||
let roomProxy = JoinedRoomProxyMock(.init(name: "Room Name"))
|
||||
@@ -74,30 +79,33 @@ class EditRoomAddressScreenViewModelTests: XCTestCase {
|
||||
clientProxy: clientProxy,
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
|
||||
XCTAssertNil(roomProxy.infoPublisher.value.canonicalAlias)
|
||||
XCTAssertEqual(viewModel.context.viewState.bindings.desiredAliasLocalPart, "room-name")
|
||||
#expect(roomProxy.infoPublisher.value.canonicalAlias == nil)
|
||||
#expect(viewModel.context.viewState.bindings.desiredAliasLocalPart == "room-name")
|
||||
|
||||
let publishingExpectation = expectation(description: "Wait for publishing")
|
||||
roomProxy.publishRoomAliasInRoomDirectoryClosure = { roomAlias in
|
||||
defer { publishingExpectation.fulfill() }
|
||||
XCTAssertEqual(roomAlias, "#room-name:matrix.org")
|
||||
return .success(true)
|
||||
await waitForConfirmation("Wait for save", expectedCount: 2) { confirm in
|
||||
roomProxy.publishRoomAliasInRoomDirectoryClosure = { roomAlias in
|
||||
#expect(roomAlias == "#room-name:matrix.org")
|
||||
confirm()
|
||||
return .success(true)
|
||||
}
|
||||
|
||||
roomProxy.updateCanonicalAliasAltAliasesClosure = { roomAlias, altAliases in
|
||||
#expect(altAliases == [])
|
||||
#expect(roomAlias == "#room-name:matrix.org")
|
||||
confirm()
|
||||
return .success(())
|
||||
}
|
||||
|
||||
context.send(viewAction: .save)
|
||||
}
|
||||
|
||||
let updateAliasExpectation = expectation(description: "Wait for alias update")
|
||||
roomProxy.updateCanonicalAliasAltAliasesClosure = { roomAlias, altAliases in
|
||||
defer { updateAliasExpectation.fulfill() }
|
||||
XCTAssertEqual(altAliases, [])
|
||||
XCTAssertEqual(roomAlias, "#room-name:matrix.org")
|
||||
return .success(())
|
||||
}
|
||||
|
||||
context.send(viewAction: .save)
|
||||
await fulfillment(of: [publishingExpectation, updateAliasExpectation], timeout: 1.0)
|
||||
XCTAssertFalse(roomProxy.removeRoomAliasFromRoomDirectoryCalled)
|
||||
#expect(roomProxy.publishRoomAliasInRoomDirectoryCalled)
|
||||
#expect(roomProxy.updateCanonicalAliasAltAliasesCalled)
|
||||
#expect(!roomProxy.removeRoomAliasFromRoomDirectoryCalled)
|
||||
}
|
||||
|
||||
func testCorrectMethodsCalledOnSaveWhenAliasOnSameHomeserverExists() async {
|
||||
@Test
|
||||
mutating func correctMethodsCalledOnSaveWhenAliasOnSameHomeserverExists() async {
|
||||
let clientProxy = ClientProxyMock(.init(userIDServerName: "matrix.org"))
|
||||
clientProxy.isAliasAvailableReturnValue = .success(true)
|
||||
let roomProxy = JoinedRoomProxyMock(.init(name: "Room Name", canonicalAlias: "#old-room-name:matrix.org"))
|
||||
@@ -108,33 +116,36 @@ class EditRoomAddressScreenViewModelTests: XCTestCase {
|
||||
|
||||
context.desiredAliasLocalPart = "room-name"
|
||||
|
||||
let publishingExpectation = expectation(description: "Wait for publishing")
|
||||
roomProxy.publishRoomAliasInRoomDirectoryClosure = { roomAlias in
|
||||
defer { publishingExpectation.fulfill() }
|
||||
XCTAssertEqual(roomAlias, "#room-name:matrix.org")
|
||||
return .success(true)
|
||||
await waitForConfirmation("Wait for save", expectedCount: 3) { confirm in
|
||||
roomProxy.publishRoomAliasInRoomDirectoryClosure = { roomAlias in
|
||||
#expect(roomAlias == "#room-name:matrix.org")
|
||||
confirm()
|
||||
return .success(true)
|
||||
}
|
||||
|
||||
roomProxy.updateCanonicalAliasAltAliasesClosure = { roomAlias, altAliases in
|
||||
#expect(altAliases == [])
|
||||
#expect(roomAlias == "#room-name:matrix.org")
|
||||
confirm()
|
||||
return .success(())
|
||||
}
|
||||
|
||||
roomProxy.removeRoomAliasFromRoomDirectoryClosure = { roomAlias in
|
||||
#expect(roomAlias == "#old-room-name:matrix.org")
|
||||
confirm()
|
||||
return .success(true)
|
||||
}
|
||||
|
||||
context.send(viewAction: .save)
|
||||
}
|
||||
|
||||
let updateAliasExpectation = expectation(description: "Wait for alias update")
|
||||
roomProxy.updateCanonicalAliasAltAliasesClosure = { roomAlias, altAliases in
|
||||
defer { updateAliasExpectation.fulfill() }
|
||||
XCTAssertEqual(altAliases, [])
|
||||
XCTAssertEqual(roomAlias, "#room-name:matrix.org")
|
||||
return .success(())
|
||||
}
|
||||
|
||||
let removeAliasExpectation = expectation(description: "Wait for alias removal")
|
||||
roomProxy.removeRoomAliasFromRoomDirectoryClosure = { roomAlias in
|
||||
defer { removeAliasExpectation.fulfill() }
|
||||
XCTAssertEqual(roomAlias, "#old-room-name:matrix.org")
|
||||
return .success(true)
|
||||
}
|
||||
|
||||
context.send(viewAction: .save)
|
||||
await fulfillment(of: [publishingExpectation, updateAliasExpectation, removeAliasExpectation], timeout: 1.0)
|
||||
#expect(roomProxy.publishRoomAliasInRoomDirectoryCalled)
|
||||
#expect(roomProxy.updateCanonicalAliasAltAliasesCalled)
|
||||
#expect(roomProxy.removeRoomAliasFromRoomDirectoryCalled)
|
||||
}
|
||||
|
||||
func testCorrectMethodsCalledOnSaveWhenAliasOnOtherHomeserverExists() async {
|
||||
@Test
|
||||
mutating func correctMethodsCalledOnSaveWhenAliasOnOtherHomeserverExists() async {
|
||||
let clientProxy = ClientProxyMock(.init(userIDServerName: "matrix.org"))
|
||||
clientProxy.isAliasAvailableReturnValue = .success(true)
|
||||
let roomProxy = JoinedRoomProxyMock(.init(name: "Room Name", canonicalAlias: "#old-room-name:element.io"))
|
||||
@@ -145,23 +156,25 @@ class EditRoomAddressScreenViewModelTests: XCTestCase {
|
||||
|
||||
context.desiredAliasLocalPart = "room-name"
|
||||
|
||||
let publishingExpectation = expectation(description: "Wait for publishing")
|
||||
roomProxy.publishRoomAliasInRoomDirectoryClosure = { roomAlias in
|
||||
defer { publishingExpectation.fulfill() }
|
||||
XCTAssertEqual(roomAlias, "#room-name:matrix.org")
|
||||
return .success(true)
|
||||
await waitForConfirmation("Wait for save", expectedCount: 2) { confirm in
|
||||
roomProxy.publishRoomAliasInRoomDirectoryClosure = { roomAlias in
|
||||
#expect(roomAlias == "#room-name:matrix.org")
|
||||
confirm()
|
||||
return .success(true)
|
||||
}
|
||||
|
||||
roomProxy.updateCanonicalAliasAltAliasesClosure = { roomAlias, altAliases in
|
||||
#expect(altAliases == ["#room-name:matrix.org"])
|
||||
#expect(roomAlias == "#old-room-name:element.io")
|
||||
confirm()
|
||||
return .success(())
|
||||
}
|
||||
|
||||
context.send(viewAction: .save)
|
||||
}
|
||||
|
||||
let updateAliasExpectation = expectation(description: "Wait for alias update")
|
||||
roomProxy.updateCanonicalAliasAltAliasesClosure = { roomAlias, altAliases in
|
||||
defer { updateAliasExpectation.fulfill() }
|
||||
XCTAssertEqual(altAliases, ["#room-name:matrix.org"])
|
||||
XCTAssertEqual(roomAlias, "#old-room-name:element.io")
|
||||
return .success(())
|
||||
}
|
||||
|
||||
context.send(viewAction: .save)
|
||||
await fulfillment(of: [publishingExpectation, updateAliasExpectation], timeout: 1.0)
|
||||
XCTAssertFalse(roomProxy.removeRoomAliasFromRoomDirectoryCalled)
|
||||
#expect(roomProxy.publishRoomAliasInRoomDirectoryCalled)
|
||||
#expect(roomProxy.updateCanonicalAliasAltAliasesCalled)
|
||||
#expect(!roomProxy.removeRoomAliasFromRoomDirectoryCalled)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,11 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
class JoinRoomScreenViewModelTests: XCTestCase {
|
||||
@Suite
|
||||
final class JoinRoomScreenViewModelTests {
|
||||
private enum TestMode {
|
||||
case joined
|
||||
case knocked
|
||||
@@ -27,74 +28,79 @@ class JoinRoomScreenViewModelTests: XCTestCase {
|
||||
viewModel.context
|
||||
}
|
||||
|
||||
override func setUp() {
|
||||
init() {
|
||||
AppSettings.resetAllSettings()
|
||||
appSettings = AppSettings()
|
||||
ServiceLocator.shared.register(appSettings: appSettings)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
deinit {
|
||||
viewModel = nil
|
||||
clientProxy = nil
|
||||
AppSettings.resetAllSettings()
|
||||
}
|
||||
|
||||
func testInteraction() async throws {
|
||||
XCTAssertTrue(appSettings.seenInvites.isEmpty, "There shouldn't be any seen invites before running the tests.")
|
||||
@Test
|
||||
func interaction() async throws {
|
||||
#expect(appSettings.seenInvites.isEmpty, "There shouldn't be any seen invites before running the tests.")
|
||||
|
||||
setupViewModel()
|
||||
try await deferFulfillment(viewModel.context.$viewState) { $0.mode == .joinable }.fulfill()
|
||||
|
||||
XCTAssertTrue(appSettings.seenInvites.isEmpty, "Only an invited room should register the room ID as a seen invite.")
|
||||
#expect(appSettings.seenInvites.isEmpty, "Only an invited room should register the room ID as a seen invite.")
|
||||
|
||||
let deferred = deferFulfillment(viewModel.actionsPublisher) { $0 == .joined(.roomID("1")) }
|
||||
context.send(viewAction: .join)
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testAcceptInviteInteraction() async throws {
|
||||
XCTAssertTrue(appSettings.seenInvites.isEmpty, "There shouldn't be any seen invites before running the tests.")
|
||||
@Test
|
||||
func acceptInviteInteraction() async throws {
|
||||
#expect(appSettings.seenInvites.isEmpty, "There shouldn't be any seen invites before running the tests.")
|
||||
|
||||
setupViewModel(mode: .invited)
|
||||
try await deferFulfillment(viewModel.context.$viewState) { $0.mode == .invited(isDM: false) }.fulfill()
|
||||
|
||||
XCTAssertEqual(appSettings.seenInvites, ["1"], "The invited room's ID should be registered as a seen invite.")
|
||||
#expect(appSettings.seenInvites == ["1"], "The invited room's ID should be registered as a seen invite.")
|
||||
|
||||
let deferred = deferFulfillment(viewModel.actionsPublisher) { $0 == .joined(.roomID("1")) }
|
||||
context.send(viewAction: .acceptInvite)
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertTrue(appSettings.seenInvites.isEmpty, "The after accepting an invite the invite should be forgotten in case the user leaves.")
|
||||
#expect(appSettings.seenInvites.isEmpty, "The after accepting an invite the invite should be forgotten in case the user leaves.")
|
||||
}
|
||||
|
||||
func testDeclineInviteInteraction() async throws {
|
||||
XCTAssertTrue(appSettings.seenInvites.isEmpty, "There shouldn't be any seen invites before running the tests.")
|
||||
@Test
|
||||
func declineInviteInteraction() async throws {
|
||||
#expect(appSettings.seenInvites.isEmpty, "There shouldn't be any seen invites before running the tests.")
|
||||
|
||||
setupViewModel(mode: .invited)
|
||||
|
||||
try await deferFulfillment(viewModel.context.$viewState) { $0.mode == .invited(isDM: false) }.fulfill()
|
||||
XCTAssertEqual(appSettings.seenInvites, ["1"], "The invited room's ID should be registered as a seen invite.")
|
||||
#expect(appSettings.seenInvites == ["1"], "The invited room's ID should be registered as a seen invite.")
|
||||
|
||||
context.send(viewAction: .declineInvite)
|
||||
|
||||
XCTAssertEqual(viewModel.context.alertInfo?.id, .declineInvite)
|
||||
#expect(viewModel.context.alertInfo?.id == .declineInvite)
|
||||
let deferred = deferFulfillment(viewModel.actionsPublisher) { $0 == .dismiss }
|
||||
context.alertInfo?.secondaryButton?.action?()
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertTrue(appSettings.seenInvites.isEmpty, "The after declining an invite the invite should be forgotten in case another invite is received.")
|
||||
#expect(appSettings.seenInvites.isEmpty, "The after declining an invite the invite should be forgotten in case another invite is received.")
|
||||
}
|
||||
|
||||
func testKnockedState() async throws {
|
||||
XCTAssertTrue(appSettings.seenInvites.isEmpty, "There shouldn't be any seen invites before running the tests.")
|
||||
@Test
|
||||
func knockedState() async throws {
|
||||
#expect(appSettings.seenInvites.isEmpty, "There shouldn't be any seen invites before running the tests.")
|
||||
setupViewModel(mode: .knocked)
|
||||
|
||||
try await deferFulfillment(viewModel.context.$viewState) { $0.mode == .knocked }.fulfill()
|
||||
|
||||
XCTAssertTrue(appSettings.seenInvites.isEmpty, "Only an invited room should register the room ID as a seen invite.")
|
||||
#expect(appSettings.seenInvites.isEmpty, "Only an invited room should register the room ID as a seen invite.")
|
||||
}
|
||||
|
||||
func testCancelKnock() async throws {
|
||||
@Test
|
||||
func cancelKnock() async throws {
|
||||
setupViewModel(mode: .knocked)
|
||||
|
||||
try await deferFulfillment(viewModel.context.$viewState) { state in
|
||||
@@ -102,7 +108,7 @@ class JoinRoomScreenViewModelTests: XCTestCase {
|
||||
}.fulfill()
|
||||
|
||||
context.send(viewAction: .cancelKnock)
|
||||
XCTAssertEqual(viewModel.context.alertInfo?.id, .cancelKnock)
|
||||
#expect(viewModel.context.alertInfo?.id == .cancelKnock)
|
||||
|
||||
let deferred = deferFulfillment(viewModel.actionsPublisher) { action in
|
||||
action == .dismiss
|
||||
@@ -111,32 +117,36 @@ class JoinRoomScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testDeclineAndBlockInviteLegacyInteraction() async throws {
|
||||
@Test
|
||||
func declineAndBlockInviteLegacyInteraction() async throws {
|
||||
setupViewModel(mode: .invited)
|
||||
clientProxy.underlyingIsReportRoomSupported = false
|
||||
let expectation = expectation(description: "Wait for the user to be ignored")
|
||||
clientProxy.ignoreUserClosure = { userID in
|
||||
defer { expectation.fulfill() }
|
||||
XCTAssertEqual(userID, "@test:matrix.org")
|
||||
return .success(())
|
||||
}
|
||||
|
||||
try await deferFulfillment(viewModel.context.$viewState) { $0.roomDetails != nil }.fulfill()
|
||||
|
||||
context.send(viewAction: .declineInviteAndBlock(userID: "@test:matrix.org"))
|
||||
|
||||
try await deferFulfillment(viewModel.context.$viewState) { $0.bindings.alertInfo != nil }.fulfill()
|
||||
XCTAssertEqual(viewModel.context.alertInfo?.id, .declineInviteAndBlock)
|
||||
#expect(viewModel.context.alertInfo?.id == .declineInviteAndBlock)
|
||||
|
||||
let deferred = deferFulfillment(viewModel.actionsPublisher) { action in
|
||||
action == .dismiss
|
||||
}
|
||||
context.alertInfo?.secondaryButton?.action?()
|
||||
await fulfillment(of: [expectation], timeout: 10)
|
||||
|
||||
await waitForConfirmation("Wait for the user to be ignored") { confirm in
|
||||
clientProxy.ignoreUserClosure = { userID in
|
||||
defer { confirm() }
|
||||
#expect(userID == "@test:matrix.org")
|
||||
return .success(())
|
||||
}
|
||||
context.alertInfo?.secondaryButton?.action?()
|
||||
}
|
||||
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testDeclineAndBlockInviteInteraction() async throws {
|
||||
@Test
|
||||
func declineAndBlockInviteInteraction() async throws {
|
||||
setupViewModel(mode: .invited)
|
||||
try await deferFulfillment(viewModel.context.$viewState) { $0.roomDetails != nil }.fulfill()
|
||||
let deferredAction = deferFulfillment(viewModel.actionsPublisher) { $0 == .presentDeclineAndBlock(userID: "@test:matrix.org") }
|
||||
@@ -144,7 +154,8 @@ class JoinRoomScreenViewModelTests: XCTestCase {
|
||||
try await deferredAction.fulfill()
|
||||
}
|
||||
|
||||
func testForgetRoom() async throws {
|
||||
@Test
|
||||
func forgetRoom() async throws {
|
||||
setupViewModel(mode: .banned)
|
||||
|
||||
try await deferFulfillment(viewModel.context.$viewState) { $0.roomDetails != nil }.fulfill()
|
||||
|
||||
@@ -8,10 +8,12 @@
|
||||
|
||||
@testable import ElementX
|
||||
import SwiftUI
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
final class CollapsibleFlowLayoutTests: XCTestCase {
|
||||
func testFlowLayoutWithExpandAndCollapse() {
|
||||
@Suite
|
||||
struct CollapsibleFlowLayoutTests {
|
||||
@Test
|
||||
func flowLayoutWithExpandAndCollapse() {
|
||||
let containerSize = CGSize(width: 250, height: 400)
|
||||
var flowLayout = CollapsibleReactionLayout(itemSpacing: 5, rowSpacing: 5, rowsBeforeCollapsible: 2)
|
||||
|
||||
@@ -25,7 +27,7 @@ final class CollapsibleFlowLayoutTests: XCTestCase {
|
||||
var size = flowLayout.sizeThatFits(proposal: ProposedViewSize(containerSize), subviews: subviewsMock, cache: &a)
|
||||
|
||||
// Collapsed target layout has 2 rows of 2 items, so just 1 spacing between items hence 205, 105
|
||||
XCTAssertEqual(size, CGSize(width: 205, height: 105))
|
||||
#expect(size == CGSize(width: 205, height: 105))
|
||||
flowLayout.placeSubviews(in: CGRect(origin: .zero, size: size), proposal: ProposedViewSize(containerSize), subviews: subviewsMock, cache: &a)
|
||||
|
||||
// 4 items are hidden in the collapsed state (put in the centre with zero size)
|
||||
@@ -39,7 +41,7 @@ final class CollapsibleFlowLayoutTests: XCTestCase {
|
||||
CGRect(x: -10000, y: -10000, width: 0, height: 0),
|
||||
CGRect(x: -10000, y: -10000, width: 0, height: 0)
|
||||
]
|
||||
XCTAssertEqual(placedViews, targetPlacements)
|
||||
#expect(placedViews == targetPlacements)
|
||||
|
||||
flowLayout.collapsed = false
|
||||
placedViews = []
|
||||
@@ -47,7 +49,7 @@ final class CollapsibleFlowLayoutTests: XCTestCase {
|
||||
size = flowLayout.sizeThatFits(proposal: ProposedViewSize(containerSize), subviews: subviewsMock, cache: &a)
|
||||
|
||||
// Expanded target layout has 4 rows and no more than 2 items per row
|
||||
XCTAssertEqual(size, CGSize(width: 205, height: 215))
|
||||
#expect(size == CGSize(width: 205, height: 215))
|
||||
|
||||
flowLayout.placeSubviews(in: CGRect(origin: .zero, size: size), proposal: ProposedViewSize(containerSize), subviews: subviewsMock, cache: &a)
|
||||
|
||||
@@ -61,10 +63,11 @@ final class CollapsibleFlowLayoutTests: XCTestCase {
|
||||
CGRect(x: 0, y: 190, width: 100, height: 50),
|
||||
CGRect(x: 105.0, y: 190, width: 100, height: 50)
|
||||
]
|
||||
XCTAssertEqual(placedViews, targetPlacements)
|
||||
#expect(placedViews == targetPlacements)
|
||||
}
|
||||
|
||||
func testFlowLayoutWithExpandButtonAndAddMoreIsHidden() {
|
||||
@Test
|
||||
func flowLayoutWithExpandButtonAndAddMoreIsHidden() {
|
||||
let containerSize = CGSize(width: 250, height: 400)
|
||||
let flowLayout = CollapsibleReactionLayout(itemSpacing: 5, rowSpacing: 5, rowsBeforeCollapsible: 2)
|
||||
|
||||
@@ -78,7 +81,7 @@ final class CollapsibleFlowLayoutTests: XCTestCase {
|
||||
var a: () = ()
|
||||
let size = flowLayout.sizeThatFits(proposal: ProposedViewSize(containerSize), subviews: subviewsMock, cache: &a)
|
||||
|
||||
XCTAssertEqual(size, CGSize(width: 205, height: 105))
|
||||
#expect(size == CGSize(width: 205, height: 105))
|
||||
flowLayout.placeSubviews(in: CGRect(origin: .zero, size: size), proposal: ProposedViewSize(containerSize), subviews: subviewsMock, cache: &a)
|
||||
|
||||
let targetPlacements: [CGRect] = [
|
||||
@@ -90,10 +93,11 @@ final class CollapsibleFlowLayoutTests: XCTestCase {
|
||||
// Expand/Collapse button is hidden
|
||||
CGRect(x: -10000, y: -10000, width: 0, height: 0)
|
||||
]
|
||||
XCTAssertEqual(placedViews, targetPlacements)
|
||||
#expect(placedViews == targetPlacements)
|
||||
}
|
||||
|
||||
func testHeightIsCorrectGivenASmallerAddButton() {
|
||||
@Test
|
||||
func heightIsCorrectGivenASmallerAddButton() {
|
||||
let containerSize = CGSize(width: 250, height: 400)
|
||||
let flowLayout = CollapsibleReactionLayout(itemSpacing: 5, rowSpacing: 5, rowsBeforeCollapsible: 2)
|
||||
|
||||
@@ -110,7 +114,7 @@ final class CollapsibleFlowLayoutTests: XCTestCase {
|
||||
var a: () = ()
|
||||
let size = flowLayout.sizeThatFits(proposal: ProposedViewSize(containerSize), subviews: subviewsMock, cache: &a)
|
||||
|
||||
XCTAssertEqual(size, CGSize(width: 205, height: 105))
|
||||
#expect(size == CGSize(width: 205, height: 105))
|
||||
flowLayout.placeSubviews(in: CGRect(origin: .zero, size: size), proposal: ProposedViewSize(containerSize), subviews: subviewsMock, cache: &a)
|
||||
|
||||
let targetPlacements: [CGRect] = [
|
||||
@@ -121,10 +125,11 @@ final class CollapsibleFlowLayoutTests: XCTestCase {
|
||||
// Expand/Collapse button is hidden
|
||||
CGRect(x: -10000, y: -10000, width: 0, height: 0)
|
||||
]
|
||||
XCTAssertEqual(placedViews, targetPlacements)
|
||||
#expect(placedViews == targetPlacements)
|
||||
}
|
||||
|
||||
func testFlowLayoutEmptyState() {
|
||||
@Test
|
||||
func flowLayoutEmptyState() {
|
||||
let containerSize = CGSize(width: 250, height: 400)
|
||||
let flowLayout = CollapsibleReactionLayout(itemSpacing: 5, rowSpacing: 5, rowsBeforeCollapsible: 2)
|
||||
|
||||
@@ -137,7 +142,7 @@ final class CollapsibleFlowLayoutTests: XCTestCase {
|
||||
var a: () = ()
|
||||
let size = flowLayout.sizeThatFits(proposal: ProposedViewSize(containerSize), subviews: subviewsMock, cache: &a)
|
||||
|
||||
XCTAssertEqual(size, CGSize(width: 0, height: 0))
|
||||
#expect(size == CGSize(width: 0, height: 0))
|
||||
flowLayout.placeSubviews(in: CGRect(origin: .zero, size: size), proposal: ProposedViewSize(containerSize), subviews: subviewsMock, cache: &a)
|
||||
|
||||
let targetPlacements: [CGRect] = [
|
||||
@@ -145,7 +150,7 @@ final class CollapsibleFlowLayoutTests: XCTestCase {
|
||||
CGRect(x: -10000, y: -10000, width: 0, height: 0),
|
||||
CGRect(x: -10000, y: -10000, width: 0, height: 0)
|
||||
]
|
||||
XCTAssertEqual(placedViews, targetPlacements)
|
||||
#expect(placedViews == targetPlacements)
|
||||
}
|
||||
|
||||
func createReactionLayoutSubviews(with sizes: [CGSize],
|
||||
|
||||
@@ -9,17 +9,19 @@
|
||||
import Combine
|
||||
@testable import ElementX
|
||||
import Kingfisher
|
||||
import XCTest
|
||||
import SwiftUI
|
||||
import Testing
|
||||
|
||||
@Suite
|
||||
@MainActor
|
||||
final class MediaProviderTests: XCTestCase {
|
||||
private var mediaLoader: MediaLoaderMock!
|
||||
private var imageCache: MockImageCache!
|
||||
struct MediaProviderTests {
|
||||
private var mediaLoader: MediaLoaderMock
|
||||
private var imageCache: MockImageCache
|
||||
private var reachabilitySubject = CurrentValueSubject<NetworkMonitorReachability, Never>(.reachable)
|
||||
|
||||
var mediaProvider: MediaProvider!
|
||||
|
||||
override func setUp() {
|
||||
init() {
|
||||
mediaLoader = MediaLoaderMock()
|
||||
imageCache = MockImageCache(name: "Test")
|
||||
|
||||
@@ -28,12 +30,10 @@ final class MediaProviderTests: XCTestCase {
|
||||
homeserverReachabilityPublisher: reachabilitySubject.asCurrentValuePublisher())
|
||||
}
|
||||
|
||||
func testLoadingRetriedOnReconnection() async throws {
|
||||
@Test
|
||||
func loadingRetriedOnReconnection() async throws {
|
||||
let testImage = try loadTestImage()
|
||||
guard let pngData = testImage.pngData() else {
|
||||
XCTFail("Test image should contain valid .png data")
|
||||
return
|
||||
}
|
||||
let pngData = try #require(testImage.pngData(), "Test image should contain valid .png data")
|
||||
|
||||
let loadTask = try mediaProvider.loadImageRetryingOnReconnection(MediaSourceProxy(url: .mockMXCImage, mimeType: "image/jpeg"))
|
||||
|
||||
@@ -51,11 +51,12 @@ final class MediaProviderTests: XCTestCase {
|
||||
|
||||
let result = try? await loadTask.value
|
||||
|
||||
XCTAssertNotNil(result)
|
||||
XCTAssertEqual(mediaLoader.loadMediaContentForSourceCallsCount, 2)
|
||||
#expect(result != nil)
|
||||
#expect(mediaLoader.loadMediaContentForSourceCallsCount == 2)
|
||||
}
|
||||
|
||||
func testLoadingRetriedOnReconnectionCancelsAfterSecondFailure() async throws {
|
||||
@Test
|
||||
func loadingRetriedOnReconnectionCancelsAfterSecondFailure() async throws {
|
||||
let loadTask = try mediaProvider.loadImageRetryingOnReconnection(MediaSourceProxy(url: .mockMXCImage, mimeType: "image/jpeg"))
|
||||
|
||||
reachabilitySubject.send(.reachable)
|
||||
@@ -64,15 +65,17 @@ final class MediaProviderTests: XCTestCase {
|
||||
|
||||
let result = try? await loadTask.value
|
||||
|
||||
XCTAssertNil(result)
|
||||
#expect(result == nil)
|
||||
}
|
||||
|
||||
func test_whenImageFromSourceWithSourceNil_nilReturned() {
|
||||
@Test
|
||||
func whenImageFromSourceWithSourceNil_nilReturned() {
|
||||
let image = mediaProvider.imageFromSource(nil, size: Avatars.Size.room(on: .timeline).scaledSize)
|
||||
XCTAssertNil(image)
|
||||
#expect(image == nil)
|
||||
}
|
||||
|
||||
func test_whenImageFromSourceWithSourceNotNilAndImageCacheContainsImage_ImageIsReturned() throws {
|
||||
@Test
|
||||
func whenImageFromSourceWithSourceNotNilAndImageCacheContainsImage_ImageIsReturned() throws {
|
||||
let avatarSize = Avatars.Size.room(on: .timeline)
|
||||
let url = URL.mockMXCImage
|
||||
let key = "\(url.absoluteString){\(avatarSize.scaledValue),\(avatarSize.scaledValue)}"
|
||||
@@ -80,16 +83,18 @@ final class MediaProviderTests: XCTestCase {
|
||||
imageCache.retrievedImagesInMemory[key] = imageForKey
|
||||
let image = try mediaProvider.imageFromSource(MediaSourceProxy(url: url, mimeType: "image/jpeg"),
|
||||
size: avatarSize.scaledSize)
|
||||
XCTAssertEqual(image, imageForKey)
|
||||
#expect(image == imageForKey)
|
||||
}
|
||||
|
||||
func test_whenImageFromSourceWithSourceNotNilAndImageNotCached_nilReturned() throws {
|
||||
@Test
|
||||
func whenImageFromSourceWithSourceNotNilAndImageNotCached_nilReturned() throws {
|
||||
let image = try mediaProvider.imageFromSource(MediaSourceProxy(url: .mockMXCImage, mimeType: "image/jpeg"),
|
||||
size: Avatars.Size.room(on: .timeline).scaledSize)
|
||||
XCTAssertNil(image)
|
||||
#expect(image == nil)
|
||||
}
|
||||
|
||||
func test_whenLoadImageFromSourceAndImageCacheContainsImage_successIsReturned() async throws {
|
||||
@Test
|
||||
func whenLoadImageFromSourceAndImageCacheContainsImage_successIsReturned() async throws {
|
||||
let avatarSize = Avatars.Size.room(on: .timeline)
|
||||
let url = URL.mockMXCImage
|
||||
let key = "\(url.absoluteString){\(avatarSize.scaledValue),\(avatarSize.scaledValue)}"
|
||||
@@ -97,10 +102,11 @@ final class MediaProviderTests: XCTestCase {
|
||||
imageCache.retrievedImagesInMemory[key] = imageForKey
|
||||
let result = try await mediaProvider.loadImageFromSource(MediaSourceProxy(url: url, mimeType: "image/jpeg"),
|
||||
size: avatarSize.scaledSize)
|
||||
XCTAssertEqual(Result.success(imageForKey), result)
|
||||
#expect(Result.success(imageForKey) == result)
|
||||
}
|
||||
|
||||
func test_whenLoadImageFromSourceAndImageNotCachedAndRetrieveImageSucceeds_successIsReturned() async throws {
|
||||
@Test
|
||||
func whenLoadImageFromSourceAndImageNotCachedAndRetrieveImageSucceeds_successIsReturned() async throws {
|
||||
let avatarSize = Avatars.Size.room(on: .timeline)
|
||||
let url = URL.mockMXCImage
|
||||
let key = "\(url.absoluteString){\(avatarSize.scaledValue),\(avatarSize.scaledValue)}"
|
||||
@@ -108,10 +114,11 @@ final class MediaProviderTests: XCTestCase {
|
||||
imageCache.retrievedImages[key] = imageForKey
|
||||
let result = try await mediaProvider.loadImageFromSource(MediaSourceProxy(url: url, mimeType: "image/jpeg"),
|
||||
size: avatarSize.scaledSize)
|
||||
XCTAssertEqual(Result.success(imageForKey), result)
|
||||
#expect(Result.success(imageForKey) == result)
|
||||
}
|
||||
|
||||
func test_whenLoadImageFromSourceAndImageNotCachedAndRetrieveImageFails_imageThumbnailIsLoaded() async throws {
|
||||
@Test
|
||||
func whenLoadImageFromSourceAndImageNotCachedAndRetrieveImageFails_imageThumbnailIsLoaded() async throws {
|
||||
let avatarSize = Avatars.Size.room(on: .timeline)
|
||||
let expectedImage = try loadTestImage()
|
||||
|
||||
@@ -121,13 +128,14 @@ final class MediaProviderTests: XCTestCase {
|
||||
size: avatarSize.scaledSize)
|
||||
switch result {
|
||||
case .success(let image):
|
||||
XCTAssertEqual(image.pngData(), expectedImage.pngData())
|
||||
#expect(image.pngData() == expectedImage.pngData())
|
||||
case .failure:
|
||||
XCTFail("Should be success")
|
||||
Issue.record("Should be success")
|
||||
}
|
||||
}
|
||||
|
||||
func test_whenLoadImageFromSourceAndImageNotCachedAndRetrieveImageFails_imageIsStored() async throws {
|
||||
@Test
|
||||
func whenLoadImageFromSourceAndImageNotCachedAndRetrieveImageFails_imageIsStored() async throws {
|
||||
let avatarSize = Avatars.Size.room(on: .timeline)
|
||||
let url = URL.mockMXCImage
|
||||
let key = "\(url.absoluteString){\(avatarSize.scaledValue),\(avatarSize.scaledValue)}"
|
||||
@@ -137,11 +145,12 @@ final class MediaProviderTests: XCTestCase {
|
||||
|
||||
_ = try await mediaProvider.loadImageFromSource(MediaSourceProxy(url: url, mimeType: "image/jpeg"),
|
||||
size: avatarSize.scaledSize)
|
||||
let storedImage = try XCTUnwrap(imageCache.storedImages[key])
|
||||
XCTAssertEqual(expectedImage.pngData(), storedImage.pngData())
|
||||
let storedImage = try #require(imageCache.storedImages[key])
|
||||
#expect(expectedImage.pngData() == storedImage.pngData())
|
||||
}
|
||||
|
||||
func test_whenLoadImageFromSourceAndImageNotCachedAndRetrieveImageFailsAndNoAvatarSize_imageContentIsLoaded() async throws {
|
||||
@Test
|
||||
func whenLoadImageFromSourceAndImageNotCachedAndRetrieveImageFailsAndNoAvatarSize_imageContentIsLoaded() async throws {
|
||||
let expectedImage = try loadTestImage()
|
||||
|
||||
mediaLoader.loadMediaContentForSourceReturnValue = expectedImage.pngData()
|
||||
@@ -150,53 +159,56 @@ final class MediaProviderTests: XCTestCase {
|
||||
size: nil)
|
||||
switch result {
|
||||
case .success(let image):
|
||||
XCTAssertEqual(image.pngData(), expectedImage.pngData())
|
||||
#expect(image.pngData() == expectedImage.pngData())
|
||||
case .failure:
|
||||
XCTFail("Should be success")
|
||||
Issue.record("Should be success")
|
||||
}
|
||||
}
|
||||
|
||||
func test_whenLoadImageFromSourceAndImageNotCachedAndRetrieveImageFailsAndLoadImageThumbnailFails_errorIsThrown() async throws {
|
||||
@Test
|
||||
func whenLoadImageFromSourceAndImageNotCachedAndRetrieveImageFailsAndLoadImageThumbnailFails_errorIsThrown() async throws {
|
||||
mediaLoader.loadMediaThumbnailForSourceWidthHeightThrowableError = MediaProviderTestsError.error
|
||||
|
||||
let result = try await mediaProvider.loadImageFromSource(MediaSourceProxy(url: .mockMXCImage, mimeType: "image/jpeg"),
|
||||
size: Avatars.Size.room(on: .timeline).scaledSize)
|
||||
switch result {
|
||||
case .success:
|
||||
XCTFail("Should fail")
|
||||
Issue.record("Should fail")
|
||||
case .failure(let error):
|
||||
XCTAssertEqual(error, MediaProviderError.failedRetrievingImage)
|
||||
#expect(error == MediaProviderError.failedRetrievingImage)
|
||||
}
|
||||
}
|
||||
|
||||
func test_whenLoadImageFromSourceAndImageNotCachedAndRetrieveImageFailsAndNoAvatarSizeAndLoadImageContentFails_errorIsThrown() async throws {
|
||||
@Test
|
||||
func whenLoadImageFromSourceAndImageNotCachedAndRetrieveImageFailsAndNoAvatarSizeAndLoadImageContentFails_errorIsThrown() async throws {
|
||||
mediaLoader.loadMediaContentForSourceThrowableError = MediaProviderTestsError.error
|
||||
|
||||
let result = try await mediaProvider.loadImageFromSource(MediaSourceProxy(url: .mockMXCImage, mimeType: "image/jpeg"),
|
||||
size: nil)
|
||||
switch result {
|
||||
case .success:
|
||||
XCTFail("Should fail")
|
||||
Issue.record("Should fail")
|
||||
case .failure(let error):
|
||||
XCTAssertEqual(error, MediaProviderError.failedRetrievingImage)
|
||||
#expect(error == MediaProviderError.failedRetrievingImage)
|
||||
}
|
||||
}
|
||||
|
||||
func test_whenLoadImageFromSourceAndImageNotCachedAndRetrieveImageFailsAndImageThumbnailIsLoadedWithCorruptedData_errorIsThrown() async throws {
|
||||
@Test
|
||||
func whenLoadImageFromSourceAndImageNotCachedAndRetrieveImageFailsAndImageThumbnailIsLoadedWithCorruptedData_errorIsThrown() async throws {
|
||||
mediaLoader.loadMediaThumbnailForSourceWidthHeightReturnValue = Data()
|
||||
|
||||
let result = try await mediaProvider.loadImageFromSource(MediaSourceProxy(url: .mockMXCImage, mimeType: "image/jpeg"),
|
||||
size: Avatars.Size.room(on: .timeline).scaledSize)
|
||||
switch result {
|
||||
case .success:
|
||||
XCTFail("Should fail")
|
||||
Issue.record("Should fail")
|
||||
case .failure(let error):
|
||||
XCTAssertEqual(error, MediaProviderError.invalidImageData)
|
||||
#expect(error == MediaProviderError.invalidImageData)
|
||||
}
|
||||
}
|
||||
|
||||
private func loadTestImage() throws -> UIImage {
|
||||
guard let path = Bundle(for: Self.self).path(forResource: "test_image", ofType: "png"),
|
||||
guard let path = Bundle(for: UnitTestsAppCoordinator.self).path(forResource: "test_image", ofType: "png"),
|
||||
let image = UIImage(contentsOfFile: path) else {
|
||||
throw MediaProviderTestsError.error
|
||||
}
|
||||
|
||||
@@ -7,10 +7,12 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
@Suite
|
||||
@MainActor
|
||||
class MediaUploadPreviewScreenViewModelTests: XCTestCase {
|
||||
final class MediaUploadPreviewScreenViewModelTests {
|
||||
var timelineProxy: TimelineProxyMock!
|
||||
var clientProxy: ClientProxyMock!
|
||||
var userIndicatorController: UserIndicatorControllerMock!
|
||||
@@ -25,7 +27,7 @@ class MediaUploadPreviewScreenViewModelTests: XCTestCase {
|
||||
case unknown
|
||||
}
|
||||
|
||||
override func setUp() {
|
||||
init() {
|
||||
AppSettings.resetAllSettings()
|
||||
let appSettings = AppSettings()
|
||||
appSettings.optimizeMediaUploads = false
|
||||
@@ -36,190 +38,205 @@ class MediaUploadPreviewScreenViewModelTests: XCTestCase {
|
||||
AppSettings.resetAllSettings()
|
||||
}
|
||||
|
||||
func testImageUploadWithoutCaption() async throws {
|
||||
@Test
|
||||
func imageUploadWithoutCaption() async throws {
|
||||
setUpViewModel(urls: [imageURL], expectedCaption: nil)
|
||||
context.caption = .init("")
|
||||
try await send()
|
||||
}
|
||||
|
||||
func testImageUploadWithBlankCaption() async throws {
|
||||
@Test
|
||||
func imageUploadWithBlankCaption() async throws {
|
||||
setUpViewModel(urls: [imageURL], expectedCaption: nil)
|
||||
context.caption = .init(" ")
|
||||
try await send()
|
||||
}
|
||||
|
||||
func testImageUploadWithCaption() async throws {
|
||||
@Test
|
||||
func imageUploadWithCaption() async throws {
|
||||
let caption = "This is a really great image!"
|
||||
setUpViewModel(urls: [imageURL], expectedCaption: caption)
|
||||
context.caption = .init(string: caption)
|
||||
try await send()
|
||||
}
|
||||
|
||||
func testVideoUploadWithoutCaption() async throws {
|
||||
@Test
|
||||
func videoUploadWithoutCaption() async throws {
|
||||
setUpViewModel(urls: [videoURL], expectedCaption: nil)
|
||||
context.caption = .init("")
|
||||
try await send()
|
||||
}
|
||||
|
||||
func testVideoUploadWithCaption() async throws {
|
||||
@Test
|
||||
func videoUploadWithCaption() async throws {
|
||||
let caption = "Check out this video!"
|
||||
setUpViewModel(urls: [videoURL], expectedCaption: caption)
|
||||
context.caption = .init(string: caption)
|
||||
try await send()
|
||||
}
|
||||
|
||||
func testAudioUploadWithoutCaption() async throws {
|
||||
@Test
|
||||
func audioUploadWithoutCaption() async throws {
|
||||
setUpViewModel(urls: [audioURL], expectedCaption: nil)
|
||||
context.caption = .init("")
|
||||
try await send()
|
||||
}
|
||||
|
||||
func testAudioUploadWithCaption() async throws {
|
||||
@Test
|
||||
func audioUploadWithCaption() async throws {
|
||||
let caption = "Listen to this!"
|
||||
setUpViewModel(urls: [audioURL], expectedCaption: caption)
|
||||
context.caption = .init(string: caption)
|
||||
try await send()
|
||||
}
|
||||
|
||||
func testFileUploadWithoutCaption() async throws {
|
||||
@Test
|
||||
func fileUploadWithoutCaption() async throws {
|
||||
setUpViewModel(urls: [fileURL], expectedCaption: nil)
|
||||
context.caption = .init("")
|
||||
try await send()
|
||||
}
|
||||
|
||||
func testFileUploadWithCaption() async throws {
|
||||
@Test
|
||||
func fileUploadWithCaption() async throws {
|
||||
let caption = "Please will you check my article."
|
||||
setUpViewModel(urls: [fileURL], expectedCaption: caption)
|
||||
context.caption = .init(string: caption)
|
||||
try await send()
|
||||
}
|
||||
|
||||
func testProcessingFailure() async throws {
|
||||
@Test
|
||||
func processingFailure() async throws {
|
||||
// Given an upload screen for a non-existent file.
|
||||
setUpViewModel(urls: [badImageURL], expectedCaption: nil)
|
||||
XCTAssertFalse(context.viewState.shouldDisableInteraction)
|
||||
XCTAssertEqual(userIndicatorController.submitIndicatorDelayCallsCount, 0)
|
||||
#expect(!context.viewState.shouldDisableInteraction)
|
||||
#expect(userIndicatorController.submitIndicatorDelayCallsCount == 0)
|
||||
|
||||
// When attempting to send the file
|
||||
let deferredFailure = deferFailure(viewModel.actions, timeout: 1, message: "The screen should remain visible.") { $0 == .dismiss }
|
||||
let deferredFailure = deferFailure(viewModel.actions, timeout: .seconds(1), message: "The screen should remain visible.") { $0 == .dismiss }
|
||||
context.send(viewAction: .send)
|
||||
XCTAssertTrue(context.viewState.shouldDisableInteraction, "The interaction should be disabled while sending.")
|
||||
XCTAssertEqual(userIndicatorController.submitIndicatorDelayCallsCount, 1) // Loading indicator
|
||||
#expect(context.viewState.shouldDisableInteraction, "The interaction should be disabled while sending.")
|
||||
#expect(userIndicatorController.submitIndicatorDelayCallsCount == 1) // Loading indicator
|
||||
|
||||
// Then the failure should occur preventing the screen from being dismissed.
|
||||
try await deferredFailure.fulfill()
|
||||
XCTAssertFalse(context.viewState.shouldDisableInteraction)
|
||||
XCTAssertEqual(userIndicatorController.submitIndicatorDelayCallsCount, 2, "An error indicator should be shown.")
|
||||
#expect(!context.viewState.shouldDisableInteraction)
|
||||
#expect(userIndicatorController.submitIndicatorDelayCallsCount == 2, "An error indicator should be shown.")
|
||||
}
|
||||
|
||||
func testUploadWithUnknownMaxUploadSize() async throws {
|
||||
@Test
|
||||
func uploadWithUnknownMaxUploadSize() async throws {
|
||||
// Given an upload screen that is unable to fetch the max upload size.
|
||||
setUpViewModel(urls: [imageURL], expectedCaption: nil, maxUploadSizeResult: .failure(.sdkError(ClientProxyMockError.generic)))
|
||||
XCTAssertFalse(context.viewState.shouldDisableInteraction)
|
||||
XCTAssertNil(context.alertInfo)
|
||||
#expect(!context.viewState.shouldDisableInteraction)
|
||||
#expect(context.alertInfo == nil)
|
||||
|
||||
// When attempting to send the media.
|
||||
let deferredAlert = deferFulfillment(context.observe(\.viewState.bindings.alertInfo)) { $0 != nil }
|
||||
let deferredFailure = deferFailure(viewModel.actions, timeout: 1, message: "The screen should remain visible.") { $0 == .dismiss }
|
||||
let deferredFailure = deferFailure(viewModel.actions, timeout: .seconds(1), message: "The screen should remain visible.") { $0 == .dismiss }
|
||||
context.send(viewAction: .send)
|
||||
|
||||
XCTAssertTrue(context.viewState.shouldDisableInteraction, "The interaction should be disabled while sending.")
|
||||
#expect(context.viewState.shouldDisableInteraction, "The interaction should be disabled while sending.")
|
||||
|
||||
// Then alert should be shown to tell the user it failed.
|
||||
try await deferredAlert.fulfill()
|
||||
try await deferredFailure.fulfill()
|
||||
|
||||
XCTAssertFalse(context.viewState.shouldDisableInteraction)
|
||||
XCTAssertEqual(context.alertInfo?.id, .maxUploadSizeUnknown)
|
||||
#expect(!context.viewState.shouldDisableInteraction)
|
||||
#expect(context.alertInfo?.id == .maxUploadSizeUnknown)
|
||||
|
||||
// When trying with the max upload size now available.
|
||||
let deferredDismiss = deferFulfillment(viewModel.actions) { $0 == .dismiss }
|
||||
clientProxy.underlyingMaxMediaUploadSize = .success(100 * 1024 * 1024)
|
||||
context.alertInfo?.primaryButton.action?()
|
||||
|
||||
XCTAssertTrue(context.viewState.shouldDisableInteraction, "The interaction should be disabled while retrying.")
|
||||
#expect(context.viewState.shouldDisableInteraction, "The interaction should be disabled while retrying.")
|
||||
|
||||
// Then the file should upload successfully.
|
||||
try await deferredDismiss.fulfill()
|
||||
}
|
||||
|
||||
func testUploadExceedingMaxUploadSize() async throws {
|
||||
@Test
|
||||
func uploadExceedingMaxUploadSize() async throws {
|
||||
// Given an upload screen with a really small max upload size.
|
||||
setUpViewModel(urls: [imageURL], expectedCaption: nil, maxUploadSizeResult: .success(100))
|
||||
XCTAssertFalse(context.viewState.shouldDisableInteraction)
|
||||
XCTAssertNil(context.alertInfo)
|
||||
#expect(!context.viewState.shouldDisableInteraction)
|
||||
#expect(context.alertInfo == nil)
|
||||
|
||||
// When attempting to send an image that is larger the limit.
|
||||
let deferredAlert = deferFulfillment(context.observe(\.viewState.bindings.alertInfo)) { $0 != nil }
|
||||
let deferredFailure = deferFailure(viewModel.actions, timeout: 1, message: "The screen should remain visible.") { $0 == .dismiss }
|
||||
let deferredFailure = deferFailure(viewModel.actions, timeout: .seconds(1), message: "The screen should remain visible.") { $0 == .dismiss }
|
||||
context.send(viewAction: .send)
|
||||
|
||||
XCTAssertTrue(context.viewState.shouldDisableInteraction, "The interaction should be disabled while sending.")
|
||||
#expect(context.viewState.shouldDisableInteraction, "The interaction should be disabled while sending.")
|
||||
|
||||
// Then an alert should be shown to inform the user of the max upload size.
|
||||
try await deferredAlert.fulfill()
|
||||
try await deferredFailure.fulfill()
|
||||
|
||||
XCTAssertFalse(context.viewState.shouldDisableInteraction)
|
||||
XCTAssertEqual(context.alertInfo?.id, .maxUploadSizeExceeded(limit: 100))
|
||||
#expect(!context.viewState.shouldDisableInteraction)
|
||||
#expect(context.alertInfo?.id == .maxUploadSizeExceeded(limit: 100))
|
||||
}
|
||||
|
||||
func testMultipleFiles() async throws {
|
||||
@Test
|
||||
func multipleFiles() async throws {
|
||||
// Given an upload screen with multiple media files.
|
||||
setUpViewModel(urls: [fileURL, imageURL, fileURL], expectedCaption: nil)
|
||||
XCTAssertFalse(context.viewState.shouldDisableInteraction)
|
||||
XCTAssertEqual(userIndicatorController.submitIndicatorDelayCallsCount, 0)
|
||||
#expect(!context.viewState.shouldDisableInteraction)
|
||||
#expect(userIndicatorController.submitIndicatorDelayCallsCount == 0)
|
||||
|
||||
// When attempting to send the files.
|
||||
let deferredDismiss = deferFulfillment(viewModel.actions) { $0 == .dismiss }
|
||||
context.send(viewAction: .send)
|
||||
|
||||
XCTAssertTrue(context.viewState.shouldDisableInteraction, "The interaction should be disabled while sending.")
|
||||
XCTAssertEqual(userIndicatorController.submitIndicatorDelayCallsCount, 1) // Loading indicator
|
||||
#expect(context.viewState.shouldDisableInteraction, "The interaction should be disabled while sending.")
|
||||
#expect(userIndicatorController.submitIndicatorDelayCallsCount == 1) // Loading indicator
|
||||
|
||||
// Then the screen should be dismissed once all of the files have been sent.
|
||||
try await deferredDismiss.fulfill()
|
||||
XCTAssertEqual(timelineProxy.sendImageUrlThumbnailURLImageInfoCaptionRequestHandleCallsCount, 1)
|
||||
XCTAssertEqual(timelineProxy.sendFileUrlFileInfoCaptionRequestHandleCallsCount, 2)
|
||||
XCTAssertEqual(userIndicatorController.submitIndicatorDelayCallsCount, 1, "Only a loading indicator should be shown.")
|
||||
#expect(timelineProxy.sendImageUrlThumbnailURLImageInfoCaptionRequestHandleCallsCount == 1)
|
||||
#expect(timelineProxy.sendFileUrlFileInfoCaptionRequestHandleCallsCount == 2)
|
||||
#expect(userIndicatorController.submitIndicatorDelayCallsCount == 1, "Only a loading indicator should be shown.")
|
||||
}
|
||||
|
||||
func testMultipleFilesWithProcessingFailure() async throws {
|
||||
@Test
|
||||
func multipleFilesWithProcessingFailure() async throws {
|
||||
// Given an upload screen for a non-existent file.
|
||||
setUpViewModel(urls: [imageURL, fileURL, badImageURL], expectedCaption: nil)
|
||||
XCTAssertFalse(context.viewState.shouldDisableInteraction)
|
||||
XCTAssertEqual(userIndicatorController.submitIndicatorDelayCallsCount, 0)
|
||||
#expect(!context.viewState.shouldDisableInteraction)
|
||||
#expect(userIndicatorController.submitIndicatorDelayCallsCount == 0)
|
||||
|
||||
// When attempting to send the file
|
||||
let deferredFailure = deferFailure(viewModel.actions, timeout: 1, message: "The screen should remain visible.") { $0 == .dismiss }
|
||||
let deferredFailure = deferFailure(viewModel.actions, timeout: .seconds(1), message: "The screen should remain visible.") { $0 == .dismiss }
|
||||
context.send(viewAction: .send)
|
||||
XCTAssertTrue(context.viewState.shouldDisableInteraction, "The interaction should be disabled while sending.")
|
||||
XCTAssertEqual(userIndicatorController.submitIndicatorDelayCallsCount, 1) // Loading indicator
|
||||
#expect(context.viewState.shouldDisableInteraction, "The interaction should be disabled while sending.")
|
||||
#expect(userIndicatorController.submitIndicatorDelayCallsCount == 1) // Loading indicator
|
||||
|
||||
// Then the failure should occur preventing the screen from being dismissed.
|
||||
try await deferredFailure.fulfill()
|
||||
XCTAssertFalse(context.viewState.shouldDisableInteraction)
|
||||
XCTAssertEqual(userIndicatorController.submitIndicatorDelayCallsCount, 2, "An error indicator should be shown.")
|
||||
#expect(!context.viewState.shouldDisableInteraction)
|
||||
#expect(userIndicatorController.submitIndicatorDelayCallsCount == 2, "An error indicator should be shown.")
|
||||
}
|
||||
|
||||
func testMultipleFilesWithSendFailure() async throws {
|
||||
@Test
|
||||
func multipleFilesWithSendFailure() async throws {
|
||||
// Given an upload screen with multiple media files where one of the files will fail to send.
|
||||
setUpViewModel(urls: [fileURL, imageURL, imageURL, fileURL], expectedCaption: nil, simulateImageSendFailures: true)
|
||||
XCTAssertFalse(context.viewState.shouldDisableInteraction)
|
||||
XCTAssertEqual(userIndicatorController.submitIndicatorDelayCallsCount, 0)
|
||||
#expect(!context.viewState.shouldDisableInteraction)
|
||||
#expect(userIndicatorController.submitIndicatorDelayCallsCount == 0)
|
||||
|
||||
// When attempting to send the files.
|
||||
let deferredDismiss = deferFulfillment(viewModel.actions) { $0 == .dismiss }
|
||||
context.send(viewAction: .send)
|
||||
|
||||
XCTAssertTrue(context.viewState.shouldDisableInteraction, "The interaction should be disabled while sending.")
|
||||
XCTAssertEqual(userIndicatorController.submitIndicatorDelayCallsCount, 1) // Loading indicator
|
||||
#expect(context.viewState.shouldDisableInteraction, "The interaction should be disabled while sending.")
|
||||
#expect(userIndicatorController.submitIndicatorDelayCallsCount == 1) // Loading indicator
|
||||
|
||||
// Then the screen should be dismissed so the user can see which files made it into the timeline.
|
||||
try await deferredDismiss.fulfill()
|
||||
XCTAssertEqual(timelineProxy.sendImageUrlThumbnailURLImageInfoCaptionRequestHandleCallsCount, 2)
|
||||
XCTAssertEqual(timelineProxy.sendFileUrlFileInfoCaptionRequestHandleCallsCount, 2)
|
||||
XCTAssertEqual(userIndicatorController.submitIndicatorDelayCallsCount, 3, "Error indicators for each failure should be shown.")
|
||||
#expect(timelineProxy.sendImageUrlThumbnailURLImageInfoCaptionRequestHandleCallsCount == 2)
|
||||
#expect(timelineProxy.sendFileUrlFileInfoCaptionRequestHandleCallsCount == 2)
|
||||
#expect(userIndicatorController.submitIndicatorDelayCallsCount == 3, "Error indicators for each failure should be shown.")
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
@@ -244,7 +261,7 @@ class MediaUploadPreviewScreenViewModelTests: XCTestCase {
|
||||
|
||||
private func assertResourceURL(filename: String) -> URL {
|
||||
guard let url = Bundle(for: Self.self).url(forResource: filename, withExtension: nil) else {
|
||||
XCTFail("Failed retrieving test asset")
|
||||
Issue.record("Failed retrieving test asset")
|
||||
return .picturesDirectory
|
||||
}
|
||||
return url
|
||||
@@ -288,19 +305,19 @@ class MediaUploadPreviewScreenViewModelTests: XCTestCase {
|
||||
|
||||
private func verifyCaption(_ caption: String?, expectedCaption: String?) -> Result<Void, TimelineProxyError> {
|
||||
guard caption == expectedCaption else {
|
||||
XCTFail("The sent caption '\(caption ?? "nil")' does not match the expected value '\(expectedCaption ?? "nil")'").self
|
||||
Issue.record("The sent caption '\(caption ?? "nil")' does not match the expected value '\(expectedCaption ?? "nil")'")
|
||||
return .failure(.sdkError(TestError.unexpectedParameter))
|
||||
}
|
||||
return .success(())
|
||||
}
|
||||
|
||||
private func send() async throws {
|
||||
XCTAssertFalse(context.viewState.shouldDisableInteraction, "Attempting to send when interaction is disabled.")
|
||||
#expect(!context.viewState.shouldDisableInteraction, "Attempting to send when interaction is disabled.")
|
||||
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0 == .dismiss }
|
||||
context.send(viewAction: .send)
|
||||
|
||||
XCTAssertTrue(context.viewState.shouldDisableInteraction, "The interaction should be disabled while sending.")
|
||||
#expect(context.viewState.shouldDisableInteraction, "The interaction should be disabled while sending.")
|
||||
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
@@ -7,15 +7,17 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import SwiftUI
|
||||
import Testing
|
||||
import UniformTypeIdentifiers
|
||||
import XCTest
|
||||
|
||||
final class MediaUploadingPreprocessorTests: XCTestCase {
|
||||
@Suite
|
||||
final class MediaUploadingPreprocessorTests {
|
||||
let maxUploadSize: UInt = 100 * 1024 * 1024
|
||||
var appSettings: AppSettings!
|
||||
var mediaUploadingPreprocessor: MediaUploadingPreprocessor!
|
||||
|
||||
override func setUp() {
|
||||
init() {
|
||||
AppSettings.resetAllSettings()
|
||||
appSettings = AppSettings()
|
||||
appSettings.optimizeMediaUploads = false
|
||||
@@ -23,450 +25,426 @@ final class MediaUploadingPreprocessorTests: XCTestCase {
|
||||
mediaUploadingPreprocessor = MediaUploadingPreprocessor(appSettings: appSettings)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
deinit {
|
||||
AppSettings.resetAllSettings()
|
||||
}
|
||||
|
||||
func testAudioFileProcessing() async {
|
||||
guard let url = Bundle(for: Self.self).url(forResource: "test_audio.mp3", withExtension: nil) else {
|
||||
XCTFail("Failed retrieving test asset")
|
||||
return
|
||||
}
|
||||
@Test
|
||||
func audioFileProcessing() async throws {
|
||||
let url = try #require(Bundle(for: Self.self).url(forResource: "test_audio.mp3", withExtension: nil), "Failed retrieving test asset")
|
||||
|
||||
guard case let .success(result) = await mediaUploadingPreprocessor.processMedia(at: url, maxUploadSize: maxUploadSize),
|
||||
case let .audio(audioURL, audioInfo) = result else {
|
||||
XCTFail("Failed processing asset")
|
||||
Issue.record("Failed processing asset")
|
||||
return
|
||||
}
|
||||
|
||||
// Check that the file name is preserved
|
||||
XCTAssertEqual(audioURL.lastPathComponent, "test_audio.mp3")
|
||||
#expect(audioURL.lastPathComponent == "test_audio.mp3")
|
||||
|
||||
XCTAssertEqual(audioInfo.mimetype, "audio/mpeg")
|
||||
XCTAssertEqual(audioInfo.duration ?? 0, 27, accuracy: 100)
|
||||
XCTAssertEqual(audioInfo.size ?? 0, 194_811, accuracy: 100)
|
||||
#expect(audioInfo.mimetype == "audio/mpeg")
|
||||
#expect(isEqual(audioInfo.duration ?? 0, 27, within: 100))
|
||||
#expect(isEqual(audioInfo.size ?? 0, 194_811, within: 100))
|
||||
}
|
||||
|
||||
func testLandscapeMovVideoProcessing() async {
|
||||
// Allow an increased execution time as we encode the video twice now.
|
||||
executionTimeAllowance = 180
|
||||
|
||||
guard let url = Bundle(for: Self.self).url(forResource: "landscape_test_video.mov", withExtension: nil) else {
|
||||
XCTFail("Failed retrieving test asset")
|
||||
return
|
||||
}
|
||||
@Test
|
||||
func landscapeMovVideoProcessing() async throws {
|
||||
let url = try #require(Bundle(for: Self.self).url(forResource: "landscape_test_video.mov", withExtension: nil), "Failed retrieving test asset")
|
||||
|
||||
guard case let .success(result) = await mediaUploadingPreprocessor.processMedia(at: url, maxUploadSize: maxUploadSize),
|
||||
case let .video(videoURL, thumbnailURL, videoInfo) = result else {
|
||||
XCTFail("Failed processing asset")
|
||||
Issue.record("Failed processing asset")
|
||||
return
|
||||
}
|
||||
|
||||
// Check that the file name is preserved
|
||||
XCTAssertEqual(videoURL.lastPathComponent, "landscape_test_video.mp4")
|
||||
XCTAssertEqual(videoURL.pathExtension, "mp4", "The file extension should match the container we use.")
|
||||
#expect(videoURL.lastPathComponent == "landscape_test_video.mp4")
|
||||
#expect(videoURL.pathExtension == "mp4", "The file extension should match the container we use.")
|
||||
|
||||
// Check that the thumbnail is generated correctly
|
||||
guard let thumbnailData = try? Data(contentsOf: thumbnailURL),
|
||||
let thumbnail = UIImage(data: thumbnailData) else {
|
||||
XCTFail("Invalid thumbnail")
|
||||
return
|
||||
}
|
||||
let thumbnailData = try Data(contentsOf: thumbnailURL)
|
||||
let thumbnail = try #require(UIImage(data: thumbnailData), "Invalid thumbnail")
|
||||
|
||||
XCTAssert(thumbnail.size.width <= MediaUploadingPreprocessor.Constants.maximumThumbnailSize.width)
|
||||
XCTAssert(thumbnail.size.height <= MediaUploadingPreprocessor.Constants.maximumThumbnailSize.height)
|
||||
#expect(thumbnail.size.width <= MediaUploadingPreprocessor.Constants.maximumThumbnailSize.width)
|
||||
#expect(thumbnail.size.height <= MediaUploadingPreprocessor.Constants.maximumThumbnailSize.height)
|
||||
|
||||
// Check resulting video info
|
||||
XCTAssertEqual(videoInfo.mimetype, "video/mp4")
|
||||
XCTAssertEqual(videoInfo.blurhash, "K9F$LJZ9,+8yA9-:yT,@%1")
|
||||
XCTAssertEqual(videoInfo.size ?? 0, 4_016_620, accuracy: 100)
|
||||
XCTAssertEqual(videoInfo.width, 1280)
|
||||
XCTAssertEqual(videoInfo.height, 720)
|
||||
XCTAssertEqual(videoInfo.duration ?? 0, 30, accuracy: 100)
|
||||
#expect(videoInfo.mimetype == "video/mp4")
|
||||
#expect(videoInfo.blurhash == "K9F$LJZ9,+8yA9-:yT,@%1")
|
||||
#expect(isEqual(videoInfo.size ?? 0, 4_016_620, within: 100))
|
||||
#expect(videoInfo.width == 1280)
|
||||
#expect(videoInfo.height == 720)
|
||||
#expect(isEqual(videoInfo.duration ?? 0, 30, within: 100))
|
||||
|
||||
XCTAssertNotNil(videoInfo.thumbnailInfo)
|
||||
XCTAssertEqual(videoInfo.thumbnailInfo?.mimetype, "image/jpeg")
|
||||
XCTAssertEqual(videoInfo.thumbnailInfo?.size ?? 0, 183_093, accuracy: 100)
|
||||
XCTAssertEqual(videoInfo.thumbnailInfo?.width, 800)
|
||||
XCTAssertEqual(videoInfo.thumbnailInfo?.height, 450)
|
||||
#expect(videoInfo.thumbnailInfo != nil)
|
||||
#expect(videoInfo.thumbnailInfo?.mimetype == "image/jpeg")
|
||||
#expect(isEqual(videoInfo.thumbnailInfo?.size ?? 0, 183_093, within: 100))
|
||||
#expect(videoInfo.thumbnailInfo?.width == 800)
|
||||
#expect(videoInfo.thumbnailInfo?.height == 450)
|
||||
|
||||
// Repeat with optimised media setting
|
||||
appSettings.optimizeMediaUploads = true
|
||||
|
||||
guard case let .success(optimizedResult) = await mediaUploadingPreprocessor.processMedia(at: url, maxUploadSize: maxUploadSize),
|
||||
case let .video(optimizedVideoURL, _, optimizedVideoInfo) = optimizedResult else {
|
||||
XCTFail("Failed processing asset")
|
||||
Issue.record("Failed processing asset")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(optimizedVideoURL.pathExtension, "mp4", "The file extension should match the container we use.")
|
||||
#expect(optimizedVideoURL.pathExtension == "mp4", "The file extension should match the container we use.")
|
||||
|
||||
// Check optimised video info
|
||||
XCTAssertEqual(optimizedVideoInfo.mimetype, "video/mp4")
|
||||
XCTAssertEqual(optimizedVideoInfo.blurhash, "K9F$LJZ9,+8yA9-:yT,@%1")
|
||||
XCTAssertEqual(optimizedVideoInfo.size ?? 0, 4_016_620, accuracy: 100) // Note: The video is already 720p so it doesn't change size.
|
||||
XCTAssertEqual(optimizedVideoInfo.width, 1280)
|
||||
XCTAssertEqual(optimizedVideoInfo.height, 720)
|
||||
XCTAssertEqual(optimizedVideoInfo.duration ?? 0, 30, accuracy: 100)
|
||||
#expect(optimizedVideoInfo.mimetype == "video/mp4")
|
||||
#expect(optimizedVideoInfo.blurhash == "K9F$LJZ9,+8yA9-:yT,@%1")
|
||||
#expect(isEqual(optimizedVideoInfo.size ?? 0, 4_016_620, within: 100)) // Note: The video is already 720p so it doesn't change size.
|
||||
#expect(optimizedVideoInfo.width == 1280)
|
||||
#expect(optimizedVideoInfo.height == 720)
|
||||
#expect(isEqual(optimizedVideoInfo.duration ?? 0, 30, within: 100))
|
||||
}
|
||||
|
||||
func testPortraitMp4VideoProcessing() async {
|
||||
// Allow an increased execution time as we encode the video twice now.
|
||||
executionTimeAllowance = 180
|
||||
|
||||
guard let url = Bundle(for: Self.self).url(forResource: "portrait_test_video.mp4", withExtension: nil) else {
|
||||
XCTFail("Failed retrieving test asset")
|
||||
return
|
||||
}
|
||||
@Test
|
||||
func portraitMp4VideoProcessing() async throws {
|
||||
let url = try #require(Bundle(for: Self.self).url(forResource: "portrait_test_video.mp4", withExtension: nil), "Failed retrieving test asset")
|
||||
|
||||
guard case let .success(result) = await mediaUploadingPreprocessor.processMedia(at: url, maxUploadSize: maxUploadSize),
|
||||
case let .video(videoURL, thumbnailURL, videoInfo) = result else {
|
||||
XCTFail("Failed processing asset")
|
||||
Issue.record("Failed processing asset")
|
||||
return
|
||||
}
|
||||
|
||||
// Check that the file name is preserved
|
||||
XCTAssertEqual(videoURL.lastPathComponent, "portrait_test_video.mp4")
|
||||
XCTAssertEqual(videoURL.pathExtension, "mp4", "The file extension should match the container we use.")
|
||||
#expect(videoURL.lastPathComponent == "portrait_test_video.mp4")
|
||||
#expect(videoURL.pathExtension == "mp4", "The file extension should match the container we use.")
|
||||
|
||||
// Check that the thumbnail is generated correctly
|
||||
guard let thumbnailData = try? Data(contentsOf: thumbnailURL),
|
||||
let thumbnail = UIImage(data: thumbnailData) else {
|
||||
XCTFail("Invalid thumbnail")
|
||||
return
|
||||
}
|
||||
let thumbnailData = try Data(contentsOf: thumbnailURL)
|
||||
let thumbnail = try #require(UIImage(data: thumbnailData), "Invalid thumbnail")
|
||||
|
||||
XCTAssert(thumbnail.size.width <= MediaUploadingPreprocessor.Constants.maximumThumbnailSize.width)
|
||||
XCTAssert(thumbnail.size.height <= MediaUploadingPreprocessor.Constants.maximumThumbnailSize.height)
|
||||
#expect(thumbnail.size.width <= MediaUploadingPreprocessor.Constants.maximumThumbnailSize.width)
|
||||
#expect(thumbnail.size.height <= MediaUploadingPreprocessor.Constants.maximumThumbnailSize.height)
|
||||
|
||||
// Check resulting video info
|
||||
XCTAssertEqual(videoInfo.mimetype, "video/mp4")
|
||||
XCTAssertEqual(videoInfo.blurhash, "KSB{R8O]MuwQS4oJvcaIt8")
|
||||
XCTAssertEqual(videoInfo.size ?? 0, 5_824_946, accuracy: 100)
|
||||
XCTAssertEqual(videoInfo.width, 1080)
|
||||
XCTAssertEqual(videoInfo.height, 1920)
|
||||
XCTAssertEqual(videoInfo.duration ?? 0, 21, accuracy: 100)
|
||||
#expect(videoInfo.mimetype == "video/mp4")
|
||||
#expect(videoInfo.blurhash == "KSB{R8O]MuwQS4oJvcaIt8")
|
||||
#expect(isEqual(videoInfo.size ?? 0, 5_824_946, within: 100))
|
||||
#expect(videoInfo.width == 1080)
|
||||
#expect(videoInfo.height == 1920)
|
||||
#expect(isEqual(videoInfo.duration ?? 0, 21, within: 100))
|
||||
|
||||
XCTAssertNotNil(videoInfo.thumbnailInfo)
|
||||
XCTAssertEqual(videoInfo.thumbnailInfo?.mimetype, "image/jpeg")
|
||||
XCTAssertEqual(videoInfo.thumbnailInfo?.size ?? 0, 40976, accuracy: 100)
|
||||
XCTAssertEqual(videoInfo.thumbnailInfo?.width, 337)
|
||||
XCTAssertEqual(videoInfo.thumbnailInfo?.height, 600)
|
||||
#expect(videoInfo.thumbnailInfo != nil)
|
||||
#expect(videoInfo.thumbnailInfo?.mimetype == "image/jpeg")
|
||||
#expect(isEqual(videoInfo.thumbnailInfo?.size ?? 0, 40976, within: 100))
|
||||
#expect(videoInfo.thumbnailInfo?.width == 337)
|
||||
#expect(videoInfo.thumbnailInfo?.height == 600)
|
||||
|
||||
// Repeat with optimised media setting
|
||||
appSettings.optimizeMediaUploads = true
|
||||
|
||||
guard case let .success(optimizedResult) = await mediaUploadingPreprocessor.processMedia(at: url, maxUploadSize: maxUploadSize),
|
||||
case let .video(optimizedVideoURL, _, optimizedVideoInfo) = optimizedResult else {
|
||||
XCTFail("Failed processing asset")
|
||||
Issue.record("Failed processing asset")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(optimizedVideoURL.pathExtension, "mp4", "The file extension should match the container we use.")
|
||||
#expect(optimizedVideoURL.pathExtension == "mp4", "The file extension should match the container we use.")
|
||||
|
||||
// Check optimised video info
|
||||
XCTAssertEqual(optimizedVideoInfo.mimetype, "video/mp4")
|
||||
XCTAssertEqual(optimizedVideoInfo.blurhash, "KSC5.vO]MuwQS4oJvcaIt8")
|
||||
XCTAssertEqual(optimizedVideoInfo.size ?? 0, 12_169_117, accuracy: 100) // Note: This is slightly stupid because it is larger now 🤦♂️
|
||||
XCTAssertEqual(optimizedVideoInfo.width, 720)
|
||||
XCTAssertEqual(optimizedVideoInfo.height, 1280)
|
||||
XCTAssertEqual(optimizedVideoInfo.duration ?? 0, 30, accuracy: 100)
|
||||
#expect(optimizedVideoInfo.mimetype == "video/mp4")
|
||||
#expect(optimizedVideoInfo.blurhash == "KSC5.vO]MuwQS4oJvcaIt8")
|
||||
#expect(isEqual(optimizedVideoInfo.size ?? 0, 12_169_117, within: 100)) // Note: This is slightly stupid because it is larger now 🤦♂️
|
||||
#expect(optimizedVideoInfo.width == 720)
|
||||
#expect(optimizedVideoInfo.height == 1280)
|
||||
#expect(isEqual(optimizedVideoInfo.duration ?? 0, 30, within: 100))
|
||||
}
|
||||
|
||||
func testLandscapeImageProcessing() async {
|
||||
guard let url = Bundle(for: Self.self).url(forResource: "landscape_test_image.jpg", withExtension: nil) else {
|
||||
XCTFail("Failed retrieving test asset")
|
||||
return
|
||||
}
|
||||
@Test
|
||||
func landscapeImageProcessing() async throws {
|
||||
let url = try #require(Bundle(for: Self.self).url(forResource: "landscape_test_image.jpg", withExtension: nil), "Failed retrieving test asset")
|
||||
|
||||
guard case let .success(result) = await mediaUploadingPreprocessor.processMedia(at: url, maxUploadSize: maxUploadSize),
|
||||
case let .image(convertedImageURL, thumbnailURL, imageInfo) = result else {
|
||||
XCTFail("Failed processing asset")
|
||||
Issue.record("Failed processing asset")
|
||||
return
|
||||
}
|
||||
|
||||
compare(originalImageAt: url, toConvertedImageAt: convertedImageURL, withThumbnailAt: thumbnailURL)
|
||||
try compare(originalImageAt: url, toConvertedImageAt: convertedImageURL, withThumbnailAt: thumbnailURL)
|
||||
|
||||
// Check resulting image info
|
||||
XCTAssertEqual(imageInfo.mimetype, "image/jpeg")
|
||||
XCTAssertEqual(imageInfo.blurhash, "K%I#.NofkC_4ayayxujsWB")
|
||||
XCTAssertEqual(imageInfo.size ?? 0, 3_305_795, accuracy: 100)
|
||||
XCTAssertEqual(imageInfo.width, 6103)
|
||||
XCTAssertEqual(imageInfo.height, 2621)
|
||||
#expect(imageInfo.mimetype == "image/jpeg")
|
||||
#expect(imageInfo.blurhash == "K%I#.NofkC_4ayayxujsWB")
|
||||
#expect(isEqual(imageInfo.size ?? 0, 3_305_795, within: 100))
|
||||
#expect(imageInfo.width == 6103)
|
||||
#expect(imageInfo.height == 2621)
|
||||
|
||||
XCTAssertNotNil(imageInfo.thumbnailInfo)
|
||||
XCTAssertEqual(imageInfo.thumbnailInfo?.mimetype, "image/jpeg")
|
||||
XCTAssertEqual(imageInfo.thumbnailInfo?.size ?? 0, 87733, accuracy: 100)
|
||||
XCTAssertEqual(imageInfo.thumbnailInfo?.width, 800)
|
||||
XCTAssertEqual(imageInfo.thumbnailInfo?.height, 344)
|
||||
#expect(imageInfo.thumbnailInfo != nil)
|
||||
#expect(imageInfo.thumbnailInfo?.mimetype == "image/jpeg")
|
||||
#expect(isEqual(imageInfo.thumbnailInfo?.size ?? 0, 87733, within: 100))
|
||||
#expect(imageInfo.thumbnailInfo?.width == 800)
|
||||
#expect(imageInfo.thumbnailInfo?.height == 344)
|
||||
|
||||
// Repeat with optimised media setting
|
||||
appSettings.optimizeMediaUploads = true
|
||||
|
||||
guard case let .success(optimizedResult) = await mediaUploadingPreprocessor.processMedia(at: url, maxUploadSize: maxUploadSize),
|
||||
case let .image(optimizedImageURL, thumbnailURL, optimizedImageInfo) = optimizedResult else {
|
||||
XCTFail("Failed processing asset")
|
||||
Issue.record("Failed processing asset")
|
||||
return
|
||||
}
|
||||
|
||||
compare(originalImageAt: url, toConvertedImageAt: optimizedImageURL, withThumbnailAt: thumbnailURL)
|
||||
try compare(originalImageAt: url, toConvertedImageAt: optimizedImageURL, withThumbnailAt: thumbnailURL)
|
||||
|
||||
// Check optimised image info
|
||||
XCTAssertEqual(optimizedImageInfo.mimetype, "image/jpeg")
|
||||
XCTAssertEqual(optimizedImageInfo.blurhash, "K%I#.NofkC_4ayaxxujsWB")
|
||||
XCTAssertEqual(optimizedImageInfo.size ?? 0, 524_226, accuracy: 100)
|
||||
XCTAssertEqual(optimizedImageInfo.width, 2048)
|
||||
XCTAssertEqual(optimizedImageInfo.height, 879)
|
||||
#expect(optimizedImageInfo.mimetype == "image/jpeg")
|
||||
#expect(optimizedImageInfo.blurhash == "K%I#.NofkC_4ayaxxujsWB")
|
||||
#expect(isEqual(optimizedImageInfo.size ?? 0, 524_226, within: 100))
|
||||
#expect(optimizedImageInfo.width == 2048)
|
||||
#expect(optimizedImageInfo.height == 879)
|
||||
}
|
||||
|
||||
func testPortraitImageProcessing() async {
|
||||
guard let url = Bundle(for: Self.self).url(forResource: "portrait_test_image.jpg", withExtension: nil) else {
|
||||
XCTFail("Failed retrieving test asset")
|
||||
return
|
||||
}
|
||||
@Test
|
||||
func portraitImageProcessing() async throws {
|
||||
let url = try #require(Bundle(for: Self.self).url(forResource: "portrait_test_image.jpg", withExtension: nil), "Failed retrieving test asset")
|
||||
|
||||
guard case let .success(result) = await mediaUploadingPreprocessor.processMedia(at: url, maxUploadSize: maxUploadSize),
|
||||
case let .image(convertedImageURL, thumbnailURL, imageInfo) = result else {
|
||||
XCTFail("Failed processing asset")
|
||||
Issue.record("Failed processing asset")
|
||||
return
|
||||
}
|
||||
|
||||
compare(originalImageAt: url, toConvertedImageAt: convertedImageURL, withThumbnailAt: thumbnailURL)
|
||||
try compare(originalImageAt: url, toConvertedImageAt: convertedImageURL, withThumbnailAt: thumbnailURL)
|
||||
|
||||
// Check resulting image info
|
||||
XCTAssertEqual(imageInfo.mimetype, "image/jpeg")
|
||||
XCTAssertEqual(imageInfo.blurhash, "KdE|0Ls+RP^-n*RP%OWAV@")
|
||||
XCTAssertEqual(imageInfo.size ?? 0, 4_414_666, accuracy: 100)
|
||||
XCTAssertEqual(imageInfo.width, 3024)
|
||||
XCTAssertEqual(imageInfo.height, 4032)
|
||||
#expect(imageInfo.mimetype == "image/jpeg")
|
||||
#expect(imageInfo.blurhash == "KdE|0Ls+RP^-n*RP%OWAV@")
|
||||
#expect(isEqual(imageInfo.size ?? 0, 4_414_666, within: 100))
|
||||
#expect(imageInfo.width == 3024)
|
||||
#expect(imageInfo.height == 4032)
|
||||
|
||||
XCTAssertNotNil(imageInfo.thumbnailInfo)
|
||||
XCTAssertEqual(imageInfo.thumbnailInfo?.mimetype, "image/jpeg")
|
||||
XCTAssertEqual(imageInfo.thumbnailInfo?.size ?? 0, 258_914, accuracy: 100)
|
||||
XCTAssertEqual(imageInfo.thumbnailInfo?.width, 600)
|
||||
XCTAssertEqual(imageInfo.thumbnailInfo?.height, 800)
|
||||
#expect(imageInfo.thumbnailInfo != nil)
|
||||
#expect(imageInfo.thumbnailInfo?.mimetype == "image/jpeg")
|
||||
#expect(isEqual(imageInfo.thumbnailInfo?.size ?? 0, 258_914, within: 100))
|
||||
#expect(imageInfo.thumbnailInfo?.width == 600)
|
||||
#expect(imageInfo.thumbnailInfo?.height == 800)
|
||||
|
||||
// Repeat with optimised media setting
|
||||
appSettings.optimizeMediaUploads = true
|
||||
|
||||
guard case let .success(optimizedResult) = await mediaUploadingPreprocessor.processMedia(at: url, maxUploadSize: maxUploadSize),
|
||||
case let .image(optimizedImageURL, thumbnailURL, optimizedImageInfo) = optimizedResult else {
|
||||
XCTFail("Failed processing asset")
|
||||
Issue.record("Failed processing asset")
|
||||
return
|
||||
}
|
||||
|
||||
compare(originalImageAt: url, toConvertedImageAt: optimizedImageURL, withThumbnailAt: thumbnailURL)
|
||||
try compare(originalImageAt: url, toConvertedImageAt: optimizedImageURL, withThumbnailAt: thumbnailURL)
|
||||
|
||||
// Check optimised image info
|
||||
XCTAssertEqual(optimizedImageInfo.mimetype, "image/jpeg")
|
||||
XCTAssertEqual(optimizedImageInfo.blurhash, "KdE|0Ls+RP^-n*RP%OWAV@")
|
||||
XCTAssertEqual(optimizedImageInfo.size ?? 0, 1_462_937, accuracy: 100)
|
||||
XCTAssertEqual(optimizedImageInfo.width, 1536)
|
||||
XCTAssertEqual(optimizedImageInfo.height, 2048)
|
||||
#expect(optimizedImageInfo.mimetype == "image/jpeg")
|
||||
#expect(optimizedImageInfo.blurhash == "KdE|0Ls+RP^-n*RP%OWAV@")
|
||||
#expect(isEqual(optimizedImageInfo.size ?? 0, 1_462_937, within: 100))
|
||||
#expect(optimizedImageInfo.width == 1536)
|
||||
#expect(optimizedImageInfo.height == 2048)
|
||||
}
|
||||
|
||||
func testPNGImageProcessing() async {
|
||||
guard let url = Bundle(for: Self.self).url(forResource: "test_image.png", withExtension: nil) else {
|
||||
XCTFail("Failed retrieving test asset")
|
||||
return
|
||||
}
|
||||
@Test
|
||||
func pngImageProcessing() async throws {
|
||||
let url = try #require(Bundle(for: Self.self).url(forResource: "test_image.png", withExtension: nil), "Failed retrieving test asset")
|
||||
|
||||
guard case let .success(result) = await mediaUploadingPreprocessor.processMedia(at: url, maxUploadSize: maxUploadSize),
|
||||
case let .image(convertedImageURL, _, imageInfo) = result else {
|
||||
XCTFail("Failed processing asset")
|
||||
Issue.record("Failed processing asset")
|
||||
return
|
||||
}
|
||||
|
||||
// Make sure the output file matches the image info.
|
||||
XCTAssertEqual(mimeType(from: convertedImageURL), "image/png", "PNGs should always be sent as PNG to preserve the alpha channel.")
|
||||
XCTAssertEqual(convertedImageURL.pathExtension, "png", "The file extension should match the MIME type.")
|
||||
#expect(mimeType(from: convertedImageURL) == "image/png", "PNGs should always be sent as PNG to preserve the alpha channel.")
|
||||
#expect(convertedImageURL.pathExtension == "png", "The file extension should match the MIME type.")
|
||||
|
||||
// Check resulting image info
|
||||
XCTAssertEqual(imageInfo.mimetype, "image/png")
|
||||
XCTAssertEqual(imageInfo.blurhash, "K0TSUA~qfQ~qj[fQfQfQfQ")
|
||||
XCTAssertEqual(imageInfo.size ?? 0, 4868, accuracy: 100)
|
||||
XCTAssertEqual(imageInfo.width, 240)
|
||||
XCTAssertEqual(imageInfo.height, 240)
|
||||
#expect(imageInfo.mimetype == "image/png")
|
||||
#expect(imageInfo.blurhash == "K0TSUA~qfQ~qj[fQfQfQfQ")
|
||||
#expect(isEqual(imageInfo.size ?? 0, 4868, within: 100))
|
||||
#expect(imageInfo.width == 240)
|
||||
#expect(imageInfo.height == 240)
|
||||
|
||||
XCTAssertNotNil(imageInfo.thumbnailInfo)
|
||||
XCTAssertEqual(imageInfo.thumbnailInfo?.mimetype, "image/jpeg")
|
||||
XCTAssertEqual(imageInfo.thumbnailInfo?.size ?? 0, 1725, accuracy: 100)
|
||||
XCTAssertEqual(imageInfo.thumbnailInfo?.width, 240)
|
||||
XCTAssertEqual(imageInfo.thumbnailInfo?.height, 240)
|
||||
#expect(imageInfo.thumbnailInfo != nil)
|
||||
#expect(imageInfo.thumbnailInfo?.mimetype == "image/jpeg")
|
||||
#expect(isEqual(imageInfo.thumbnailInfo?.size ?? 0, 1725, within: 100))
|
||||
#expect(imageInfo.thumbnailInfo?.width == 240)
|
||||
#expect(imageInfo.thumbnailInfo?.height == 240)
|
||||
|
||||
// Repeat with optimised media setting
|
||||
appSettings.optimizeMediaUploads = true
|
||||
|
||||
guard case let .success(optimizedResult) = await mediaUploadingPreprocessor.processMedia(at: url, maxUploadSize: maxUploadSize),
|
||||
case let .image(optimizedImageURL, _, optimizedImageInfo) = optimizedResult else {
|
||||
XCTFail("Failed processing asset")
|
||||
Issue.record("Failed processing asset")
|
||||
return
|
||||
}
|
||||
|
||||
// Make sure the output file matches the image info.
|
||||
XCTAssertEqual(mimeType(from: optimizedImageURL), "image/png", "PNGs should always be sent as PNG to preserve the alpha channel.")
|
||||
XCTAssertEqual(optimizedImageURL.pathExtension, "png", "The file extension should match the MIME type.")
|
||||
#expect(mimeType(from: optimizedImageURL) == "image/png", "PNGs should always be sent as PNG to preserve the alpha channel.")
|
||||
#expect(optimizedImageURL.pathExtension == "png", "The file extension should match the MIME type.")
|
||||
|
||||
// Check optimised image info
|
||||
XCTAssertEqual(optimizedImageInfo.mimetype, "image/png")
|
||||
XCTAssertEqual(optimizedImageInfo.blurhash, "K0TSUA~qfQ~qj[fQfQfQfQ")
|
||||
XCTAssertEqual(optimizedImageInfo.size ?? 0, 8199, accuracy: 100)
|
||||
#expect(optimizedImageInfo.mimetype == "image/png")
|
||||
#expect(optimizedImageInfo.blurhash == "K0TSUA~qfQ~qj[fQfQfQfQ")
|
||||
#expect(isEqual(optimizedImageInfo.size ?? 0, 8199, within: 100))
|
||||
// Assert that resizing didn't upscale to the maxPixelSize.
|
||||
XCTAssertEqual(optimizedImageInfo.width, 240)
|
||||
XCTAssertEqual(optimizedImageInfo.height, 240)
|
||||
#expect(optimizedImageInfo.width == 240)
|
||||
#expect(optimizedImageInfo.height == 240)
|
||||
}
|
||||
|
||||
func testHEICImageProcessing() async {
|
||||
guard let url = Bundle(for: Self.self).url(forResource: "test_apple_image.heic", withExtension: nil) else {
|
||||
XCTFail("Failed retrieving test asset")
|
||||
return
|
||||
}
|
||||
@Test
|
||||
func heicImageProcessing() async throws {
|
||||
let url = try #require(Bundle(for: Self.self).url(forResource: "test_apple_image.heic", withExtension: nil), "Failed retrieving test asset")
|
||||
|
||||
guard case let .success(result) = await mediaUploadingPreprocessor.processMedia(at: url, maxUploadSize: maxUploadSize),
|
||||
case let .image(convertedImageURL, thumbnailURL, imageInfo) = result else {
|
||||
XCTFail("Failed processing asset")
|
||||
Issue.record("Failed processing asset")
|
||||
return
|
||||
}
|
||||
|
||||
compare(originalImageAt: url, toConvertedImageAt: convertedImageURL, withThumbnailAt: thumbnailURL)
|
||||
try compare(originalImageAt: url, toConvertedImageAt: convertedImageURL, withThumbnailAt: thumbnailURL)
|
||||
|
||||
// Make sure the output file matches the image info.
|
||||
XCTAssertEqual(mimeType(from: convertedImageURL), "image/heic", "Unoptimised HEICs should always be sent as is.")
|
||||
XCTAssertEqual(convertedImageURL.pathExtension, "heic", "The file extension should match the MIME type.")
|
||||
#expect(mimeType(from: convertedImageURL) == "image/heic", "Unoptimised HEICs should always be sent as is.")
|
||||
#expect(convertedImageURL.pathExtension == "heic", "The file extension should match the MIME type.")
|
||||
|
||||
// Check resulting image info
|
||||
XCTAssertEqual(imageInfo.mimetype, "image/heic")
|
||||
XCTAssertEqual(imageInfo.blurhash, "KGD]3ns:T00$kWxFXmt6xv")
|
||||
XCTAssertEqual(imageInfo.size ?? 0, 1_848_525, accuracy: 100)
|
||||
XCTAssertEqual(imageInfo.width, 3024)
|
||||
XCTAssertEqual(imageInfo.height, 4032)
|
||||
#expect(imageInfo.mimetype == "image/heic")
|
||||
#expect(imageInfo.blurhash == "KGD]3ns:T00$kWxFXmt6xv")
|
||||
#expect(isEqual(imageInfo.size ?? 0, 1_848_525, within: 100))
|
||||
#expect(imageInfo.width == 3024)
|
||||
#expect(imageInfo.height == 4032)
|
||||
|
||||
XCTAssertNotNil(imageInfo.thumbnailInfo)
|
||||
XCTAssertEqual(imageInfo.thumbnailInfo?.mimetype, "image/jpeg")
|
||||
XCTAssertEqual(imageInfo.thumbnailInfo?.size ?? 0, 218_108, accuracy: 100)
|
||||
XCTAssertEqual(imageInfo.thumbnailInfo?.width, 600)
|
||||
XCTAssertEqual(imageInfo.thumbnailInfo?.height, 800)
|
||||
#expect(imageInfo.thumbnailInfo != nil)
|
||||
#expect(imageInfo.thumbnailInfo?.mimetype == "image/jpeg")
|
||||
#expect(isEqual(imageInfo.thumbnailInfo?.size ?? 0, 218_108, within: 100))
|
||||
#expect(imageInfo.thumbnailInfo?.width == 600)
|
||||
#expect(imageInfo.thumbnailInfo?.height == 800)
|
||||
|
||||
// Repeat with optimised media setting
|
||||
appSettings.optimizeMediaUploads = true
|
||||
|
||||
guard case let .success(optimizedResult) = await mediaUploadingPreprocessor.processMedia(at: url, maxUploadSize: maxUploadSize),
|
||||
case let .image(optimizedImageURL, thumbnailURL, optimizedImageInfo) = optimizedResult else {
|
||||
XCTFail("Failed processing asset")
|
||||
Issue.record("Failed processing asset")
|
||||
return
|
||||
}
|
||||
|
||||
compare(originalImageAt: url, toConvertedImageAt: optimizedImageURL, withThumbnailAt: thumbnailURL)
|
||||
try compare(originalImageAt: url, toConvertedImageAt: optimizedImageURL, withThumbnailAt: thumbnailURL)
|
||||
|
||||
// Make sure the output file matches the image info.
|
||||
XCTAssertEqual(mimeType(from: optimizedImageURL), "image/jpeg", "Optimised HEICs should always be converted to JPEG for compatibility.")
|
||||
XCTAssertEqual(optimizedImageURL.pathExtension, "jpeg", "The file extension should match the MIME type.")
|
||||
#expect(mimeType(from: optimizedImageURL) == "image/jpeg", "Optimised HEICs should always be converted to JPEG for compatibility.")
|
||||
#expect(optimizedImageURL.pathExtension == "jpeg", "The file extension should match the MIME type.")
|
||||
|
||||
// Check optimised image info
|
||||
XCTAssertEqual(optimizedImageInfo.mimetype, "image/jpeg")
|
||||
XCTAssertEqual(optimizedImageInfo.blurhash, "KGD]3ns:T00#kWxFb^s:xv")
|
||||
XCTAssertEqual(optimizedImageInfo.size ?? 0, 1_049_393, accuracy: 100)
|
||||
XCTAssertEqual(optimizedImageInfo.width, 1536)
|
||||
XCTAssertEqual(optimizedImageInfo.height, 2048)
|
||||
#expect(optimizedImageInfo.mimetype == "image/jpeg")
|
||||
#expect(optimizedImageInfo.blurhash == "KGD]3ns:T00#kWxFb^s:xv")
|
||||
#expect(isEqual(optimizedImageInfo.size ?? 0, 1_049_393, within: 100))
|
||||
#expect(optimizedImageInfo.width == 1536)
|
||||
#expect(optimizedImageInfo.height == 2048)
|
||||
}
|
||||
|
||||
func testGIFImageProcessing() async {
|
||||
guard let url = Bundle(for: Self.self).url(forResource: "test_animated_image.gif", withExtension: nil) else {
|
||||
XCTFail("Failed retrieving test asset")
|
||||
return
|
||||
}
|
||||
guard let originalSize = try? FileManager.default.sizeForItem(at: url), originalSize > 0 else {
|
||||
XCTFail("Failed fetching test asset's original size")
|
||||
return
|
||||
}
|
||||
@Test
|
||||
func gifImageProcessing() async throws {
|
||||
let url = try #require(Bundle(for: Self.self).url(forResource: "test_animated_image.gif", withExtension: nil), "Failed retrieving test asset")
|
||||
let originalSizeValue = try UInt64(FileManager.default.sizeForItem(at: url))
|
||||
let originalSize = try #require(originalSizeValue > 0 ? originalSizeValue : nil, "File size must be greater than zero")
|
||||
|
||||
guard case let .success(result) = await mediaUploadingPreprocessor.processMedia(at: url, maxUploadSize: maxUploadSize),
|
||||
case let .image(convertedImageURL, _, imageInfo) = result else {
|
||||
XCTFail("Failed processing asset")
|
||||
Issue.record("Failed processing asset")
|
||||
return
|
||||
}
|
||||
|
||||
// Make sure the output file matches the image info.
|
||||
XCTAssertEqual(mimeType(from: convertedImageURL), "image/gif", "GIFs should always be sent as GIF to preserve the animation.")
|
||||
XCTAssertEqual(convertedImageURL.pathExtension, "gif", "The file extension should match the MIME type.")
|
||||
#expect(mimeType(from: convertedImageURL) == "image/gif", "GIFs should always be sent as GIF to preserve the animation.")
|
||||
#expect(convertedImageURL.pathExtension == "gif", "The file extension should match the MIME type.")
|
||||
|
||||
// Check resulting image info
|
||||
XCTAssertEqual(imageInfo.mimetype, "image/gif")
|
||||
XCTAssertEqual(imageInfo.blurhash, "KpRMPTj[_NxuaeRj%MofMx")
|
||||
XCTAssertEqual(imageInfo.size ?? 0, UInt64(originalSize), accuracy: 100)
|
||||
XCTAssertEqual(imageInfo.width, 331)
|
||||
XCTAssertEqual(imageInfo.height, 472)
|
||||
#expect(imageInfo.mimetype == "image/gif")
|
||||
#expect(imageInfo.blurhash == "KpRMPTj[_NxuaeRj%MofMx")
|
||||
#expect(isEqual(imageInfo.size ?? 0, originalSize, within: 100))
|
||||
#expect(imageInfo.width == 331)
|
||||
#expect(imageInfo.height == 472)
|
||||
|
||||
XCTAssertNotNil(imageInfo.thumbnailInfo)
|
||||
XCTAssertEqual(imageInfo.thumbnailInfo?.mimetype, "image/jpeg")
|
||||
XCTAssertEqual(imageInfo.thumbnailInfo?.size ?? 0, 34215, accuracy: 100)
|
||||
XCTAssertEqual(imageInfo.thumbnailInfo?.width, 331)
|
||||
XCTAssertEqual(imageInfo.thumbnailInfo?.height, 472)
|
||||
#expect(imageInfo.thumbnailInfo != nil)
|
||||
#expect(imageInfo.thumbnailInfo?.mimetype == "image/jpeg")
|
||||
#expect(isEqual(imageInfo.thumbnailInfo?.size ?? 0, 34215, within: 100))
|
||||
#expect(imageInfo.thumbnailInfo?.width == 331)
|
||||
#expect(imageInfo.thumbnailInfo?.height == 472)
|
||||
|
||||
// Repeat with optimised media setting
|
||||
appSettings.optimizeMediaUploads = true
|
||||
|
||||
guard case let .success(optimizedResult) = await mediaUploadingPreprocessor.processMedia(at: url, maxUploadSize: maxUploadSize),
|
||||
case let .image(optimizedImageURL, _, optimizedImageInfo) = optimizedResult else {
|
||||
XCTFail("Failed processing asset")
|
||||
Issue.record("Failed processing asset")
|
||||
return
|
||||
}
|
||||
|
||||
// Make sure the output file matches the image info.
|
||||
XCTAssertEqual(mimeType(from: optimizedImageURL), "image/gif", "GIFs should always be sent as GIF to preserve the animation.")
|
||||
XCTAssertEqual(optimizedImageURL.pathExtension, "gif", "The file extension should match the MIME type.")
|
||||
#expect(mimeType(from: optimizedImageURL) == "image/gif", "GIFs should always be sent as GIF to preserve the animation.")
|
||||
#expect(optimizedImageURL.pathExtension == "gif", "The file extension should match the MIME type.")
|
||||
|
||||
// Ensure optimised image is still the same as the original image.
|
||||
XCTAssertEqual(optimizedImageInfo.mimetype, "image/gif")
|
||||
XCTAssertEqual(optimizedImageInfo.blurhash, "KpRMPTj[_NxuaeRj%MofMx")
|
||||
XCTAssertEqual(optimizedImageInfo.size ?? 0, UInt64(originalSize), accuracy: 100)
|
||||
XCTAssertEqual(optimizedImageInfo.width, 331)
|
||||
XCTAssertEqual(optimizedImageInfo.height, 472)
|
||||
#expect(optimizedImageInfo.mimetype == "image/gif")
|
||||
#expect(optimizedImageInfo.blurhash == "KpRMPTj[_NxuaeRj%MofMx")
|
||||
#expect(isEqual(optimizedImageInfo.size ?? 0, originalSize, within: 100))
|
||||
#expect(optimizedImageInfo.width == 331)
|
||||
#expect(optimizedImageInfo.height == 472)
|
||||
}
|
||||
|
||||
func testRotatedImageProcessing() async {
|
||||
guard let url = Bundle(for: Self.self).url(forResource: "test_rotated_image.jpg", withExtension: nil) else {
|
||||
XCTFail("Failed retrieving test asset")
|
||||
return
|
||||
}
|
||||
@Test
|
||||
func rotatedImageProcessing() async throws {
|
||||
let url = try #require(Bundle(for: Self.self).url(forResource: "test_rotated_image.jpg", withExtension: nil), "Failed retrieving test asset")
|
||||
|
||||
guard case let .success(result) = await mediaUploadingPreprocessor.processMedia(at: url, maxUploadSize: maxUploadSize),
|
||||
case let .image(convertedImageURL, thumbnailURL, imageInfo) = result else {
|
||||
XCTFail("Failed processing asset")
|
||||
Issue.record("Failed processing asset")
|
||||
return
|
||||
}
|
||||
|
||||
compare(originalImageAt: url, toConvertedImageAt: convertedImageURL, withThumbnailAt: thumbnailURL)
|
||||
try compare(originalImageAt: url, toConvertedImageAt: convertedImageURL, withThumbnailAt: thumbnailURL)
|
||||
|
||||
// Check resulting image info
|
||||
XCTAssertEqual(imageInfo.mimetype, "image/jpeg")
|
||||
XCTAssertEqual(imageInfo.width, 2848)
|
||||
XCTAssertEqual(imageInfo.height, 4272)
|
||||
#expect(imageInfo.mimetype == "image/jpeg")
|
||||
#expect(imageInfo.width == 2848)
|
||||
#expect(imageInfo.height == 4272)
|
||||
|
||||
XCTAssertNotNil(imageInfo.thumbnailInfo)
|
||||
XCTAssertEqual(imageInfo.thumbnailInfo?.width, 533)
|
||||
XCTAssertEqual(imageInfo.thumbnailInfo?.height, 800)
|
||||
#expect(imageInfo.thumbnailInfo != nil)
|
||||
#expect(imageInfo.thumbnailInfo?.width == 533)
|
||||
#expect(imageInfo.thumbnailInfo?.height == 800)
|
||||
|
||||
// Repeat with optimised media setting
|
||||
appSettings.optimizeMediaUploads = true
|
||||
|
||||
guard case let .success(optimizedResult) = await mediaUploadingPreprocessor.processMedia(at: url, maxUploadSize: maxUploadSize),
|
||||
case let .image(optimizedImageURL, thumbnailURL, optimizedImageInfo) = optimizedResult else {
|
||||
XCTFail("Failed processing asset")
|
||||
Issue.record("Failed processing asset")
|
||||
return
|
||||
}
|
||||
|
||||
compare(originalImageAt: url, toConvertedImageAt: optimizedImageURL, withThumbnailAt: thumbnailURL)
|
||||
try compare(originalImageAt: url, toConvertedImageAt: optimizedImageURL, withThumbnailAt: thumbnailURL)
|
||||
|
||||
// Check optimised image info
|
||||
XCTAssertEqual(optimizedImageInfo.mimetype, "image/jpeg")
|
||||
XCTAssertEqual(optimizedImageInfo.width, 1365)
|
||||
XCTAssertEqual(optimizedImageInfo.height, 2048)
|
||||
#expect(optimizedImageInfo.mimetype == "image/jpeg")
|
||||
#expect(optimizedImageInfo.width == 1365)
|
||||
#expect(optimizedImageInfo.height == 2048)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func compare(originalImageAt originalImageURL: URL, toConvertedImageAt convertedImageURL: URL, withThumbnailAt thumbnailURL: URL) {
|
||||
private func isEqual<N: UnsignedInteger>(_ lhs: N, _ rhs: N, within tolerance: N) -> Bool {
|
||||
isEqual(Double(lhs), Double(rhs), within: Double(tolerance))
|
||||
}
|
||||
|
||||
private func isEqual<N: SignedNumeric & Comparable>(_ lhs: N, _ rhs: N, within tolerance: N) -> Bool {
|
||||
abs(lhs - rhs) <= tolerance
|
||||
}
|
||||
|
||||
private func compare(originalImageAt originalImageURL: URL, toConvertedImageAt convertedImageURL: URL, withThumbnailAt thumbnailURL: URL) throws {
|
||||
guard let originalImageData = try? Data(contentsOf: originalImageURL),
|
||||
let originalImage = UIImage(data: originalImageData),
|
||||
let convertedImageData = try? Data(contentsOf: convertedImageURL),
|
||||
@@ -476,53 +454,41 @@ final class MediaUploadingPreprocessorTests: XCTestCase {
|
||||
|
||||
if appSettings.optimizeMediaUploads {
|
||||
// Check that new image has been scaled within the requirements for an optimised image
|
||||
XCTAssert(convertedImage.size.width <= MediaUploadingPreprocessor.Constants.optimizedMaxPixelSize)
|
||||
XCTAssert(convertedImage.size.height <= MediaUploadingPreprocessor.Constants.optimizedMaxPixelSize)
|
||||
#expect(convertedImage.size.width <= MediaUploadingPreprocessor.Constants.optimizedMaxPixelSize)
|
||||
#expect(convertedImage.size.height <= MediaUploadingPreprocessor.Constants.optimizedMaxPixelSize)
|
||||
} else {
|
||||
// Check that the file name is preserved
|
||||
XCTAssertEqual(originalImageURL.lastPathComponent, convertedImageURL.lastPathComponent)
|
||||
#expect(originalImageURL.lastPathComponent == convertedImageURL.lastPathComponent)
|
||||
// Check that new image is the same size as the original one
|
||||
XCTAssertEqual(originalImage.size, convertedImage.size)
|
||||
#expect(originalImage.size == convertedImage.size)
|
||||
}
|
||||
|
||||
// Check that the GPS data has been stripped
|
||||
let originalMetadata = metadata(from: originalImageData)
|
||||
XCTAssertNotNil(originalMetadata.value(forKeyPath: "\(kCGImagePropertyGPSDictionary)"))
|
||||
let originalMetadata = try metadata(from: originalImageData)
|
||||
#expect(originalMetadata.value(forKeyPath: "\(kCGImagePropertyGPSDictionary)") != nil)
|
||||
|
||||
let convertedMetadata = metadata(from: convertedImageData)
|
||||
XCTAssertNil(convertedMetadata.value(forKeyPath: "\(kCGImagePropertyGPSDictionary)"))
|
||||
let convertedMetadata = try metadata(from: convertedImageData)
|
||||
#expect(convertedMetadata.value(forKeyPath: "\(kCGImagePropertyGPSDictionary)") == nil)
|
||||
|
||||
// Check that the thumbnail is generated correctly
|
||||
guard let thumbnailData = try? Data(contentsOf: thumbnailURL),
|
||||
let thumbnail = UIImage(data: thumbnailData) else {
|
||||
XCTFail("Invalid thumbnail")
|
||||
return
|
||||
}
|
||||
let thumbnailData = try Data(contentsOf: thumbnailURL)
|
||||
let thumbnail = try #require(UIImage(data: thumbnailData), "Invalid thumbnail")
|
||||
|
||||
if thumbnail.size.width > thumbnail.size.height {
|
||||
XCTAssert(thumbnail.size.width <= MediaUploadingPreprocessor.Constants.maximumThumbnailSize.width)
|
||||
XCTAssert(thumbnail.size.height <= MediaUploadingPreprocessor.Constants.maximumThumbnailSize.height)
|
||||
#expect(thumbnail.size.width <= MediaUploadingPreprocessor.Constants.maximumThumbnailSize.width)
|
||||
#expect(thumbnail.size.height <= MediaUploadingPreprocessor.Constants.maximumThumbnailSize.height)
|
||||
} else {
|
||||
XCTAssert(thumbnail.size.width <= MediaUploadingPreprocessor.Constants.maximumThumbnailSize.height)
|
||||
XCTAssert(thumbnail.size.height <= MediaUploadingPreprocessor.Constants.maximumThumbnailSize.width)
|
||||
#expect(thumbnail.size.width <= MediaUploadingPreprocessor.Constants.maximumThumbnailSize.height)
|
||||
#expect(thumbnail.size.height <= MediaUploadingPreprocessor.Constants.maximumThumbnailSize.width)
|
||||
}
|
||||
|
||||
let thumbnailMetadata = metadata(from: thumbnailData)
|
||||
XCTAssertNil(thumbnailMetadata.value(forKeyPath: "\(kCGImagePropertyGPSDictionary)"))
|
||||
let thumbnailMetadata = try metadata(from: thumbnailData)
|
||||
#expect(thumbnailMetadata.value(forKeyPath: "\(kCGImagePropertyGPSDictionary)") == nil)
|
||||
}
|
||||
|
||||
private func metadata(from imageData: Data) -> NSDictionary {
|
||||
guard let imageSource = CGImageSourceCreateWithData(imageData as NSData, nil) else {
|
||||
XCTFail("Invalid asset")
|
||||
return [:]
|
||||
}
|
||||
|
||||
guard let convertedMetadata: NSDictionary = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) else {
|
||||
XCTFail("Test asset is expected to contain metadata")
|
||||
return [:]
|
||||
}
|
||||
|
||||
return convertedMetadata
|
||||
private func metadata(from imageData: Data) throws -> NSDictionary {
|
||||
let imageSource = try #require(CGImageSourceCreateWithData(imageData as NSData, nil), "Invalid asset")
|
||||
return try #require(CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as NSDictionary?, "Test asset is expected to contain metadata")
|
||||
}
|
||||
|
||||
private func mimeType(from url: URL) -> String? {
|
||||
@@ -530,7 +496,7 @@ final class MediaUploadingPreprocessorTests: XCTestCase {
|
||||
let typeIdentifier = CGImageSourceGetType(imageSource),
|
||||
let type = UTType(typeIdentifier as String),
|
||||
let mimeType = type.preferredMIMEType else {
|
||||
XCTFail("Failed to get mimetype from URL.")
|
||||
Issue.record("Failed to get mimetype from URL.")
|
||||
return nil
|
||||
}
|
||||
return mimeType
|
||||
|
||||
@@ -7,37 +7,42 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
@Suite
|
||||
@MainActor
|
||||
class NavigationSplitCoordinatorTests: XCTestCase {
|
||||
private var navigationSplitCoordinator: NavigationSplitCoordinator!
|
||||
struct NavigationSplitCoordinatorTests {
|
||||
private var navigationSplitCoordinator: NavigationSplitCoordinator
|
||||
|
||||
override func setUp() {
|
||||
init() {
|
||||
navigationSplitCoordinator = NavigationSplitCoordinator(placeholderCoordinator: SomeTestCoordinator())
|
||||
}
|
||||
|
||||
func testSidebar() {
|
||||
XCTAssertNil(navigationSplitCoordinator.sidebarCoordinator)
|
||||
XCTAssertNil(navigationSplitCoordinator.detailCoordinator)
|
||||
@Test
|
||||
func sidebar() {
|
||||
#expect(navigationSplitCoordinator.sidebarCoordinator == nil)
|
||||
#expect(navigationSplitCoordinator.detailCoordinator == nil)
|
||||
|
||||
let sidebarCoordinator = SomeTestCoordinator()
|
||||
navigationSplitCoordinator.setSidebarCoordinator(sidebarCoordinator)
|
||||
assertCoordinatorsEqual(sidebarCoordinator, navigationSplitCoordinator.sidebarCoordinator)
|
||||
}
|
||||
|
||||
func testDetail() {
|
||||
XCTAssertNil(navigationSplitCoordinator.sidebarCoordinator)
|
||||
XCTAssertNil(navigationSplitCoordinator.detailCoordinator)
|
||||
@Test
|
||||
func detail() {
|
||||
#expect(navigationSplitCoordinator.sidebarCoordinator == nil)
|
||||
#expect(navigationSplitCoordinator.detailCoordinator == nil)
|
||||
|
||||
let detailCoordinator = SomeTestCoordinator()
|
||||
navigationSplitCoordinator.setDetailCoordinator(detailCoordinator)
|
||||
assertCoordinatorsEqual(detailCoordinator, navigationSplitCoordinator.detailCoordinator)
|
||||
}
|
||||
|
||||
func testSidebarAndDetail() {
|
||||
XCTAssertNil(navigationSplitCoordinator.sidebarCoordinator)
|
||||
XCTAssertNil(navigationSplitCoordinator.detailCoordinator)
|
||||
@Test
|
||||
func sidebarAndDetail() {
|
||||
#expect(navigationSplitCoordinator.sidebarCoordinator == nil)
|
||||
#expect(navigationSplitCoordinator.detailCoordinator == nil)
|
||||
|
||||
let sidebarCoordinator = SomeTestCoordinator()
|
||||
navigationSplitCoordinator.setSidebarCoordinator(sidebarCoordinator)
|
||||
@@ -49,7 +54,8 @@ class NavigationSplitCoordinatorTests: XCTestCase {
|
||||
assertCoordinatorsEqual(detailCoordinator, navigationSplitCoordinator.detailCoordinator)
|
||||
}
|
||||
|
||||
func testSingleSheet() {
|
||||
@Test
|
||||
func singleSheet() {
|
||||
let sidebarCoordinator = SomeTestCoordinator()
|
||||
navigationSplitCoordinator.setSidebarCoordinator(sidebarCoordinator)
|
||||
let detailCoordinator = SomeTestCoordinator()
|
||||
@@ -66,10 +72,11 @@ class NavigationSplitCoordinatorTests: XCTestCase {
|
||||
|
||||
assertCoordinatorsEqual(sidebarCoordinator, navigationSplitCoordinator.sidebarCoordinator)
|
||||
assertCoordinatorsEqual(detailCoordinator, navigationSplitCoordinator.detailCoordinator)
|
||||
XCTAssertNil(navigationSplitCoordinator.sheetCoordinator)
|
||||
#expect(navigationSplitCoordinator.sheetCoordinator == nil)
|
||||
}
|
||||
|
||||
func testMultipleSheets() {
|
||||
@Test
|
||||
func multipleSheets() {
|
||||
let sidebarCoordinator = SomeTestCoordinator()
|
||||
navigationSplitCoordinator.setSidebarCoordinator(sidebarCoordinator)
|
||||
let detailCoordinator = SomeTestCoordinator()
|
||||
@@ -90,7 +97,8 @@ class NavigationSplitCoordinatorTests: XCTestCase {
|
||||
assertCoordinatorsEqual(someOtherSheetCoordinator, navigationSplitCoordinator.sheetCoordinator)
|
||||
}
|
||||
|
||||
func testFullScreenCover() {
|
||||
@Test
|
||||
func fullScreenCover() {
|
||||
let sidebarCoordinator = SomeTestCoordinator()
|
||||
navigationSplitCoordinator.setSidebarCoordinator(sidebarCoordinator)
|
||||
let detailCoordinator = SomeTestCoordinator()
|
||||
@@ -107,62 +115,67 @@ class NavigationSplitCoordinatorTests: XCTestCase {
|
||||
|
||||
assertCoordinatorsEqual(sidebarCoordinator, navigationSplitCoordinator.sidebarCoordinator)
|
||||
assertCoordinatorsEqual(detailCoordinator, navigationSplitCoordinator.detailCoordinator)
|
||||
XCTAssertNil(navigationSplitCoordinator.fullScreenCoverCoordinator)
|
||||
#expect(navigationSplitCoordinator.fullScreenCoverCoordinator == nil)
|
||||
}
|
||||
|
||||
// MARK: - Dismissal Callbacks
|
||||
|
||||
func testSidebarReplacementCallbacks() {
|
||||
@Test
|
||||
func sidebarReplacementCallbacks() async {
|
||||
let sidebarCoordinator = SomeTestCoordinator()
|
||||
|
||||
let expectation = expectation(description: "Wait for callback")
|
||||
navigationSplitCoordinator.setSidebarCoordinator(sidebarCoordinator) {
|
||||
expectation.fulfill()
|
||||
await waitForConfirmation("Wait for callback", timeout: .seconds(1)) { confirm in
|
||||
navigationSplitCoordinator.setSidebarCoordinator(sidebarCoordinator) {
|
||||
confirm()
|
||||
}
|
||||
|
||||
navigationSplitCoordinator.setSidebarCoordinator(nil)
|
||||
}
|
||||
|
||||
navigationSplitCoordinator.setSidebarCoordinator(nil)
|
||||
waitForExpectations(timeout: 1.0)
|
||||
}
|
||||
|
||||
func testDetailReplacementCallbacks() {
|
||||
@Test
|
||||
func detailReplacementCallbacks() async {
|
||||
let detailCoordinator = SomeTestCoordinator()
|
||||
|
||||
let expectation = expectation(description: "Wait for callback")
|
||||
navigationSplitCoordinator.setDetailCoordinator(detailCoordinator) {
|
||||
expectation.fulfill()
|
||||
await waitForConfirmation("Wait for callback", timeout: .seconds(1)) { confirm in
|
||||
navigationSplitCoordinator.setDetailCoordinator(detailCoordinator) {
|
||||
confirm()
|
||||
}
|
||||
|
||||
navigationSplitCoordinator.setDetailCoordinator(nil)
|
||||
}
|
||||
|
||||
navigationSplitCoordinator.setDetailCoordinator(nil)
|
||||
waitForExpectations(timeout: 1.0)
|
||||
}
|
||||
|
||||
func testSheetDismissalCallback() {
|
||||
@Test
|
||||
func sheetDismissalCallback() async {
|
||||
let sheetCoordinator = SomeTestCoordinator()
|
||||
|
||||
let expectation = expectation(description: "Wait for callback")
|
||||
navigationSplitCoordinator.setSheetCoordinator(sheetCoordinator) {
|
||||
expectation.fulfill()
|
||||
await waitForConfirmation("Wait for callback", timeout: .seconds(1)) { confirm in
|
||||
navigationSplitCoordinator.setSheetCoordinator(sheetCoordinator) {
|
||||
confirm()
|
||||
}
|
||||
|
||||
navigationSplitCoordinator.setSheetCoordinator(nil)
|
||||
}
|
||||
|
||||
navigationSplitCoordinator.setSheetCoordinator(nil)
|
||||
waitForExpectations(timeout: 1.0)
|
||||
}
|
||||
|
||||
func testFullScreenCoverDismissalCallback() {
|
||||
@Test
|
||||
func fullScreenCoverDismissalCallback() async {
|
||||
let fullScreenCoordinator = SomeTestCoordinator()
|
||||
|
||||
let expectation = expectation(description: "Wait for callback")
|
||||
navigationSplitCoordinator.setFullScreenCoverCoordinator(fullScreenCoordinator) {
|
||||
expectation.fulfill()
|
||||
await waitForConfirmation("Wait for callback", timeout: .seconds(1)) { confirm in
|
||||
navigationSplitCoordinator.setFullScreenCoverCoordinator(fullScreenCoordinator) {
|
||||
confirm()
|
||||
}
|
||||
|
||||
navigationSplitCoordinator.setFullScreenCoverCoordinator(nil)
|
||||
}
|
||||
|
||||
navigationSplitCoordinator.setFullScreenCoverCoordinator(nil)
|
||||
waitForExpectations(timeout: 1.0)
|
||||
}
|
||||
|
||||
// MARK: - Advanced
|
||||
|
||||
func testEmbeddedStackPresentsSheetThroughSplit() {
|
||||
@Test
|
||||
func embeddedStackPresentsSheetThroughSplit() {
|
||||
let sidebarNavigationStackCoordinator = NavigationStackCoordinator(navigationSplitCoordinator: navigationSplitCoordinator)
|
||||
sidebarNavigationStackCoordinator.setRootCoordinator(SomeTestCoordinator())
|
||||
|
||||
@@ -175,7 +188,8 @@ class NavigationSplitCoordinatorTests: XCTestCase {
|
||||
assertCoordinatorsEqual(sheetCoordinator, navigationSplitCoordinator.sheetCoordinator)
|
||||
}
|
||||
|
||||
func testSplitTracksEmbeddedStackRootChanges() {
|
||||
@Test
|
||||
func splitTracksEmbeddedStackRootChanges() async {
|
||||
let sidebarNavigationStackCoordinator = NavigationStackCoordinator(navigationSplitCoordinator: navigationSplitCoordinator)
|
||||
sidebarNavigationStackCoordinator.setRootCoordinator(SomeTestCoordinator())
|
||||
|
||||
@@ -185,36 +199,38 @@ class NavigationSplitCoordinatorTests: XCTestCase {
|
||||
|
||||
sidebarNavigationStackCoordinator.setRootCoordinator(SomeTestCoordinator())
|
||||
|
||||
let expectation = expectation(description: "Coordinators should match")
|
||||
DispatchQueue.main.async {
|
||||
self.assertCoordinatorsEqual(sidebarNavigationStackCoordinator.rootCoordinator, self.navigationSplitCoordinator.compactLayoutRootCoordinator)
|
||||
expectation.fulfill()
|
||||
}
|
||||
waitForExpectations(timeout: 1.0)
|
||||
}
|
||||
|
||||
func testSplitTracksEmbeddedStackChanges() {
|
||||
let sidebarNavigationStackCoordinator = NavigationStackCoordinator(navigationSplitCoordinator: navigationSplitCoordinator)
|
||||
sidebarNavigationStackCoordinator.setRootCoordinator(SomeTestCoordinator())
|
||||
|
||||
navigationSplitCoordinator.setSidebarCoordinator(sidebarNavigationStackCoordinator)
|
||||
|
||||
assertCoordinatorsEqual(sidebarNavigationStackCoordinator.rootCoordinator, navigationSplitCoordinator.compactLayoutRootCoordinator)
|
||||
|
||||
sidebarNavigationStackCoordinator.push(SomeTestCoordinator())
|
||||
|
||||
let expectation = expectation(description: "Coordinators should match")
|
||||
DispatchQueue.main.async {
|
||||
XCTAssertEqual(sidebarNavigationStackCoordinator.stackCoordinators.count, self.navigationSplitCoordinator.compactLayoutStackCoordinators.count)
|
||||
for index in sidebarNavigationStackCoordinator.stackCoordinators.indices {
|
||||
self.assertCoordinatorsEqual(sidebarNavigationStackCoordinator.stackCoordinators[index], self.navigationSplitCoordinator.compactLayoutStackCoordinators[index])
|
||||
await waitForConfirmation("Coordinators should match", timeout: .seconds(1)) { confirm in
|
||||
DispatchQueue.main.async {
|
||||
assertCoordinatorsEqual(sidebarNavigationStackCoordinator.rootCoordinator, navigationSplitCoordinator.compactLayoutRootCoordinator)
|
||||
confirm()
|
||||
}
|
||||
expectation.fulfill()
|
||||
}
|
||||
waitForExpectations(timeout: 1.0)
|
||||
}
|
||||
|
||||
func testSplitPropagatesCompactStackChanges() {
|
||||
@Test
|
||||
func splitTracksEmbeddedStackChanges() async {
|
||||
let sidebarNavigationStackCoordinator = NavigationStackCoordinator(navigationSplitCoordinator: navigationSplitCoordinator)
|
||||
sidebarNavigationStackCoordinator.setRootCoordinator(SomeTestCoordinator())
|
||||
|
||||
navigationSplitCoordinator.setSidebarCoordinator(sidebarNavigationStackCoordinator)
|
||||
|
||||
assertCoordinatorsEqual(sidebarNavigationStackCoordinator.rootCoordinator, navigationSplitCoordinator.compactLayoutRootCoordinator)
|
||||
|
||||
sidebarNavigationStackCoordinator.push(SomeTestCoordinator())
|
||||
|
||||
await waitForConfirmation("Coordinators should match", timeout: .seconds(1)) { confirm in
|
||||
DispatchQueue.main.async {
|
||||
#expect(sidebarNavigationStackCoordinator.stackCoordinators.count == navigationSplitCoordinator.compactLayoutStackCoordinators.count)
|
||||
for index in sidebarNavigationStackCoordinator.stackCoordinators.indices {
|
||||
assertCoordinatorsEqual(sidebarNavigationStackCoordinator.stackCoordinators[index], navigationSplitCoordinator.compactLayoutStackCoordinators[index])
|
||||
}
|
||||
confirm()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func splitPropagatesCompactStackChanges() {
|
||||
let sidebarNavigationStackCoordinator = NavigationStackCoordinator(navigationSplitCoordinator: navigationSplitCoordinator)
|
||||
sidebarNavigationStackCoordinator.setRootCoordinator(SomeTestCoordinator())
|
||||
sidebarNavigationStackCoordinator.push(SomeTestCoordinator())
|
||||
@@ -222,14 +238,15 @@ class NavigationSplitCoordinatorTests: XCTestCase {
|
||||
navigationSplitCoordinator.setSidebarCoordinator(sidebarNavigationStackCoordinator)
|
||||
|
||||
assertCoordinatorsEqual(sidebarNavigationStackCoordinator.rootCoordinator, navigationSplitCoordinator.compactLayoutRootCoordinator)
|
||||
XCTAssertEqual(sidebarNavigationStackCoordinator.stackCoordinators.count, navigationSplitCoordinator.compactLayoutStackCoordinators.count)
|
||||
#expect(sidebarNavigationStackCoordinator.stackCoordinators.count == navigationSplitCoordinator.compactLayoutStackCoordinators.count)
|
||||
|
||||
navigationSplitCoordinator.compactLayoutStackModules.removeAll()
|
||||
|
||||
XCTAssertTrue(sidebarNavigationStackCoordinator.stackCoordinators.isEmpty)
|
||||
#expect(sidebarNavigationStackCoordinator.stackCoordinators.isEmpty)
|
||||
}
|
||||
|
||||
func testCompactStackCreation() {
|
||||
@Test
|
||||
func compactStackCreation() async {
|
||||
let sidebarCoordinator = NavigationStackCoordinator()
|
||||
sidebarCoordinator.setRootCoordinator(SomeTestCoordinator())
|
||||
sidebarCoordinator.push(SomeTestCoordinator())
|
||||
@@ -242,19 +259,20 @@ class NavigationSplitCoordinatorTests: XCTestCase {
|
||||
navigationSplitCoordinator.setSidebarCoordinator(sidebarCoordinator)
|
||||
navigationSplitCoordinator.setDetailCoordinator(detailCoordinator)
|
||||
|
||||
let expectation = expectation(description: "Coordinators should match")
|
||||
DispatchQueue.main.async {
|
||||
self.assertCoordinatorsEqual(self.navigationSplitCoordinator.compactLayoutRootCoordinator, sidebarCoordinator.rootCoordinator)
|
||||
self.assertCoordinatorsEqual(self.navigationSplitCoordinator.compactLayoutStackModules[0].coordinator, sidebarCoordinator.stackCoordinators.first)
|
||||
self.assertCoordinatorsEqual(self.navigationSplitCoordinator.compactLayoutStackModules[1].coordinator, detailCoordinator.rootCoordinator)
|
||||
self.assertCoordinatorsEqual(self.navigationSplitCoordinator.compactLayoutStackModules[2].coordinator, detailCoordinator.stackCoordinators.first)
|
||||
self.assertCoordinatorsEqual(self.navigationSplitCoordinator.compactLayoutStackModules[3].coordinator, detailCoordinator.stackCoordinators.last)
|
||||
expectation.fulfill()
|
||||
await waitForConfirmation("Coordinators should match", timeout: .seconds(1)) { confirm in
|
||||
DispatchQueue.main.async {
|
||||
assertCoordinatorsEqual(navigationSplitCoordinator.compactLayoutRootCoordinator, sidebarCoordinator.rootCoordinator)
|
||||
assertCoordinatorsEqual(navigationSplitCoordinator.compactLayoutStackModules[0].coordinator, sidebarCoordinator.stackCoordinators.first)
|
||||
assertCoordinatorsEqual(navigationSplitCoordinator.compactLayoutStackModules[1].coordinator, detailCoordinator.rootCoordinator)
|
||||
assertCoordinatorsEqual(navigationSplitCoordinator.compactLayoutStackModules[2].coordinator, detailCoordinator.stackCoordinators.first)
|
||||
assertCoordinatorsEqual(navigationSplitCoordinator.compactLayoutStackModules[3].coordinator, detailCoordinator.stackCoordinators.last)
|
||||
confirm()
|
||||
}
|
||||
}
|
||||
waitForExpectations(timeout: 1.0)
|
||||
}
|
||||
|
||||
func testRemovesDetailRootFromCompactStack() {
|
||||
@Test
|
||||
func removesDetailRootFromCompactStack() async {
|
||||
let sidebarCoordinator = NavigationStackCoordinator()
|
||||
sidebarCoordinator.setRootCoordinator(SomeTestCoordinator())
|
||||
|
||||
@@ -265,25 +283,26 @@ class NavigationSplitCoordinatorTests: XCTestCase {
|
||||
navigationSplitCoordinator.setSidebarCoordinator(sidebarCoordinator)
|
||||
navigationSplitCoordinator.setDetailCoordinator(detailCoordinator)
|
||||
|
||||
let expectation = expectation(description: "Coordinators should match")
|
||||
DispatchQueue.main.async {
|
||||
self.assertCoordinatorsEqual(self.navigationSplitCoordinator.compactLayoutRootCoordinator, sidebarCoordinator.rootCoordinator)
|
||||
self.assertCoordinatorsEqual(self.navigationSplitCoordinator.compactLayoutStackModules[0].coordinator, detailCoordinator.rootCoordinator)
|
||||
self.assertCoordinatorsEqual(self.navigationSplitCoordinator.compactLayoutStackModules[1].coordinator, detailCoordinator.stackCoordinators.first)
|
||||
|
||||
detailCoordinator.setRootCoordinator(nil)
|
||||
|
||||
await waitForConfirmation("Coordinators should match", timeout: .seconds(1)) { confirm in
|
||||
DispatchQueue.main.async {
|
||||
self.assertCoordinatorsEqual(self.navigationSplitCoordinator.compactLayoutRootCoordinator, sidebarCoordinator.rootCoordinator)
|
||||
self.assertCoordinatorsEqual(self.navigationSplitCoordinator.compactLayoutStackModules[0].coordinator, detailCoordinator.stackCoordinators.first)
|
||||
assertCoordinatorsEqual(navigationSplitCoordinator.compactLayoutRootCoordinator, sidebarCoordinator.rootCoordinator)
|
||||
assertCoordinatorsEqual(navigationSplitCoordinator.compactLayoutStackModules[0].coordinator, detailCoordinator.rootCoordinator)
|
||||
assertCoordinatorsEqual(navigationSplitCoordinator.compactLayoutStackModules[1].coordinator, detailCoordinator.stackCoordinators.first)
|
||||
|
||||
detailCoordinator.setRootCoordinator(nil)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
assertCoordinatorsEqual(navigationSplitCoordinator.compactLayoutRootCoordinator, sidebarCoordinator.rootCoordinator)
|
||||
assertCoordinatorsEqual(navigationSplitCoordinator.compactLayoutStackModules[0].coordinator, detailCoordinator.stackCoordinators.first)
|
||||
}
|
||||
|
||||
confirm()
|
||||
}
|
||||
|
||||
expectation.fulfill()
|
||||
}
|
||||
waitForExpectations(timeout: 1.0)
|
||||
}
|
||||
|
||||
func testSetRootDetailToNilAfterPoppingToRoot() {
|
||||
@Test
|
||||
mutating func setRootDetailToNilAfterPoppingToRoot() async {
|
||||
navigationSplitCoordinator = NavigationSplitCoordinator(placeholderCoordinator: SomeTestCoordinator())
|
||||
let sidebarCoordinator = NavigationStackCoordinator()
|
||||
sidebarCoordinator.setRootCoordinator(SomeTestCoordinator())
|
||||
@@ -295,35 +314,36 @@ class NavigationSplitCoordinatorTests: XCTestCase {
|
||||
navigationSplitCoordinator.setSidebarCoordinator(sidebarCoordinator)
|
||||
navigationSplitCoordinator.setDetailCoordinator(detailCoordinator)
|
||||
|
||||
let expectation = expectation(description: "Details coordinator should be nil, and the compact layout revert to the sidebar root")
|
||||
DispatchQueue.main.async {
|
||||
detailCoordinator.popToRoot(animated: true)
|
||||
self.navigationSplitCoordinator.setDetailCoordinator(nil)
|
||||
await waitForConfirmation("Details coordinator should be nil, and the compact layout revert to the sidebar root",
|
||||
timeout: .seconds(1)) { [navigationSplitCoordinator] confirm in
|
||||
DispatchQueue.main.async {
|
||||
XCTAssertNil(self.navigationSplitCoordinator.detailCoordinator)
|
||||
self.assertCoordinatorsEqual(self.navigationSplitCoordinator.compactLayoutRootCoordinator, sidebarCoordinator.rootCoordinator)
|
||||
XCTAssertTrue(self.navigationSplitCoordinator.compactLayoutStackModules.isEmpty)
|
||||
expectation.fulfill()
|
||||
detailCoordinator.popToRoot(animated: true)
|
||||
navigationSplitCoordinator.setDetailCoordinator(nil)
|
||||
DispatchQueue.main.async {
|
||||
#expect(navigationSplitCoordinator.detailCoordinator == nil)
|
||||
assertCoordinatorsEqual(navigationSplitCoordinator.compactLayoutRootCoordinator, sidebarCoordinator.rootCoordinator)
|
||||
#expect(navigationSplitCoordinator.compactLayoutStackModules.isEmpty)
|
||||
confirm()
|
||||
}
|
||||
}
|
||||
}
|
||||
waitForExpectations(timeout: 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func assertCoordinatorsEqual(_ lhs: CoordinatorProtocol?, _ rhs: CoordinatorProtocol?) {
|
||||
if lhs == nil, rhs == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func assertCoordinatorsEqual(_ lhs: CoordinatorProtocol?, _ rhs: CoordinatorProtocol?) {
|
||||
if lhs == nil, rhs == nil {
|
||||
return
|
||||
}
|
||||
|
||||
guard let lhs = lhs as? SomeTestCoordinator,
|
||||
let rhs = rhs as? SomeTestCoordinator else {
|
||||
XCTFail("Coordinators are not the same: \(String(describing: lhs)) != \(String(describing: rhs))")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(lhs.id, rhs.id)
|
||||
guard let lhs = lhs as? SomeTestCoordinator,
|
||||
let rhs = rhs as? SomeTestCoordinator else {
|
||||
Issue.record("Coordinators are not the same: \(String(describing: lhs)) != \(String(describing: rhs))")
|
||||
return
|
||||
}
|
||||
|
||||
#expect(lhs.id == rhs.id)
|
||||
}
|
||||
|
||||
private class SomeTestCoordinator: CoordinatorProtocol {
|
||||
|
||||
@@ -9,10 +9,11 @@
|
||||
import Combine
|
||||
@testable import ElementX
|
||||
import NotificationCenter
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@Suite
|
||||
@MainActor
|
||||
final class NotificationManagerTests: XCTestCase {
|
||||
final class NotificationManagerTests {
|
||||
var notificationManager: NotificationManager!
|
||||
private let clientProxy = ClientProxyMock(.init(userID: "@test:user.net"))
|
||||
private lazy var mockUserSession = UserSessionMock(.init(clientProxy: clientProxy))
|
||||
@@ -27,7 +28,7 @@ final class NotificationManagerTests: XCTestCase {
|
||||
ServiceLocator.shared.settings
|
||||
}
|
||||
|
||||
override func setUp() {
|
||||
init() {
|
||||
AppSettings.resetAllSettings()
|
||||
notificationCenter = UserNotificationCenterMock()
|
||||
notificationCenter.requestAuthorizationOptionsReturnValue = true
|
||||
@@ -39,80 +40,88 @@ final class NotificationManagerTests: XCTestCase {
|
||||
notificationManager.setUserSession(mockUserSession)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
deinit {
|
||||
notificationCenter = nil
|
||||
notificationManager = nil
|
||||
}
|
||||
|
||||
func test_whenRegistered_pusherIsCalled() async {
|
||||
@Test
|
||||
func whenRegistered_pusherIsCalled() async {
|
||||
_ = await notificationManager.register(with: Data())
|
||||
|
||||
XCTAssertTrue(clientProxy.setPusherWithCalled)
|
||||
#expect(clientProxy.setPusherWithCalled)
|
||||
}
|
||||
|
||||
func test_whenRegisteredSuccess_completionSuccessIsCalled() async {
|
||||
@Test
|
||||
func whenRegisteredSuccess_completionSuccessIsCalled() async {
|
||||
let success = await notificationManager.register(with: Data())
|
||||
XCTAssertTrue(success)
|
||||
#expect(success)
|
||||
}
|
||||
|
||||
func test_whenRegisteredAndPusherThrowsError_completionFalseIsCalled() async {
|
||||
@Test
|
||||
func whenRegisteredAndPusherThrowsError_completionFalseIsCalled() async {
|
||||
enum TestError: Error {
|
||||
case someError
|
||||
}
|
||||
|
||||
clientProxy.setPusherWithThrowableError = TestError.someError
|
||||
let success = await notificationManager.register(with: Data())
|
||||
XCTAssertFalse(success)
|
||||
#expect(!success)
|
||||
}
|
||||
|
||||
func test_whenRegistered_pusherIsCalledWithCorrectValues() async throws {
|
||||
@Test
|
||||
func whenRegistered_pusherIsCalledWithCorrectValues() async throws {
|
||||
let pushkeyData = Data("1234".utf8)
|
||||
_ = await notificationManager.register(with: pushkeyData)
|
||||
|
||||
guard let configuration = clientProxy.setPusherWithReceivedInvocations.first else {
|
||||
XCTFail("Invalid pusher configuration sent")
|
||||
Issue.record("Invalid pusher configuration sent")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(configuration.identifiers.pushkey, pushkeyData.base64EncodedString())
|
||||
XCTAssertEqual(configuration.identifiers.appId, appSettings.pusherAppID)
|
||||
XCTAssertEqual(configuration.appDisplayName, "\(InfoPlistReader.main.bundleDisplayName) (iOS)")
|
||||
XCTAssertEqual(configuration.deviceDisplayName, UIDevice.current.name)
|
||||
XCTAssertNotNil(configuration.profileTag)
|
||||
XCTAssertEqual(configuration.lang, Bundle.app.preferredLocalizations.first)
|
||||
#expect(configuration.identifiers.pushkey == pushkeyData.base64EncodedString())
|
||||
#expect(configuration.identifiers.appId == appSettings.pusherAppID)
|
||||
#expect(configuration.appDisplayName == "\(InfoPlistReader.main.bundleDisplayName) (iOS)")
|
||||
#expect(configuration.deviceDisplayName == UIDevice.current.name)
|
||||
#expect(configuration.profileTag != nil)
|
||||
#expect(configuration.lang == Bundle.app.preferredLocalizations.first)
|
||||
guard case let .http(data) = configuration.kind else {
|
||||
XCTFail("Http kind expected")
|
||||
Issue.record("Http kind expected")
|
||||
return
|
||||
}
|
||||
XCTAssertEqual(data.url, appSettings.pushGatewayNotifyEndpoint.absoluteString)
|
||||
XCTAssertEqual(data.format, .eventIdOnly)
|
||||
#expect(data.url == appSettings.pushGatewayNotifyEndpoint.absoluteString)
|
||||
#expect(data.format == .eventIdOnly)
|
||||
let defaultPayload = APNSPayload(aps: APSInfo(mutableContent: 1,
|
||||
alert: APSAlert(locKey: "Notification",
|
||||
locArgs: [])),
|
||||
pusherNotificationClientIdentifier: nil)
|
||||
XCTAssertEqual(data.defaultPayload, try defaultPayload.toJsonString())
|
||||
#expect(try data.defaultPayload == (defaultPayload.toJsonString()))
|
||||
}
|
||||
|
||||
func test_whenRegisteredAndPusherTagNotSetInSettings_tagGeneratedAndSavedInSettings() async {
|
||||
@Test
|
||||
func whenRegisteredAndPusherTagNotSetInSettings_tagGeneratedAndSavedInSettings() async {
|
||||
appSettings.pusherProfileTag = nil
|
||||
_ = await notificationManager.register(with: Data())
|
||||
XCTAssertNotNil(appSettings.pusherProfileTag)
|
||||
#expect(appSettings.pusherProfileTag != nil)
|
||||
}
|
||||
|
||||
func test_whenRegisteredAndPusherTagIsSetInSettings_tagNotGenerated() async {
|
||||
@Test
|
||||
func whenRegisteredAndPusherTagIsSetInSettings_tagNotGenerated() async {
|
||||
appSettings.pusherProfileTag = "12345"
|
||||
_ = await notificationManager.register(with: Data())
|
||||
XCTAssertEqual(appSettings.pusherProfileTag, "12345")
|
||||
#expect(appSettings.pusherProfileTag == "12345")
|
||||
}
|
||||
|
||||
func test_whenShowLocalNotification_notificationRequestGetsAdded() async throws {
|
||||
@Test
|
||||
func whenShowLocalNotification_notificationRequestGetsAdded() async throws {
|
||||
await notificationManager.showLocalNotification(with: "Title", subtitle: "Subtitle")
|
||||
let request = try XCTUnwrap(notificationCenter.addReceivedRequest)
|
||||
XCTAssertEqual(request.content.title, "Title")
|
||||
XCTAssertEqual(request.content.subtitle, "Subtitle")
|
||||
let request = try #require(notificationCenter.addReceivedRequest)
|
||||
#expect(request.content.title == "Title")
|
||||
#expect(request.content.subtitle == "Subtitle")
|
||||
}
|
||||
|
||||
func test_whenStart_notificationCategoriesAreSet() {
|
||||
@Test
|
||||
func whenStart_notificationCategoriesAreSet() {
|
||||
let replyAction = UNTextInputNotificationAction(identifier: NotificationConstants.Action.inlineReply,
|
||||
title: L10n.actionQuickReply,
|
||||
options: [])
|
||||
@@ -125,39 +134,42 @@ final class NotificationManagerTests: XCTestCase {
|
||||
actions: [],
|
||||
intentIdentifiers: [],
|
||||
options: [])
|
||||
XCTAssertEqual(notificationCenter.setNotificationCategoriesReceivedCategories, [messageCategory, inviteCategory])
|
||||
#expect(notificationCenter.setNotificationCategoriesReceivedCategories == [messageCategory, inviteCategory])
|
||||
}
|
||||
|
||||
func test_whenStart_delegateIsSet() throws {
|
||||
let delegate = try XCTUnwrap(notificationCenter.delegate)
|
||||
XCTAssertTrue(delegate.isEqual(notificationManager))
|
||||
@Test
|
||||
func whenStart_delegateIsSet() throws {
|
||||
let delegate = try #require(notificationCenter.delegate)
|
||||
#expect(delegate.isEqual(notificationManager))
|
||||
}
|
||||
|
||||
func test_whenStart_requestAuthorizationCalledWithCorrectParams() async {
|
||||
let expectation = expectation(description: "requestAuthorization should be called")
|
||||
notificationCenter.requestAuthorizationOptionsClosure = { _ in
|
||||
expectation.fulfill()
|
||||
return true
|
||||
@Test
|
||||
func whenStart_requestAuthorizationCalledWithCorrectParams() async {
|
||||
await waitForConfirmation("requestAuthorization should be called", timeout: .seconds(10)) { confirm in
|
||||
notificationCenter.requestAuthorizationOptionsClosure = { _ in
|
||||
confirm()
|
||||
return true
|
||||
}
|
||||
notificationManager.requestAuthorization()
|
||||
}
|
||||
notificationManager.requestAuthorization()
|
||||
await fulfillment(of: [expectation])
|
||||
XCTAssertEqual(notificationCenter.requestAuthorizationOptionsReceivedOptions, [.alert, .sound, .badge])
|
||||
#expect(notificationCenter.requestAuthorizationOptionsReceivedOptions == [.alert, .sound, .badge])
|
||||
}
|
||||
|
||||
func test_whenStartAndAuthorizationGranted_delegateCalled() async {
|
||||
@Test
|
||||
func whenStartAndAuthorizationGranted_delegateCalled() async {
|
||||
authorizationStatusWasGranted = false
|
||||
notificationManager.delegate = self
|
||||
let expectation: XCTestExpectation = expectation(description: "registerForRemoteNotifications delegate function should be called")
|
||||
expectation.assertForOverFulfill = false
|
||||
registerForRemoteNotificationsDelegateCalled = {
|
||||
expectation.fulfill()
|
||||
await waitForConfirmation("registerForRemoteNotifications delegate function should be called", timeout: .seconds(10)) { confirm in
|
||||
registerForRemoteNotificationsDelegateCalled = {
|
||||
confirm()
|
||||
}
|
||||
notificationManager.requestAuthorization()
|
||||
}
|
||||
notificationManager.requestAuthorization()
|
||||
await fulfillment(of: [expectation])
|
||||
XCTAssertTrue(authorizationStatusWasGranted)
|
||||
#expect(authorizationStatusWasGranted)
|
||||
}
|
||||
|
||||
func test_whenStartAndAuthorizedAndNotificationDisabled_registerForRemoteNotificationsNotCalled() async throws {
|
||||
@Test
|
||||
func whenStartAndAuthorizedAndNotificationDisabled_registerForRemoteNotificationsNotCalled() async throws {
|
||||
appSettings.enableNotifications = false
|
||||
notificationCenter.authorizationStatusReturnValue = .authorized
|
||||
notificationManager.delegate = self
|
||||
@@ -165,65 +177,69 @@ final class NotificationManagerTests: XCTestCase {
|
||||
notificationManager.setUserSession(UserSessionMock(.init()))
|
||||
try await Task.sleep(for: .seconds(1))
|
||||
|
||||
XCTAssertFalse(authorizationStatusWasGranted)
|
||||
#expect(!authorizationStatusWasGranted)
|
||||
}
|
||||
|
||||
func test_whenStartAndAuthorized_registerForRemoteNotificationsCalled() async {
|
||||
@Test
|
||||
func whenStartAndAuthorized_registerForRemoteNotificationsCalled() async {
|
||||
appSettings.enableNotifications = true
|
||||
notificationCenter.authorizationStatusReturnValue = .authorized
|
||||
notificationManager.delegate = self
|
||||
|
||||
let expectation: XCTestExpectation = expectation(description: "registerForRemoteNotifications delegate function should be called")
|
||||
expectation.assertForOverFulfill = false
|
||||
registerForRemoteNotificationsDelegateCalled = {
|
||||
expectation.fulfill()
|
||||
await waitForConfirmation("registerForRemoteNotifications delegate function should be called", timeout: .seconds(10)) { confirm in
|
||||
registerForRemoteNotificationsDelegateCalled = {
|
||||
confirm()
|
||||
}
|
||||
notificationManager.setUserSession(UserSessionMock(.init()))
|
||||
}
|
||||
|
||||
notificationManager.setUserSession(UserSessionMock(.init()))
|
||||
await fulfillment(of: [expectation])
|
||||
|
||||
XCTAssertTrue(authorizationStatusWasGranted)
|
||||
#expect(authorizationStatusWasGranted)
|
||||
}
|
||||
|
||||
func test_whenWillPresentNotificationsDelegateNotSet_CorrectPresentationOptionsReturned() async throws {
|
||||
@Test
|
||||
func whenWillPresentNotificationsDelegateNotSet_CorrectPresentationOptionsReturned() async throws {
|
||||
let archiver = MockCoder(requiringSecureCoding: false)
|
||||
let notification = try XCTUnwrap(UNNotification(coder: archiver))
|
||||
let notification = try #require(UNNotification(coder: archiver))
|
||||
let options = await notificationManager.userNotificationCenter(UNUserNotificationCenter.current(), willPresent: notification)
|
||||
XCTAssertEqual(options, [.badge, .sound, .list, .banner])
|
||||
#expect(options == [.badge, .sound, .list, .banner])
|
||||
}
|
||||
|
||||
func test_whenWillPresentNotificationsDelegateSetAndNotificationsShoudNotBeDisplayed_CorrectPresentationOptionsReturned() async throws {
|
||||
@Test
|
||||
func whenWillPresentNotificationsDelegateSetAndNotificationsShoudNotBeDisplayed_CorrectPresentationOptionsReturned() async throws {
|
||||
shouldDisplayInAppNotificationReturnValue = false
|
||||
notificationManager.delegate = self
|
||||
|
||||
let notification = try UNNotification.with(userInfo: [AnyHashable: Any]())
|
||||
let options = await notificationManager.userNotificationCenter(UNUserNotificationCenter.current(), willPresent: notification)
|
||||
XCTAssertEqual(options, [])
|
||||
#expect(options == [])
|
||||
}
|
||||
|
||||
func test_whenWillPresentNotificationsDelegateSetAndNotificationsShoudBeDisplayed_CorrectPresentationOptionsReturned() async throws {
|
||||
@Test
|
||||
func whenWillPresentNotificationsDelegateSetAndNotificationsShoudBeDisplayed_CorrectPresentationOptionsReturned() async throws {
|
||||
shouldDisplayInAppNotificationReturnValue = true
|
||||
notificationManager.delegate = self
|
||||
|
||||
let notification = try UNNotification.with(userInfo: [AnyHashable: Any]())
|
||||
let options = await notificationManager.userNotificationCenter(UNUserNotificationCenter.current(), willPresent: notification)
|
||||
XCTAssertEqual(options, [.badge, .sound, .list, .banner])
|
||||
#expect(options == [.badge, .sound, .list, .banner])
|
||||
}
|
||||
|
||||
func test_whenNotificationCenterReceivedResponseInLineReply_delegateIsCalled() async throws {
|
||||
@Test
|
||||
func whenNotificationCenterReceivedResponseInLineReply_delegateIsCalled() async throws {
|
||||
handleInlineReplyDelegateCalled = false
|
||||
notificationManager.delegate = self
|
||||
let response = try UNTextInputNotificationResponse.with(userInfo: [AnyHashable: Any](), actionIdentifier: NotificationConstants.Action.inlineReply)
|
||||
await notificationManager.userNotificationCenter(UNUserNotificationCenter.current(), didReceive: response)
|
||||
XCTAssertTrue(handleInlineReplyDelegateCalled)
|
||||
#expect(handleInlineReplyDelegateCalled)
|
||||
}
|
||||
|
||||
func test_whenNotificationCenterReceivedResponseWithActionIdentifier_delegateIsCalled() async throws {
|
||||
@Test
|
||||
func whenNotificationCenterReceivedResponseWithActionIdentifier_delegateIsCalled() async throws {
|
||||
notificationTappedDelegateCalled = false
|
||||
notificationManager.delegate = self
|
||||
let response = try UNTextInputNotificationResponse.with(userInfo: [AnyHashable: Any](), actionIdentifier: UNNotificationDefaultActionIdentifier)
|
||||
await notificationManager.userNotificationCenter(UNUserNotificationCenter.current(), didReceive: response)
|
||||
XCTAssertTrue(notificationTappedDelegateCalled)
|
||||
#expect(notificationTappedDelegateCalled)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,10 +8,11 @@
|
||||
|
||||
@testable import ElementX
|
||||
import MatrixRustSDK
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@Suite
|
||||
@MainActor
|
||||
class NotificationSettingsEditScreenViewModelTests: XCTestCase {
|
||||
struct NotificationSettingsEditScreenViewModelTests {
|
||||
private var viewModel: NotificationSettingsEditScreenViewModelProtocol!
|
||||
private var notificationSettingsProxy: NotificationSettingsProxyMock!
|
||||
private var userSession: UserSessionMock!
|
||||
@@ -21,7 +22,7 @@ class NotificationSettingsEditScreenViewModelTests: XCTestCase {
|
||||
viewModel.context
|
||||
}
|
||||
|
||||
@MainActor override func setUpWithError() throws {
|
||||
init() throws {
|
||||
notificationSettingsProxy = NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration())
|
||||
notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneReturnValue = .allMessages
|
||||
|
||||
@@ -29,7 +30,8 @@ class NotificationSettingsEditScreenViewModelTests: XCTestCase {
|
||||
userSession = UserSessionMock(.init(clientProxy: clientProxy))
|
||||
}
|
||||
|
||||
func testFetchSettings() async throws {
|
||||
@Test
|
||||
mutating func fetchSettings() async throws {
|
||||
notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneClosure = { isEncrypted, isOneToOne in
|
||||
switch (isEncrypted, isOneToOne) {
|
||||
case (_, true):
|
||||
@@ -49,21 +51,22 @@ class NotificationSettingsEditScreenViewModelTests: XCTestCase {
|
||||
// `getDefaultRoomNotificationModeIsEncryptedIsOneToOne` must have been called twice (for encrypted and unencrypted group chats)
|
||||
let invocations = notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneReceivedInvocations
|
||||
|
||||
XCTAssertEqual(invocations.count, 2)
|
||||
#expect(invocations.count == 2)
|
||||
// First call for encrypted group chats
|
||||
XCTAssertEqual(invocations[0].isEncrypted, true)
|
||||
XCTAssertEqual(invocations[0].isOneToOne, false)
|
||||
#expect(invocations[0].isEncrypted == true)
|
||||
#expect(invocations[0].isOneToOne == false)
|
||||
// Second call for unencrypted group chats
|
||||
XCTAssertEqual(invocations[1].isEncrypted, false)
|
||||
XCTAssertEqual(invocations[1].isOneToOne, false)
|
||||
#expect(invocations[1].isEncrypted == false)
|
||||
#expect(invocations[1].isOneToOne == false)
|
||||
|
||||
XCTAssertEqual(context.viewState.defaultMode, .mentionsAndKeywordsOnly)
|
||||
XCTAssertNil(context.viewState.bindings.alertInfo)
|
||||
XCTAssertFalse(context.viewState.canPushEncryptedEvents)
|
||||
XCTAssertNotNil(context.viewState.description(for: .mentionsAndKeywordsOnly))
|
||||
#expect(context.viewState.defaultMode == .mentionsAndKeywordsOnly)
|
||||
#expect(context.viewState.bindings.alertInfo == nil)
|
||||
#expect(!context.viewState.canPushEncryptedEvents)
|
||||
#expect(context.viewState.description(for: .mentionsAndKeywordsOnly) != nil)
|
||||
}
|
||||
|
||||
func testFetchSettingsWithCanPushEncryptedEvents() async throws {
|
||||
@Test
|
||||
mutating func fetchSettingsWithCanPushEncryptedEvents() async throws {
|
||||
notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneClosure = { isEncrypted, isOneToOne in
|
||||
switch (isEncrypted, isOneToOne) {
|
||||
case (_, true):
|
||||
@@ -86,21 +89,22 @@ class NotificationSettingsEditScreenViewModelTests: XCTestCase {
|
||||
// `getDefaultRoomNotificationModeIsEncryptedIsOneToOne` must have been called twice (for encrypted and unencrypted group chats)
|
||||
let invocations = notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneReceivedInvocations
|
||||
|
||||
XCTAssertEqual(invocations.count, 2)
|
||||
#expect(invocations.count == 2)
|
||||
// First call for encrypted group chats
|
||||
XCTAssertEqual(invocations[0].isEncrypted, true)
|
||||
XCTAssertEqual(invocations[0].isOneToOne, false)
|
||||
#expect(invocations[0].isEncrypted == true)
|
||||
#expect(invocations[0].isOneToOne == false)
|
||||
// Second call for unencrypted group chats
|
||||
XCTAssertEqual(invocations[1].isEncrypted, false)
|
||||
XCTAssertEqual(invocations[1].isOneToOne, false)
|
||||
#expect(invocations[1].isEncrypted == false)
|
||||
#expect(invocations[1].isOneToOne == false)
|
||||
|
||||
XCTAssertEqual(context.viewState.defaultMode, .mentionsAndKeywordsOnly)
|
||||
XCTAssertNil(context.viewState.bindings.alertInfo)
|
||||
XCTAssertTrue(context.viewState.canPushEncryptedEvents)
|
||||
XCTAssertNil(context.viewState.description(for: .mentionsAndKeywordsOnly))
|
||||
#expect(context.viewState.defaultMode == .mentionsAndKeywordsOnly)
|
||||
#expect(context.viewState.bindings.alertInfo == nil)
|
||||
#expect(context.viewState.canPushEncryptedEvents)
|
||||
#expect(context.viewState.description(for: .mentionsAndKeywordsOnly) == nil)
|
||||
}
|
||||
|
||||
func testSetModeAllMessages() async throws {
|
||||
@Test
|
||||
mutating func setModeAllMessages() async throws {
|
||||
notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneReturnValue = .mentionsAndKeywordsOnly
|
||||
viewModel = NotificationSettingsEditScreenViewModel(chatType: .groupChat, userSession: userSession)
|
||||
let deferred = deferFulfillment(viewModel.context.observe(\.viewState.defaultMode)) { $0 != nil }
|
||||
@@ -118,26 +122,27 @@ class NotificationSettingsEditScreenViewModelTests: XCTestCase {
|
||||
|
||||
// `setDefaultRoomNotificationModeIsEncryptedIsOneToOneMode` must have been called twice (for encrypted and unencrypted group chats)
|
||||
let invocations = notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeReceivedInvocations
|
||||
XCTAssertEqual(notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeCallsCount, 2)
|
||||
#expect(notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeCallsCount == 2)
|
||||
// First call for encrypted group chats
|
||||
XCTAssertEqual(invocations[0].isEncrypted, true)
|
||||
XCTAssertEqual(invocations[0].isOneToOne, false)
|
||||
XCTAssertEqual(invocations[0].mode, .allMessages)
|
||||
#expect(invocations[0].isEncrypted == true)
|
||||
#expect(invocations[0].isOneToOne == false)
|
||||
#expect(invocations[0].mode == .allMessages)
|
||||
// Second call for unencrypted group chats
|
||||
XCTAssertEqual(invocations[1].isEncrypted, false)
|
||||
XCTAssertEqual(invocations[1].isOneToOne, false)
|
||||
XCTAssertEqual(invocations[1].mode, .allMessages)
|
||||
#expect(invocations[1].isEncrypted == false)
|
||||
#expect(invocations[1].isOneToOne == false)
|
||||
#expect(invocations[1].mode == .allMessages)
|
||||
|
||||
deferredViewState = deferFulfillment(viewModel.context.observe(\.viewState.defaultMode),
|
||||
transitionValues: [.allMessages])
|
||||
|
||||
try await deferredViewState.fulfill()
|
||||
|
||||
XCTAssertEqual(context.viewState.defaultMode, .allMessages)
|
||||
XCTAssertNil(context.viewState.bindings.alertInfo)
|
||||
#expect(context.viewState.defaultMode == .allMessages)
|
||||
#expect(context.viewState.bindings.alertInfo == nil)
|
||||
}
|
||||
|
||||
func testSetModeMentions() async throws {
|
||||
@Test
|
||||
mutating func setModeMentions() async throws {
|
||||
viewModel = NotificationSettingsEditScreenViewModel(chatType: .groupChat, userSession: userSession)
|
||||
|
||||
let deferred = deferFulfillment(viewModel.context.observe(\.viewState.defaultMode)) { $0 != nil }
|
||||
@@ -155,26 +160,27 @@ class NotificationSettingsEditScreenViewModelTests: XCTestCase {
|
||||
|
||||
// `setDefaultRoomNotificationModeIsEncryptedIsOneToOneMode` must have been called twice (for encrypted and unencrypted group chats)
|
||||
let invocations = notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeReceivedInvocations
|
||||
XCTAssertEqual(notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeCallsCount, 2)
|
||||
#expect(notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeCallsCount == 2)
|
||||
// First call for encrypted group chats
|
||||
XCTAssertEqual(invocations[0].isEncrypted, true)
|
||||
XCTAssertEqual(invocations[0].isOneToOne, false)
|
||||
XCTAssertEqual(invocations[0].mode, .mentionsAndKeywordsOnly)
|
||||
#expect(invocations[0].isEncrypted == true)
|
||||
#expect(invocations[0].isOneToOne == false)
|
||||
#expect(invocations[0].mode == .mentionsAndKeywordsOnly)
|
||||
// Second call for unencrypted group chats
|
||||
XCTAssertEqual(invocations[1].isEncrypted, false)
|
||||
XCTAssertEqual(invocations[1].isOneToOne, false)
|
||||
XCTAssertEqual(invocations[1].mode, .mentionsAndKeywordsOnly)
|
||||
#expect(invocations[1].isEncrypted == false)
|
||||
#expect(invocations[1].isOneToOne == false)
|
||||
#expect(invocations[1].mode == .mentionsAndKeywordsOnly)
|
||||
|
||||
deferredViewState = deferFulfillment(viewModel.context.observe(\.viewState.defaultMode),
|
||||
transitionValues: [.mentionsAndKeywordsOnly])
|
||||
|
||||
try await deferredViewState.fulfill()
|
||||
|
||||
XCTAssertEqual(context.viewState.defaultMode, .mentionsAndKeywordsOnly)
|
||||
XCTAssertNil(context.viewState.bindings.alertInfo)
|
||||
#expect(context.viewState.defaultMode == .mentionsAndKeywordsOnly)
|
||||
#expect(context.viewState.bindings.alertInfo == nil)
|
||||
}
|
||||
|
||||
func testSetModeDirectChats() async throws {
|
||||
@Test
|
||||
mutating func setModeDirectChats() async throws {
|
||||
notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneReturnValue = .mentionsAndKeywordsOnly
|
||||
// Initialize for direct chats
|
||||
viewModel = NotificationSettingsEditScreenViewModel(chatType: .oneToOneChat, userSession: userSession)
|
||||
@@ -194,18 +200,19 @@ class NotificationSettingsEditScreenViewModelTests: XCTestCase {
|
||||
|
||||
// `setDefaultRoomNotificationModeIsEncryptedIsOneToOneMode` must have been called twice (for encrypted and unencrypted direct chats)
|
||||
let invocations = notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeReceivedInvocations
|
||||
XCTAssertEqual(notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeCallsCount, 2)
|
||||
#expect(notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeCallsCount == 2)
|
||||
// First call for encrypted direct chats
|
||||
XCTAssertEqual(invocations[0].isEncrypted, true)
|
||||
XCTAssertEqual(invocations[0].isOneToOne, true)
|
||||
XCTAssertEqual(invocations[0].mode, .allMessages)
|
||||
#expect(invocations[0].isEncrypted == true)
|
||||
#expect(invocations[0].isOneToOne == true)
|
||||
#expect(invocations[0].mode == .allMessages)
|
||||
// Second call for unencrypted direct chats
|
||||
XCTAssertEqual(invocations[1].isEncrypted, false)
|
||||
XCTAssertEqual(invocations[1].isOneToOne, true)
|
||||
XCTAssertEqual(invocations[1].mode, .allMessages)
|
||||
#expect(invocations[1].isEncrypted == false)
|
||||
#expect(invocations[1].isOneToOne == true)
|
||||
#expect(invocations[1].mode == .allMessages)
|
||||
}
|
||||
|
||||
func testSetModeFailure() async throws {
|
||||
@Test
|
||||
mutating func setModeFailure() async throws {
|
||||
notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneReturnValue = .mentionsAndKeywordsOnly
|
||||
notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeThrowableError = NotificationSettingsError.Generic(msg: "error")
|
||||
viewModel = NotificationSettingsEditScreenViewModel(chatType: .oneToOneChat, userSession: userSession)
|
||||
@@ -223,10 +230,11 @@ class NotificationSettingsEditScreenViewModelTests: XCTestCase {
|
||||
|
||||
try await deferredViewState.fulfill()
|
||||
|
||||
XCTAssertNotNil(context.viewState.bindings.alertInfo)
|
||||
#expect(context.viewState.bindings.alertInfo != nil)
|
||||
}
|
||||
|
||||
func testSelectRoom() async throws {
|
||||
@Test
|
||||
mutating func selectRoom() async throws {
|
||||
let roomID = "!roomidentifier:matrix.org"
|
||||
viewModel = NotificationSettingsEditScreenViewModel(chatType: .oneToOneChat, userSession: userSession)
|
||||
|
||||
@@ -243,7 +251,7 @@ class NotificationSettingsEditScreenViewModelTests: XCTestCase {
|
||||
|
||||
let expectedAction = NotificationSettingsEditScreenViewModelAction.requestRoomNotificationSettingsPresentation(roomID: roomID)
|
||||
guard case let .requestRoomNotificationSettingsPresentation(roomID: receivedRoomID) = sentAction, receivedRoomID == roomID else {
|
||||
XCTFail("Expected action \(expectedAction), but was \(sentAction)")
|
||||
Issue.record("Expected action \(expectedAction), but was \(sentAction)")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,17 +8,18 @@
|
||||
|
||||
@testable import ElementX
|
||||
import MatrixRustSDK
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@Suite
|
||||
@MainActor
|
||||
class NotificationSettingsScreenViewModelTests: XCTestCase {
|
||||
private var viewModel: NotificationSettingsScreenViewModelProtocol!
|
||||
private var context: NotificationSettingsScreenViewModelType.Context!
|
||||
private var appSettings: AppSettings!
|
||||
private var userNotificationCenter: UserNotificationCenterMock!
|
||||
private var notificationSettingsProxy: NotificationSettingsProxyMock!
|
||||
struct NotificationSettingsScreenViewModelTests {
|
||||
private var viewModel: NotificationSettingsScreenViewModelProtocol
|
||||
private var context: NotificationSettingsScreenViewModelType.Context
|
||||
private var appSettings: AppSettings
|
||||
private var userNotificationCenter: UserNotificationCenterMock
|
||||
private var notificationSettingsProxy: NotificationSettingsProxyMock
|
||||
|
||||
@MainActor override func setUpWithError() throws {
|
||||
init() throws {
|
||||
AppSettings.resetAllSettings()
|
||||
|
||||
userNotificationCenter = UserNotificationCenterMock()
|
||||
@@ -36,19 +37,22 @@ class NotificationSettingsScreenViewModelTests: XCTestCase {
|
||||
context = viewModel.context
|
||||
}
|
||||
|
||||
func testEnableNotifications() {
|
||||
@Test
|
||||
func enableNotifications() {
|
||||
appSettings.enableNotifications = false
|
||||
context.send(viewAction: .changedEnableNotifications)
|
||||
XCTAssertTrue(appSettings.enableNotifications)
|
||||
#expect(appSettings.enableNotifications)
|
||||
}
|
||||
|
||||
func testDisableNotifications() {
|
||||
@Test
|
||||
func disableNotifications() {
|
||||
appSettings.enableNotifications = true
|
||||
context.send(viewAction: .changedEnableNotifications)
|
||||
XCTAssertFalse(appSettings.enableNotifications)
|
||||
#expect(!appSettings.enableNotifications)
|
||||
}
|
||||
|
||||
func testFetchSettings() async throws {
|
||||
@Test
|
||||
func fetchSettings() async throws {
|
||||
notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneClosure = { isEncrypted, isOneToOne in
|
||||
switch (isEncrypted, isOneToOne) {
|
||||
case (_, true):
|
||||
@@ -64,17 +68,18 @@ class NotificationSettingsScreenViewModelTests: XCTestCase {
|
||||
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertEqual(notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneCallsCount, 4)
|
||||
XCTAssert(notificationSettingsProxy.isRoomMentionEnabledCalled)
|
||||
XCTAssert(notificationSettingsProxy.isCallEnabledCalled)
|
||||
#expect(notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneCallsCount == 4)
|
||||
#expect(notificationSettingsProxy.isRoomMentionEnabledCalled)
|
||||
#expect(notificationSettingsProxy.isCallEnabledCalled)
|
||||
|
||||
XCTAssertEqual(context.viewState.settings?.groupChatsMode, .mentionsAndKeywordsOnly)
|
||||
XCTAssertEqual(context.viewState.settings?.directChatsMode, .allMessages)
|
||||
XCTAssertEqual(context.viewState.settings?.inconsistentSettings, [])
|
||||
XCTAssertNil(context.viewState.bindings.alertInfo)
|
||||
#expect(context.viewState.settings?.groupChatsMode == .mentionsAndKeywordsOnly)
|
||||
#expect(context.viewState.settings?.directChatsMode == .allMessages)
|
||||
#expect(context.viewState.settings?.inconsistentSettings == [])
|
||||
#expect(context.viewState.bindings.alertInfo == nil)
|
||||
}
|
||||
|
||||
func testInconsistentGroupChatsSettings() async throws {
|
||||
@Test
|
||||
func inconsistentGroupChatsSettings() async throws {
|
||||
notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneClosure = { isEncrypted, isOneToOne in
|
||||
switch (isEncrypted, isOneToOne) {
|
||||
case (true, false):
|
||||
@@ -92,11 +97,12 @@ class NotificationSettingsScreenViewModelTests: XCTestCase {
|
||||
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertEqual(context.viewState.settings?.groupChatsMode, .allMessages)
|
||||
XCTAssertEqual(context.viewState.settings?.inconsistentSettings, [.init(chatType: .groupChat, isEncrypted: false)])
|
||||
#expect(context.viewState.settings?.groupChatsMode == .allMessages)
|
||||
#expect(context.viewState.settings?.inconsistentSettings == [.init(chatType: .groupChat, isEncrypted: false)])
|
||||
}
|
||||
|
||||
func testInconsistentDirectChatsSettings() async throws {
|
||||
@Test
|
||||
func inconsistentDirectChatsSettings() async throws {
|
||||
notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneClosure = { isEncrypted, isOneToOne in
|
||||
switch (isEncrypted, isOneToOne) {
|
||||
case (true, true):
|
||||
@@ -114,11 +120,12 @@ class NotificationSettingsScreenViewModelTests: XCTestCase {
|
||||
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertEqual(context.viewState.settings?.directChatsMode, .allMessages)
|
||||
XCTAssertEqual(context.viewState.settings?.inconsistentSettings, [.init(chatType: .oneToOneChat, isEncrypted: false)])
|
||||
#expect(context.viewState.settings?.directChatsMode == .allMessages)
|
||||
#expect(context.viewState.settings?.inconsistentSettings == [.init(chatType: .oneToOneChat, isEncrypted: false)])
|
||||
}
|
||||
|
||||
func testFixInconsistentSettings() async throws {
|
||||
@Test
|
||||
func fixInconsistentSettings() async throws {
|
||||
// Initialize with a configuration mismatch where encrypted one-to-one chats is `.allMessages` and unencrypted one-to-one chats is `.mentionsAndKeywordsOnly`
|
||||
notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneClosure = { isEncrypted, isOneToOne in
|
||||
switch (isEncrypted, isOneToOne) {
|
||||
@@ -137,8 +144,8 @@ class NotificationSettingsScreenViewModelTests: XCTestCase {
|
||||
|
||||
try await deferredSettings.fulfill()
|
||||
|
||||
XCTAssertEqual(context.viewState.settings?.directChatsMode, .allMessages)
|
||||
XCTAssertEqual(context.viewState.settings?.inconsistentSettings, [.init(chatType: .oneToOneChat, isEncrypted: false)])
|
||||
#expect(context.viewState.settings?.directChatsMode == .allMessages)
|
||||
#expect(context.viewState.settings?.inconsistentSettings == [.init(chatType: .oneToOneChat, isEncrypted: false)])
|
||||
|
||||
let deferredMismatch = deferFulfillment(viewModel.context.observe(\.viewState.fixingConfigurationMismatch),
|
||||
transitionValues: [false, true, false])
|
||||
@@ -148,14 +155,15 @@ class NotificationSettingsScreenViewModelTests: XCTestCase {
|
||||
try await deferredMismatch.fulfill()
|
||||
|
||||
// Ensure we only fix the invalid setting: unencrypted one-to-one chats should be set to `.allMessages` (to match encrypted one-to-one chats)
|
||||
XCTAssertEqual(notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeCallsCount, 1)
|
||||
#expect(notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeCallsCount == 1)
|
||||
let callArguments = notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeReceivedArguments
|
||||
XCTAssertEqual(callArguments?.isEncrypted, false)
|
||||
XCTAssertEqual(callArguments?.isOneToOne, true)
|
||||
XCTAssertEqual(callArguments?.mode, .allMessages)
|
||||
#expect(callArguments?.isEncrypted == false)
|
||||
#expect(callArguments?.isOneToOne == true)
|
||||
#expect(callArguments?.mode == .allMessages)
|
||||
}
|
||||
|
||||
func testFixAllInconsistentSettings() async throws {
|
||||
@Test
|
||||
func fixAllInconsistentSettings() async throws {
|
||||
// Initialize with a configuration mismatch where
|
||||
// - encrypted one-to-one chats is `.allMessages` and unencrypted one-to-one chats is `.mentionsAndKeywordsOnly`
|
||||
// - encrypted group chats is `.allMessages` and unencrypted group chats is `.mentionsAndKeywordsOnly`
|
||||
@@ -174,8 +182,8 @@ class NotificationSettingsScreenViewModelTests: XCTestCase {
|
||||
|
||||
try await deferredSettings.fulfill()
|
||||
|
||||
XCTAssertEqual(context.viewState.settings?.directChatsMode, .allMessages)
|
||||
XCTAssertEqual(context.viewState.settings?.inconsistentSettings, [.init(chatType: .groupChat, isEncrypted: false), .init(chatType: .oneToOneChat, isEncrypted: false)])
|
||||
#expect(context.viewState.settings?.directChatsMode == .allMessages)
|
||||
#expect(context.viewState.settings?.inconsistentSettings == [.init(chatType: .groupChat, isEncrypted: false), .init(chatType: .oneToOneChat, isEncrypted: false)])
|
||||
|
||||
var deferredMismatch = deferFulfillment(viewModel.context.observe(\.viewState.fixingConfigurationMismatch)) { $0 }
|
||||
|
||||
@@ -188,19 +196,20 @@ class NotificationSettingsScreenViewModelTests: XCTestCase {
|
||||
try await deferredMismatch.fulfill()
|
||||
|
||||
// All problems should be fixed
|
||||
XCTAssertEqual(notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeCallsCount, 2)
|
||||
#expect(notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeCallsCount == 2)
|
||||
let callArguments = notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeReceivedInvocations
|
||||
// Ensure we fix the invalid unencrypted group chats setting (it should be set to `.allMessages` to match encrypted group chats)
|
||||
XCTAssertEqual(callArguments[0].isEncrypted, false)
|
||||
XCTAssertEqual(callArguments[0].isOneToOne, false)
|
||||
XCTAssertEqual(callArguments[0].mode, .allMessages)
|
||||
#expect(callArguments[0].isEncrypted == false)
|
||||
#expect(callArguments[0].isOneToOne == false)
|
||||
#expect(callArguments[0].mode == .allMessages)
|
||||
// Ensure we fix the invalid unencrypted one-to-one chats setting (it should be set to `.allMessages` to match encrypted one-to-one chats)
|
||||
XCTAssertEqual(callArguments[1].isEncrypted, false)
|
||||
XCTAssertEqual(callArguments[1].isOneToOne, true)
|
||||
XCTAssertEqual(callArguments[1].mode, .allMessages)
|
||||
#expect(callArguments[1].isEncrypted == false)
|
||||
#expect(callArguments[1].isOneToOne == true)
|
||||
#expect(callArguments[1].mode == .allMessages)
|
||||
}
|
||||
|
||||
func testToggleRoomMentionOff() async throws {
|
||||
@Test
|
||||
func toggleRoomMentionOff() async throws {
|
||||
notificationSettingsProxy.isRoomMentionEnabledReturnValue = true
|
||||
|
||||
let deferredState = deferFulfillment(viewModel.context.observe(\.viewState.settings)) { $0 != nil }
|
||||
@@ -219,11 +228,12 @@ class NotificationSettingsScreenViewModelTests: XCTestCase {
|
||||
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssert(notificationSettingsProxy.setRoomMentionEnabledEnabledCalled)
|
||||
XCTAssertEqual(notificationSettingsProxy.setRoomMentionEnabledEnabledReceivedEnabled, false)
|
||||
#expect(notificationSettingsProxy.setRoomMentionEnabledEnabledCalled)
|
||||
#expect(notificationSettingsProxy.setRoomMentionEnabledEnabledReceivedEnabled == false)
|
||||
}
|
||||
|
||||
func testToggleRoomMentionOn() async throws {
|
||||
@Test
|
||||
func toggleRoomMentionOn() async throws {
|
||||
notificationSettingsProxy.isRoomMentionEnabledReturnValue = false
|
||||
|
||||
let deferredInitialFetch = deferFulfillment(viewModel.context.observe(\.viewState.settings)) { $0 != nil }
|
||||
@@ -241,11 +251,12 @@ class NotificationSettingsScreenViewModelTests: XCTestCase {
|
||||
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssert(notificationSettingsProxy.setRoomMentionEnabledEnabledCalled)
|
||||
XCTAssertEqual(notificationSettingsProxy.setRoomMentionEnabledEnabledReceivedEnabled, true)
|
||||
#expect(notificationSettingsProxy.setRoomMentionEnabledEnabledCalled)
|
||||
#expect(notificationSettingsProxy.setRoomMentionEnabledEnabledReceivedEnabled == true)
|
||||
}
|
||||
|
||||
func testToggleRoomMentionFailure() async throws {
|
||||
@Test
|
||||
func toggleRoomMentionFailure() async throws {
|
||||
notificationSettingsProxy.setRoomMentionEnabledEnabledThrowableError = NotificationSettingsError.Generic(msg: "error")
|
||||
notificationSettingsProxy.isRoomMentionEnabledReturnValue = false
|
||||
|
||||
@@ -267,10 +278,11 @@ class NotificationSettingsScreenViewModelTests: XCTestCase {
|
||||
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertNotNil(context.alertInfo)
|
||||
#expect(context.alertInfo != nil)
|
||||
}
|
||||
|
||||
func testToggleCallsOff() async throws {
|
||||
@Test
|
||||
func toggleCallsOff() async throws {
|
||||
notificationSettingsProxy.isCallEnabledReturnValue = true
|
||||
|
||||
let deferredInitialFetch = deferFulfillment(viewModel.context.observe(\.viewState.settings)) { $0 != nil }
|
||||
@@ -288,11 +300,12 @@ class NotificationSettingsScreenViewModelTests: XCTestCase {
|
||||
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssert(notificationSettingsProxy.setCallEnabledEnabledCalled)
|
||||
XCTAssertEqual(notificationSettingsProxy.setCallEnabledEnabledReceivedEnabled, false)
|
||||
#expect(notificationSettingsProxy.setCallEnabledEnabledCalled)
|
||||
#expect(notificationSettingsProxy.setCallEnabledEnabledReceivedEnabled == false)
|
||||
}
|
||||
|
||||
func testToggleCallsOn() async throws {
|
||||
@Test
|
||||
func toggleCallsOn() async throws {
|
||||
notificationSettingsProxy.isCallEnabledReturnValue = false
|
||||
|
||||
let deferredInitialFetch = deferFulfillment(viewModel.context.observe(\.viewState.settings)) { $0 != nil }
|
||||
@@ -311,11 +324,12 @@ class NotificationSettingsScreenViewModelTests: XCTestCase {
|
||||
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssert(notificationSettingsProxy.setCallEnabledEnabledCalled)
|
||||
XCTAssertEqual(notificationSettingsProxy.setCallEnabledEnabledReceivedEnabled, true)
|
||||
#expect(notificationSettingsProxy.setCallEnabledEnabledCalled)
|
||||
#expect(notificationSettingsProxy.setCallEnabledEnabledReceivedEnabled == true)
|
||||
}
|
||||
|
||||
func testToggleCallsFailure() async throws {
|
||||
@Test
|
||||
func toggleCallsFailure() async throws {
|
||||
notificationSettingsProxy.setCallEnabledEnabledThrowableError = NotificationSettingsError.Generic(msg: "error")
|
||||
notificationSettingsProxy.isCallEnabledReturnValue = false
|
||||
|
||||
@@ -337,10 +351,11 @@ class NotificationSettingsScreenViewModelTests: XCTestCase {
|
||||
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertNotNil(context.alertInfo)
|
||||
#expect(context.alertInfo != nil)
|
||||
}
|
||||
|
||||
func testToggleInvitationsOff() async throws {
|
||||
@Test
|
||||
func toggleInvitationsOff() async throws {
|
||||
notificationSettingsProxy.isInviteForMeEnabledReturnValue = true
|
||||
|
||||
let deferredInitialFetch = deferFulfillment(viewModel.context.observe(\.viewState.settings)) { $0 != nil }
|
||||
@@ -358,11 +373,12 @@ class NotificationSettingsScreenViewModelTests: XCTestCase {
|
||||
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssert(notificationSettingsProxy.setInviteForMeEnabledEnabledCalled)
|
||||
XCTAssertEqual(notificationSettingsProxy.setInviteForMeEnabledEnabledReceivedEnabled, false)
|
||||
#expect(notificationSettingsProxy.setInviteForMeEnabledEnabledCalled)
|
||||
#expect(notificationSettingsProxy.setInviteForMeEnabledEnabledReceivedEnabled == false)
|
||||
}
|
||||
|
||||
func testToggleInvitationsOn() async throws {
|
||||
@Test
|
||||
func toggleInvitationsOn() async throws {
|
||||
notificationSettingsProxy.isInviteForMeEnabledReturnValue = false
|
||||
|
||||
let deferredInitialFetch = deferFulfillment(viewModel.context.observe(\.viewState.settings)) { $0 != nil }
|
||||
@@ -381,11 +397,12 @@ class NotificationSettingsScreenViewModelTests: XCTestCase {
|
||||
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssert(notificationSettingsProxy.setInviteForMeEnabledEnabledCalled)
|
||||
XCTAssertEqual(notificationSettingsProxy.setInviteForMeEnabledEnabledReceivedEnabled, true)
|
||||
#expect(notificationSettingsProxy.setInviteForMeEnabledEnabledCalled)
|
||||
#expect(notificationSettingsProxy.setInviteForMeEnabledEnabledReceivedEnabled == true)
|
||||
}
|
||||
|
||||
func testToggleInvitesFailure() async throws {
|
||||
@Test
|
||||
func toggleInvitesFailure() async throws {
|
||||
notificationSettingsProxy.setInviteForMeEnabledEnabledThrowableError = NotificationSettingsError.Generic(msg: "error")
|
||||
notificationSettingsProxy.isInviteForMeEnabledReturnValue = false
|
||||
|
||||
@@ -407,6 +424,6 @@ class NotificationSettingsScreenViewModelTests: XCTestCase {
|
||||
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertNotNil(context.alertInfo)
|
||||
#expect(context.alertInfo != nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,11 @@
|
||||
|
||||
@testable import ElementX
|
||||
import MatrixRustSDK
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@Suite
|
||||
@MainActor
|
||||
class RoomDetailsEditScreenViewModelTests: XCTestCase {
|
||||
struct RoomDetailsEditScreenViewModelTests {
|
||||
var viewModel: RoomDetailsEditScreenViewModel!
|
||||
|
||||
var userIndicatorController: UserIndicatorControllerMock!
|
||||
@@ -20,65 +21,74 @@ class RoomDetailsEditScreenViewModelTests: XCTestCase {
|
||||
viewModel.context
|
||||
}
|
||||
|
||||
func testCannotSaveOnLanding() {
|
||||
@Test
|
||||
mutating func cannotSaveOnLanding() {
|
||||
setupViewModel(roomProxyConfiguration: .init(name: "Some room", members: [.mockMeAdmin]))
|
||||
XCTAssertFalse(context.viewState.canSave)
|
||||
#expect(!context.viewState.canSave)
|
||||
}
|
||||
|
||||
func testCanEdit() async throws {
|
||||
@Test
|
||||
mutating func canEdit() async throws {
|
||||
setupViewModel(roomProxyConfiguration: .init(name: "Some room", members: [.mockMeAdmin]))
|
||||
|
||||
let deferred = deferFulfillment(context.$viewState) { $0.canEditName }
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertTrue(context.viewState.canEditAvatar)
|
||||
XCTAssertTrue(context.viewState.canEditName)
|
||||
XCTAssertTrue(context.viewState.canEditTopic)
|
||||
#expect(context.viewState.canEditAvatar)
|
||||
#expect(context.viewState.canEditName)
|
||||
#expect(context.viewState.canEditTopic)
|
||||
}
|
||||
|
||||
func testCannotEdit() {
|
||||
@Test
|
||||
mutating func cannotEdit() {
|
||||
setupViewModel(roomProxyConfiguration: .init(name: "Some room", members: [.mockMe]))
|
||||
XCTAssertFalse(context.viewState.canEditAvatar)
|
||||
XCTAssertFalse(context.viewState.canEditName)
|
||||
XCTAssertFalse(context.viewState.canEditTopic)
|
||||
#expect(!context.viewState.canEditAvatar)
|
||||
#expect(!context.viewState.canEditName)
|
||||
#expect(!context.viewState.canEditTopic)
|
||||
}
|
||||
|
||||
func testNameDidChange() {
|
||||
@Test
|
||||
mutating func nameDidChange() {
|
||||
setupViewModel(roomProxyConfiguration: .init(name: "Some room", members: [.mockMeAdmin]))
|
||||
context.name = "name"
|
||||
XCTAssertTrue(context.viewState.nameDidChange)
|
||||
XCTAssertTrue(context.viewState.canSave)
|
||||
#expect(context.viewState.nameDidChange)
|
||||
#expect(context.viewState.canSave)
|
||||
}
|
||||
|
||||
func testTopicDidChange() {
|
||||
@Test
|
||||
mutating func topicDidChange() {
|
||||
setupViewModel(roomProxyConfiguration: .init(name: "Some room", members: [.mockMeAdmin]))
|
||||
context.topic = "topic"
|
||||
XCTAssertTrue(context.viewState.topicDidChange)
|
||||
XCTAssertTrue(context.viewState.canSave)
|
||||
#expect(context.viewState.topicDidChange)
|
||||
#expect(context.viewState.canSave)
|
||||
}
|
||||
|
||||
func testAvatarDidChange() {
|
||||
@Test
|
||||
mutating func avatarDidChange() {
|
||||
setupViewModel(roomProxyConfiguration: .init(name: "Some room", avatarURL: .mockMXCAvatar, members: [.mockMeAdmin]))
|
||||
context.send(viewAction: .removeImage)
|
||||
XCTAssertTrue(context.viewState.avatarDidChange)
|
||||
XCTAssertTrue(context.viewState.canSave)
|
||||
#expect(context.viewState.avatarDidChange)
|
||||
#expect(context.viewState.canSave)
|
||||
}
|
||||
|
||||
func testEmptyNameCannotBeSaved() {
|
||||
@Test
|
||||
mutating func emptyNameCannotBeSaved() {
|
||||
setupViewModel(roomProxyConfiguration: .init(name: "Some room", members: [.mockMeAdmin]))
|
||||
context.name = ""
|
||||
XCTAssertFalse(context.viewState.canSave)
|
||||
#expect(!context.viewState.canSave)
|
||||
}
|
||||
|
||||
func testAvatarPickerShowsSheet() {
|
||||
@Test
|
||||
mutating func avatarPickerShowsSheet() {
|
||||
setupViewModel(roomProxyConfiguration: .init(name: "Some room", members: [.mockMeAdmin]))
|
||||
context.name = "name"
|
||||
XCTAssertFalse(context.showMediaSheet)
|
||||
#expect(!context.showMediaSheet)
|
||||
context.send(viewAction: .presentMediaSource)
|
||||
XCTAssertTrue(context.showMediaSheet)
|
||||
#expect(context.showMediaSheet)
|
||||
}
|
||||
|
||||
func testSaveTriggersViewModelAction() async throws {
|
||||
@Test
|
||||
mutating func saveTriggersViewModelAction() async throws {
|
||||
setupViewModel(roomProxyConfiguration: .init(name: "Some room", members: [.mockMeAdmin]))
|
||||
|
||||
let deferred = deferFulfillment(viewModel.actions) { action in
|
||||
@@ -89,67 +99,72 @@ class RoomDetailsEditScreenViewModelTests: XCTestCase {
|
||||
context.send(viewAction: .save)
|
||||
|
||||
let action = try await deferred.fulfill()
|
||||
XCTAssertEqual(action, .saveFinished)
|
||||
#expect(action == .saveFinished)
|
||||
}
|
||||
|
||||
func testCancelWithoutChanges() async throws {
|
||||
@Test
|
||||
mutating func cancelWithoutChanges() async throws {
|
||||
setupViewModel(roomProxyConfiguration: .init(name: "Some room", members: [.mockMeAdmin]))
|
||||
XCTAssertFalse(context.viewState.canSave)
|
||||
XCTAssertNil(context.alertInfo)
|
||||
#expect(!context.viewState.canSave)
|
||||
#expect(context.alertInfo == nil)
|
||||
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0 == .cancel }
|
||||
context.send(viewAction: .cancel)
|
||||
try await deferred.fulfill()
|
||||
XCTAssertNil(context.alertInfo)
|
||||
#expect(context.alertInfo == nil)
|
||||
}
|
||||
|
||||
func testCancelWithChangesAndDiscard() async throws {
|
||||
@Test
|
||||
mutating func cancelWithChangesAndDiscard() async throws {
|
||||
setupViewModel(roomProxyConfiguration: .init(name: "Some room", members: [.mockMeAdmin]))
|
||||
context.name = "name"
|
||||
XCTAssertTrue(context.viewState.canSave)
|
||||
XCTAssertNil(context.alertInfo)
|
||||
#expect(context.viewState.canSave)
|
||||
#expect(context.alertInfo == nil)
|
||||
|
||||
context.send(viewAction: .cancel)
|
||||
|
||||
XCTAssertNotNil(context.alertInfo)
|
||||
#expect(context.alertInfo != nil)
|
||||
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0 == .cancel }
|
||||
context.alertInfo?.secondaryButton?.action?() // Discard
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testCancelWithChangesAndSave() async throws {
|
||||
@Test
|
||||
mutating func cancelWithChangesAndSave() async throws {
|
||||
setupViewModel(roomProxyConfiguration: .init(name: "Some room", members: [.mockMeAdmin]))
|
||||
context.name = "name"
|
||||
XCTAssertTrue(context.viewState.canSave)
|
||||
XCTAssertNil(context.alertInfo)
|
||||
#expect(context.viewState.canSave)
|
||||
#expect(context.alertInfo == nil)
|
||||
|
||||
context.send(viewAction: .cancel)
|
||||
|
||||
XCTAssertNotNil(context.alertInfo)
|
||||
#expect(context.alertInfo != nil)
|
||||
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0 == .saveFinished }
|
||||
context.alertInfo?.primaryButton.action?() // Save
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testErrorShownOnFailedFetchOfMedia() async {
|
||||
@Test
|
||||
mutating func errorShownOnFailedFetchOfMedia() async {
|
||||
setupViewModel(roomProxyConfiguration: .init(name: "Some room", members: [.mockMeAdmin]))
|
||||
viewModel.didSelectMediaUrl(url: .picturesDirectory)
|
||||
try? await Task.sleep(for: .milliseconds(100))
|
||||
XCTAssertNotNil(context.alertInfo)
|
||||
#expect(context.alertInfo != nil)
|
||||
}
|
||||
|
||||
func testDeleteAvatar() {
|
||||
@Test
|
||||
mutating func deleteAvatar() {
|
||||
setupViewModel(roomProxyConfiguration: .init(name: "Some room", avatarURL: .mockMXCAvatar, members: [.mockMeAdmin]))
|
||||
XCTAssertNotNil(context.viewState.avatarURL)
|
||||
#expect(context.viewState.avatarURL != nil)
|
||||
context.send(viewAction: .removeImage)
|
||||
XCTAssertNil(context.viewState.avatarURL)
|
||||
#expect(context.viewState.avatarURL == nil)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func setupViewModel(roomProxyConfiguration: JoinedRoomProxyMockConfiguration) {
|
||||
private mutating func setupViewModel(roomProxyConfiguration: JoinedRoomProxyMockConfiguration) {
|
||||
userIndicatorController = UserIndicatorControllerMock.default
|
||||
viewModel = .init(roomProxy: JoinedRoomProxyMock(roomProxyConfiguration),
|
||||
userSession: UserSessionMock(.init()),
|
||||
|
||||
@@ -11,10 +11,11 @@ import Combine
|
||||
@testable import ElementX
|
||||
import MatrixRustSDK
|
||||
import SwiftUI
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@Suite
|
||||
@MainActor
|
||||
class RoomDetailsScreenViewModelTests: XCTestCase {
|
||||
struct RoomDetailsScreenViewModelTests {
|
||||
var viewModel: RoomDetailsScreenViewModel!
|
||||
var roomProxyMock: JoinedRoomProxyMock!
|
||||
var notificationSettingsProxyMock: NotificationSettingsProxyMock!
|
||||
@@ -24,7 +25,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
||||
|
||||
var cancellables = Set<AnyCancellable>()
|
||||
|
||||
override func setUp() {
|
||||
init() {
|
||||
AppSettings.resetAllSettings()
|
||||
cancellables.removeAll()
|
||||
roomProxyMock = JoinedRoomProxyMock(.init(name: "Test"))
|
||||
@@ -38,7 +39,8 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
||||
appSettings: ServiceLocator.shared.settings)
|
||||
}
|
||||
|
||||
func testLeaveRoomTappedWhenPublic() async throws {
|
||||
@Test
|
||||
mutating func leaveRoomTappedWhenPublic() async throws {
|
||||
let mockedMembers: [RoomMemberProxyMock] = [.mockBob, .mockAlice]
|
||||
roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", members: mockedMembers, joinRule: .public))
|
||||
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
|
||||
@@ -53,11 +55,12 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
||||
context.send(viewAction: .processTapLeave)
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertEqual(context.viewState.bindings.leaveRoomAlertItem?.state, .public)
|
||||
XCTAssertEqual(context.viewState.bindings.leaveRoomAlertItem?.subtitle, L10n.leaveRoomAlertSubtitle)
|
||||
#expect(context.viewState.bindings.leaveRoomAlertItem?.state == .public)
|
||||
#expect(context.viewState.bindings.leaveRoomAlertItem?.subtitle == L10n.leaveRoomAlertSubtitle)
|
||||
}
|
||||
|
||||
func testLeaveRoomTappedWhenRoomNotPublic() async throws {
|
||||
@Test
|
||||
mutating func leaveRoomTappedWhenRoomNotPublic() async throws {
|
||||
let mockedMembers: [RoomMemberProxyMock] = [.mockBob, .mockAlice]
|
||||
roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", members: mockedMembers))
|
||||
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
|
||||
@@ -73,11 +76,12 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
||||
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertEqual(context.viewState.bindings.leaveRoomAlertItem?.state, .private)
|
||||
XCTAssertEqual(context.viewState.bindings.leaveRoomAlertItem?.subtitle, L10n.leaveRoomAlertPrivateSubtitle)
|
||||
#expect(context.viewState.bindings.leaveRoomAlertItem?.state == .private)
|
||||
#expect(context.viewState.bindings.leaveRoomAlertItem?.subtitle == L10n.leaveRoomAlertPrivateSubtitle)
|
||||
}
|
||||
|
||||
func testLeaveRoomTappedWithLessThanTwoMembers() {
|
||||
@Test
|
||||
mutating func leaveRoomTappedWithLessThanTwoMembers() {
|
||||
let mockedMembers: [RoomMemberProxyMock] = [.mockAlice]
|
||||
roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", members: mockedMembers))
|
||||
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
|
||||
@@ -89,11 +93,12 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
||||
appSettings: ServiceLocator.shared.settings)
|
||||
|
||||
context.send(viewAction: .processTapLeave)
|
||||
XCTAssertEqual(context.leaveRoomAlertItem?.state, .empty)
|
||||
XCTAssertEqual(context.leaveRoomAlertItem?.subtitle, L10n.leaveRoomAlertEmptySubtitle)
|
||||
#expect(context.leaveRoomAlertItem?.state == .empty)
|
||||
#expect(context.leaveRoomAlertItem?.subtitle == L10n.leaveRoomAlertEmptySubtitle)
|
||||
}
|
||||
|
||||
func testLeaveRoomSuccess() async throws {
|
||||
@Test
|
||||
func leaveRoomSuccess() async throws {
|
||||
roomProxyMock.leaveRoomClosure = {
|
||||
.success(())
|
||||
}
|
||||
@@ -111,24 +116,29 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
||||
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertEqual(roomProxyMock.leaveRoomCallsCount, 1)
|
||||
#expect(roomProxyMock.leaveRoomCallsCount == 1)
|
||||
}
|
||||
|
||||
func testLeaveRoomError() async {
|
||||
let expectation = expectation(description: #function)
|
||||
roomProxyMock.leaveRoomClosure = {
|
||||
defer {
|
||||
expectation.fulfill()
|
||||
@Test
|
||||
func leaveRoomError() async throws {
|
||||
try await confirmation("leaveRoomError") { confirm in
|
||||
roomProxyMock.leaveRoomClosure = {
|
||||
defer {
|
||||
confirm()
|
||||
}
|
||||
return .failure(.sdkError(ClientProxyMockError.generic))
|
||||
}
|
||||
return .failure(.sdkError(ClientProxyMockError.generic))
|
||||
|
||||
let deferred = deferFulfillment(context.observe(\.alertInfo)) { $0 != nil }
|
||||
context.send(viewAction: .confirmLeave)
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
context.send(viewAction: .confirmLeave)
|
||||
await fulfillment(of: [expectation])
|
||||
XCTAssertEqual(roomProxyMock.leaveRoomCallsCount, 1)
|
||||
XCTAssertNotNil(context.alertInfo)
|
||||
|
||||
#expect(roomProxyMock.leaveRoomCallsCount == 1)
|
||||
}
|
||||
|
||||
func testInitialDMDetailsState() async throws {
|
||||
@Test
|
||||
mutating func initialDMDetailsState() async throws {
|
||||
let recipient = RoomMemberProxyMock.mockDan
|
||||
let mockedMembers: [RoomMemberProxyMock] = [.mockMe, recipient]
|
||||
roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isDirect: true, isEncrypted: true, members: mockedMembers))
|
||||
@@ -144,10 +154,11 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
||||
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertEqual(context.viewState.dmRecipientInfo?.member, RoomMemberDetails(withProxy: recipient))
|
||||
#expect(context.viewState.dmRecipientInfo?.member == RoomMemberDetails(withProxy: recipient))
|
||||
}
|
||||
|
||||
func testIgnoreSuccess() async throws {
|
||||
@Test
|
||||
mutating func ignoreSuccess() async throws {
|
||||
let recipient = RoomMemberProxyMock.mockDan
|
||||
|
||||
let mockedMembers: [RoomMemberProxyMock] = [.mockMe, recipient]
|
||||
@@ -164,7 +175,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
||||
|
||||
try await deferredRecipient.fulfill()
|
||||
|
||||
XCTAssertEqual(context.viewState.dmRecipientInfo?.member, RoomMemberDetails(withProxy: recipient))
|
||||
#expect(context.viewState.dmRecipientInfo?.member == RoomMemberDetails(withProxy: recipient))
|
||||
|
||||
let deferredProcessing = deferFulfillment(viewModel.context.observe(\.viewState.isProcessingIgnoreRequest),
|
||||
transitionValues: [false, true, false])
|
||||
@@ -173,10 +184,11 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
||||
|
||||
try await deferredProcessing.fulfill()
|
||||
|
||||
XCTAssert(context.viewState.dmRecipientInfo?.member.isIgnored == true)
|
||||
#expect(context.viewState.dmRecipientInfo?.member.isIgnored == true)
|
||||
}
|
||||
|
||||
func testIgnoreFailure() async throws {
|
||||
@Test
|
||||
mutating func ignoreFailure() async throws {
|
||||
let recipient = RoomMemberProxyMock.mockDan
|
||||
let mockedMembers: [RoomMemberProxyMock] = [.mockMe, recipient]
|
||||
let clientProxy = ClientProxyMock(.init())
|
||||
@@ -194,7 +206,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
||||
|
||||
try await deferredRecipient.fulfill()
|
||||
|
||||
XCTAssertEqual(context.viewState.dmRecipientInfo?.member, RoomMemberDetails(withProxy: recipient))
|
||||
#expect(context.viewState.dmRecipientInfo?.member == RoomMemberDetails(withProxy: recipient))
|
||||
|
||||
let deferredProcessing = deferFulfillment(viewModel.context.observe(\.viewState.isProcessingIgnoreRequest),
|
||||
transitionValues: [false, true, false])
|
||||
@@ -203,11 +215,12 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
||||
|
||||
try await deferredProcessing.fulfill()
|
||||
|
||||
XCTAssert(context.viewState.dmRecipientInfo?.member.isIgnored == false)
|
||||
XCTAssertNotNil(context.alertInfo)
|
||||
#expect(context.viewState.dmRecipientInfo?.member.isIgnored == false)
|
||||
#expect(context.alertInfo != nil)
|
||||
}
|
||||
|
||||
func testUnignoreSuccess() async throws {
|
||||
@Test
|
||||
mutating func unignoreSuccess() async throws {
|
||||
let recipient = RoomMemberProxyMock.mockIgnored
|
||||
let mockedMembers: [RoomMemberProxyMock] = [.mockMe, recipient]
|
||||
roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isDirect: true, isEncrypted: true, members: mockedMembers))
|
||||
@@ -223,7 +236,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
||||
|
||||
try await deferredRecipient.fulfill()
|
||||
|
||||
XCTAssertEqual(context.viewState.dmRecipientInfo?.member, RoomMemberDetails(withProxy: recipient))
|
||||
#expect(context.viewState.dmRecipientInfo?.member == RoomMemberDetails(withProxy: recipient))
|
||||
|
||||
let deferredProcessing = deferFulfillment(viewModel.context.observe(\.viewState.isProcessingIgnoreRequest),
|
||||
transitionValues: [false, true, false])
|
||||
@@ -232,10 +245,11 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
||||
|
||||
try await deferredProcessing.fulfill()
|
||||
|
||||
XCTAssert(context.viewState.dmRecipientInfo?.member.isIgnored == false)
|
||||
#expect(context.viewState.dmRecipientInfo?.member.isIgnored == false)
|
||||
}
|
||||
|
||||
func testUnignoreFailure() async throws {
|
||||
@Test
|
||||
mutating func unignoreFailure() async throws {
|
||||
let recipient = RoomMemberProxyMock.mockIgnored
|
||||
let mockedMembers: [RoomMemberProxyMock] = [.mockMe, recipient]
|
||||
let clientProxy = ClientProxyMock(.init())
|
||||
@@ -253,7 +267,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
||||
|
||||
try await deferredRecipient.fulfill()
|
||||
|
||||
XCTAssertEqual(context.viewState.dmRecipientInfo?.member, RoomMemberDetails(withProxy: recipient))
|
||||
#expect(context.viewState.dmRecipientInfo?.member == RoomMemberDetails(withProxy: recipient))
|
||||
|
||||
let deferredProcessing = deferFulfillment(viewModel.context.observe(\.viewState.isProcessingIgnoreRequest),
|
||||
transitionValues: [false, true, false])
|
||||
@@ -262,11 +276,12 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
||||
|
||||
try await deferredProcessing.fulfill()
|
||||
|
||||
XCTAssert(context.viewState.dmRecipientInfo?.member.isIgnored == true)
|
||||
XCTAssertNotNil(context.alertInfo)
|
||||
#expect(context.viewState.dmRecipientInfo?.member.isIgnored == true)
|
||||
#expect(context.alertInfo != nil)
|
||||
}
|
||||
|
||||
func testCannotInvitePeople() async {
|
||||
@Test
|
||||
mutating func cannotInvitePeople() async {
|
||||
let mockedMembers: [RoomMemberProxyMock] = [.mockMe, .mockAlice]
|
||||
roomProxyMock = JoinedRoomProxyMock(.init(name: "Test",
|
||||
members: mockedMembers,
|
||||
@@ -282,10 +297,11 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
||||
|
||||
_ = await context.observe(\.viewState).debounce(for: .milliseconds(100)).first()
|
||||
|
||||
XCTAssertFalse(context.viewState.canInviteUsers)
|
||||
#expect(!context.viewState.canInviteUsers)
|
||||
}
|
||||
|
||||
func testInvitePeople() async {
|
||||
@Test
|
||||
mutating func invitePeople() async {
|
||||
let mockedMembers: [RoomMemberProxyMock] = [.mockMe, .mockBob, .mockAlice]
|
||||
roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", members: mockedMembers, joinRule: .public))
|
||||
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
|
||||
@@ -298,7 +314,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
||||
|
||||
_ = await context.observe(\.viewState).debounce(for: .milliseconds(100)).first()
|
||||
|
||||
XCTAssertTrue(context.viewState.canInviteUsers)
|
||||
#expect(context.viewState.canInviteUsers)
|
||||
|
||||
var callbackCorrectlyCalled = false
|
||||
viewModel.actions
|
||||
@@ -314,10 +330,11 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
||||
|
||||
context.send(viewAction: .processTapInvite)
|
||||
await Task.yield()
|
||||
XCTAssertTrue(callbackCorrectlyCalled)
|
||||
#expect(callbackCorrectlyCalled)
|
||||
}
|
||||
|
||||
func testCanEditAvatar() async {
|
||||
@Test
|
||||
mutating func canEditAvatar() async {
|
||||
let mockedMembers: [RoomMemberProxyMock] = [.mockMe, .mockBob, .mockAlice]
|
||||
|
||||
let configuration = JoinedRoomProxyMockConfiguration(name: "Test",
|
||||
@@ -349,13 +366,14 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
||||
|
||||
_ = await context.observe(\.viewState).debounce(for: .milliseconds(100)).first()
|
||||
|
||||
XCTAssertTrue(context.viewState.canEditRoomAvatar)
|
||||
XCTAssertFalse(context.viewState.canEditRoomName)
|
||||
XCTAssertFalse(context.viewState.canEditRoomTopic)
|
||||
XCTAssertTrue(context.viewState.canEditBaseInfo)
|
||||
#expect(context.viewState.canEditRoomAvatar)
|
||||
#expect(!context.viewState.canEditRoomName)
|
||||
#expect(!context.viewState.canEditRoomTopic)
|
||||
#expect(context.viewState.canEditBaseInfo)
|
||||
}
|
||||
|
||||
func testCanEditName() async {
|
||||
@Test
|
||||
mutating func canEditName() async {
|
||||
let mockedMembers: [RoomMemberProxyMock] = [.mockMe, .mockBob, .mockAlice]
|
||||
|
||||
let configuration = JoinedRoomProxyMockConfiguration(name: "Test",
|
||||
@@ -387,13 +405,14 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
||||
|
||||
_ = await context.observe(\.viewState).debounce(for: .milliseconds(100)).first()
|
||||
|
||||
XCTAssertFalse(context.viewState.canEditRoomAvatar)
|
||||
XCTAssertTrue(context.viewState.canEditRoomName)
|
||||
XCTAssertFalse(context.viewState.canEditRoomTopic)
|
||||
XCTAssertTrue(context.viewState.canEditBaseInfo)
|
||||
#expect(!context.viewState.canEditRoomAvatar)
|
||||
#expect(context.viewState.canEditRoomName)
|
||||
#expect(!context.viewState.canEditRoomTopic)
|
||||
#expect(context.viewState.canEditBaseInfo)
|
||||
}
|
||||
|
||||
func testCanEditTopic() async {
|
||||
@Test
|
||||
mutating func canEditTopic() async {
|
||||
let mockedMembers: [RoomMemberProxyMock] = [.mockMe, .mockBob, .mockAlice]
|
||||
|
||||
let configuration = JoinedRoomProxyMockConfiguration(name: "Test",
|
||||
@@ -425,13 +444,14 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
||||
|
||||
_ = await context.observe(\.viewState).debounce(for: .milliseconds(100)).first()
|
||||
|
||||
XCTAssertFalse(context.viewState.canEditRoomAvatar)
|
||||
XCTAssertFalse(context.viewState.canEditRoomName)
|
||||
XCTAssertTrue(context.viewState.canEditRoomTopic)
|
||||
XCTAssertTrue(context.viewState.canEditBaseInfo)
|
||||
#expect(!context.viewState.canEditRoomAvatar)
|
||||
#expect(!context.viewState.canEditRoomName)
|
||||
#expect(context.viewState.canEditRoomTopic)
|
||||
#expect(context.viewState.canEditBaseInfo)
|
||||
}
|
||||
|
||||
func testCannotEditRoom() async {
|
||||
@Test
|
||||
mutating func cannotEditRoom() async {
|
||||
let mockedMembers: [RoomMemberProxyMock] = [.mockMe, .mockBob, .mockAlice]
|
||||
roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isDirect: false, members: mockedMembers))
|
||||
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
|
||||
@@ -444,13 +464,14 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
||||
|
||||
_ = await context.observe(\.viewState).debounce(for: .milliseconds(100)).first()
|
||||
|
||||
XCTAssertFalse(context.viewState.canEditRoomAvatar)
|
||||
XCTAssertFalse(context.viewState.canEditRoomName)
|
||||
XCTAssertFalse(context.viewState.canEditRoomTopic)
|
||||
XCTAssertFalse(context.viewState.canEditBaseInfo)
|
||||
#expect(!context.viewState.canEditRoomAvatar)
|
||||
#expect(!context.viewState.canEditRoomName)
|
||||
#expect(!context.viewState.canEditRoomTopic)
|
||||
#expect(!context.viewState.canEditBaseInfo)
|
||||
}
|
||||
|
||||
func testCannotEditDirectRoom() async {
|
||||
@Test
|
||||
mutating func cannotEditDirectRoom() async {
|
||||
let mockedMembers: [RoomMemberProxyMock] = [.mockMeAdmin, .mockBob, .mockAlice]
|
||||
roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isDirect: true, members: mockedMembers))
|
||||
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
|
||||
@@ -463,12 +484,13 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
||||
|
||||
_ = await context.observe(\.viewState).debounce(for: .milliseconds(100)).first()
|
||||
|
||||
XCTAssertFalse(context.viewState.canEditBaseInfo)
|
||||
#expect(!context.viewState.canEditBaseInfo)
|
||||
}
|
||||
|
||||
// MARK: - Notifications
|
||||
|
||||
func testNotificationLoadingSettingsFailure() async throws {
|
||||
@Test
|
||||
mutating func notificationLoadingSettingsFailure() async throws {
|
||||
notificationSettingsProxyMock.getNotificationSettingsRoomIdIsEncryptedIsOneToOneThrowableError = NotificationSettingsError.Generic(msg: "error")
|
||||
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
|
||||
userSession: UserSessionMock(.init()),
|
||||
@@ -491,12 +513,13 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
||||
let expectedAlertInfo = AlertInfo(id: RoomDetailsScreenErrorType.alert,
|
||||
title: L10n.commonError,
|
||||
message: L10n.screenRoomDetailsErrorLoadingNotificationSettings)
|
||||
XCTAssertEqual(context.viewState.bindings.alertInfo?.id, expectedAlertInfo.id)
|
||||
XCTAssertEqual(context.viewState.bindings.alertInfo?.title, expectedAlertInfo.title)
|
||||
XCTAssertEqual(context.viewState.bindings.alertInfo?.message, expectedAlertInfo.message)
|
||||
#expect(context.viewState.bindings.alertInfo?.id == expectedAlertInfo.id)
|
||||
#expect(context.viewState.bindings.alertInfo?.title == expectedAlertInfo.title)
|
||||
#expect(context.viewState.bindings.alertInfo?.message == expectedAlertInfo.message)
|
||||
}
|
||||
|
||||
func testNotificationDefaultMode() async throws {
|
||||
@Test
|
||||
func notificationDefaultMode() async throws {
|
||||
notificationSettingsProxyMock.getNotificationSettingsRoomIdIsEncryptedIsOneToOneReturnValue = RoomNotificationSettingsProxyMock(with: .init(mode: .allMessages, isDefault: true))
|
||||
|
||||
let deferred = deferFulfillment(context.observe(\.viewState.notificationSettingsState)) { $0.isLoaded }
|
||||
@@ -504,10 +527,11 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
||||
notificationSettingsProxyMock.callbacks.send(.settingsDidChange)
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertEqual(context.viewState.notificationSettingsState.label, "Default")
|
||||
#expect(context.viewState.notificationSettingsState.label == "Default")
|
||||
}
|
||||
|
||||
func testNotificationCustomMode() async throws {
|
||||
@Test
|
||||
func notificationCustomMode() async throws {
|
||||
notificationSettingsProxyMock.getNotificationSettingsRoomIdIsEncryptedIsOneToOneReturnValue = RoomNotificationSettingsProxyMock(with: .init(mode: .allMessages, isDefault: false))
|
||||
|
||||
let deferred = deferFulfillment(context.observe(\.viewState.notificationSettingsState)) { $0.isCustom }
|
||||
@@ -515,10 +539,11 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
||||
notificationSettingsProxyMock.callbacks.send(.settingsDidChange)
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertEqual(context.viewState.notificationSettingsState.label, "Custom")
|
||||
#expect(context.viewState.notificationSettingsState.label == "Custom")
|
||||
}
|
||||
|
||||
func testNotificationRoomMuted() async throws {
|
||||
@Test
|
||||
func notificationRoomMuted() async throws {
|
||||
notificationSettingsProxyMock.getNotificationSettingsRoomIdIsEncryptedIsOneToOneReturnValue = RoomNotificationSettingsProxyMock(with: .init(mode: .mute, isDefault: false))
|
||||
|
||||
let deferred = deferFulfillment(context.observe(\.viewState.notificationSettingsState)) { $0.isLoaded }
|
||||
@@ -528,11 +553,12 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
||||
|
||||
_ = await context.observe(\.viewState).debounce(for: .milliseconds(100)).first()
|
||||
|
||||
XCTAssertEqual(context.viewState.notificationShortcutButtonTitle, L10n.commonUnmute)
|
||||
XCTAssertEqual(context.viewState.notificationShortcutButtonIcon, \.notificationsOff)
|
||||
#expect(context.viewState.notificationShortcutButtonTitle == L10n.commonUnmute)
|
||||
#expect(context.viewState.notificationShortcutButtonIcon == \.notificationsOff)
|
||||
}
|
||||
|
||||
func testNotificationRoomNotMuted() async throws {
|
||||
@Test
|
||||
func notificationRoomNotMuted() async throws {
|
||||
notificationSettingsProxyMock.getNotificationSettingsRoomIdIsEncryptedIsOneToOneReturnValue = RoomNotificationSettingsProxyMock(with: .init(mode: .mentionsAndKeywordsOnly, isDefault: false))
|
||||
|
||||
let deferred = deferFulfillment(context.observe(\.viewState.notificationSettingsState)) { $0.isLoaded }
|
||||
@@ -540,72 +566,81 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
||||
notificationSettingsProxyMock.callbacks.send(.settingsDidChange)
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertEqual(context.viewState.notificationShortcutButtonTitle, L10n.commonMute)
|
||||
XCTAssertEqual(context.viewState.notificationShortcutButtonIcon, \.notifications)
|
||||
#expect(context.viewState.notificationShortcutButtonTitle == L10n.commonMute)
|
||||
#expect(context.viewState.notificationShortcutButtonIcon == \.notifications)
|
||||
}
|
||||
|
||||
func testUnmuteTappedFailure() async throws {
|
||||
try await testNotificationRoomMuted()
|
||||
@Test
|
||||
func unmuteTappedFailure() async throws {
|
||||
try await notificationRoomMuted()
|
||||
|
||||
let expectation = expectation(description: #function)
|
||||
notificationSettingsProxyMock.unmuteRoomRoomIdIsEncryptedIsOneToOneClosure = { _, _, _ in
|
||||
defer {
|
||||
expectation.fulfill()
|
||||
try await confirmation("unmuteTappedFailure") { confirm in
|
||||
notificationSettingsProxyMock.unmuteRoomRoomIdIsEncryptedIsOneToOneClosure = { _, _, _ in
|
||||
defer {
|
||||
confirm()
|
||||
}
|
||||
throw NotificationSettingsError.Generic(msg: "unmute error")
|
||||
}
|
||||
throw NotificationSettingsError.Generic(msg: "unmute error")
|
||||
context.send(viewAction: .processToggleMuteNotifications)
|
||||
try await deferFulfillment(context.observe(\.alertInfo)) { $0 != nil }.fulfill()
|
||||
}
|
||||
context.send(viewAction: .processToggleMuteNotifications)
|
||||
await fulfillment(of: [expectation])
|
||||
|
||||
XCTAssertFalse(context.viewState.isProcessingMuteToggleAction)
|
||||
XCTAssertEqual(context.viewState.notificationShortcutButtonTitle, L10n.commonUnmute)
|
||||
#expect(!context.viewState.isProcessingMuteToggleAction)
|
||||
#expect(context.viewState.notificationShortcutButtonTitle == L10n.commonUnmute)
|
||||
|
||||
let expectedAlertInfo = AlertInfo(id: RoomDetailsScreenErrorType.alert,
|
||||
title: L10n.commonError,
|
||||
message: L10n.screenRoomDetailsErrorUnmuting)
|
||||
|
||||
XCTAssertEqual(context.viewState.bindings.alertInfo?.id, expectedAlertInfo.id)
|
||||
XCTAssertEqual(context.viewState.bindings.alertInfo?.title, expectedAlertInfo.title)
|
||||
XCTAssertEqual(context.viewState.bindings.alertInfo?.message, expectedAlertInfo.message)
|
||||
#expect(context.viewState.bindings.alertInfo?.id == expectedAlertInfo.id)
|
||||
#expect(context.viewState.bindings.alertInfo?.title == expectedAlertInfo.title)
|
||||
#expect(context.viewState.bindings.alertInfo?.message == expectedAlertInfo.message)
|
||||
}
|
||||
|
||||
func testMuteTappedFailure() async throws {
|
||||
try await testNotificationRoomNotMuted()
|
||||
@Test
|
||||
func muteTappedFailure() async throws {
|
||||
try await notificationRoomNotMuted()
|
||||
|
||||
let expectation = expectation(description: #function)
|
||||
notificationSettingsProxyMock.setNotificationModeRoomIdModeClosure = { _, _ in
|
||||
defer {
|
||||
expectation.fulfill()
|
||||
try await confirmation("muteTappedFailure") { confirm in
|
||||
notificationSettingsProxyMock.setNotificationModeRoomIdModeClosure = { _, _ in
|
||||
defer {
|
||||
confirm()
|
||||
}
|
||||
throw NotificationSettingsError.Generic(msg: "mute error")
|
||||
}
|
||||
throw NotificationSettingsError.Generic(msg: "mute error")
|
||||
|
||||
let deferred = deferFulfillment(context.observe(\.alertInfo)) { $0 != nil }
|
||||
context.send(viewAction: .processToggleMuteNotifications)
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
context.send(viewAction: .processToggleMuteNotifications)
|
||||
await fulfillment(of: [expectation])
|
||||
|
||||
XCTAssertFalse(context.viewState.isProcessingMuteToggleAction)
|
||||
XCTAssertEqual(context.viewState.notificationShortcutButtonTitle, L10n.commonMute)
|
||||
#expect(!context.viewState.isProcessingMuteToggleAction)
|
||||
#expect(context.viewState.notificationShortcutButtonTitle == L10n.commonMute)
|
||||
|
||||
let expectedAlertInfo = AlertInfo(id: RoomDetailsScreenErrorType.alert,
|
||||
title: L10n.commonError,
|
||||
message: L10n.screenRoomDetailsErrorMuting)
|
||||
|
||||
XCTAssertEqual(context.viewState.bindings.alertInfo?.id, expectedAlertInfo.id)
|
||||
XCTAssertEqual(context.viewState.bindings.alertInfo?.title, expectedAlertInfo.title)
|
||||
XCTAssertEqual(context.viewState.bindings.alertInfo?.message, expectedAlertInfo.message)
|
||||
#expect(context.viewState.bindings.alertInfo?.id == expectedAlertInfo.id)
|
||||
#expect(context.viewState.bindings.alertInfo?.title == expectedAlertInfo.title)
|
||||
#expect(context.viewState.bindings.alertInfo?.message == expectedAlertInfo.message)
|
||||
}
|
||||
|
||||
func testMuteTapped() async throws {
|
||||
try await testNotificationRoomNotMuted()
|
||||
@Test
|
||||
func muteTapped() async throws {
|
||||
try await notificationRoomNotMuted()
|
||||
|
||||
let expectation = expectation(description: #function)
|
||||
notificationSettingsProxyMock.setNotificationModeRoomIdModeClosure = { [weak notificationSettingsProxyMock] _, mode in
|
||||
notificationSettingsProxyMock?.getNotificationSettingsRoomIdIsEncryptedIsOneToOneReturnValue = RoomNotificationSettingsProxyMock(with: .init(mode: mode, isDefault: false))
|
||||
expectation.fulfill()
|
||||
try await confirmation("muteTapped") { confirm in
|
||||
notificationSettingsProxyMock.setNotificationModeRoomIdModeClosure = { [weak notificationSettingsProxyMock] _, mode in
|
||||
notificationSettingsProxyMock?.getNotificationSettingsRoomIdIsEncryptedIsOneToOneReturnValue = RoomNotificationSettingsProxyMock(with: .init(mode: mode, isDefault: false))
|
||||
confirm()
|
||||
}
|
||||
|
||||
let deferred = deferFulfillment(context.observe(\.viewState.isProcessingMuteToggleAction),
|
||||
transitionValues: [false, true, false])
|
||||
context.send(viewAction: .processToggleMuteNotifications)
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
context.send(viewAction: .processToggleMuteNotifications)
|
||||
await fulfillment(of: [expectation])
|
||||
|
||||
XCTAssertFalse(context.viewState.isProcessingMuteToggleAction)
|
||||
|
||||
let deferred = deferFulfillment(context.observe(\.viewState.notificationSettingsState)) { state in
|
||||
switch state {
|
||||
@@ -618,25 +653,28 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
if case .loaded(let newNotificationSettingsState) = viewModel.state.notificationSettingsState {
|
||||
XCTAssertFalse(newNotificationSettingsState.isDefault)
|
||||
XCTAssertEqual(newNotificationSettingsState.mode, .mute)
|
||||
#expect(!newNotificationSettingsState.isDefault)
|
||||
#expect(newNotificationSettingsState.mode == .mute)
|
||||
} else {
|
||||
XCTFail("invalid state")
|
||||
Issue.record("invalid state")
|
||||
}
|
||||
}
|
||||
|
||||
func testUnmuteTapped() async throws {
|
||||
try await testNotificationRoomMuted()
|
||||
@Test
|
||||
func unmuteTapped() async throws {
|
||||
try await notificationRoomMuted()
|
||||
|
||||
let expectation = expectation(description: #function)
|
||||
notificationSettingsProxyMock.unmuteRoomRoomIdIsEncryptedIsOneToOneClosure = { [weak notificationSettingsProxyMock] _, _, _ in
|
||||
notificationSettingsProxyMock?.getNotificationSettingsRoomIdIsEncryptedIsOneToOneReturnValue = RoomNotificationSettingsProxyMock(with: .init(mode: .allMessages, isDefault: false))
|
||||
expectation.fulfill()
|
||||
try await confirmation("unmuteTapped") { confirm in
|
||||
notificationSettingsProxyMock.unmuteRoomRoomIdIsEncryptedIsOneToOneClosure = { [weak notificationSettingsProxyMock] _, _, _ in
|
||||
notificationSettingsProxyMock?.getNotificationSettingsRoomIdIsEncryptedIsOneToOneReturnValue = RoomNotificationSettingsProxyMock(with: .init(mode: .allMessages, isDefault: false))
|
||||
confirm()
|
||||
}
|
||||
|
||||
let deferred = deferFulfillment(context.observe(\.viewState.isProcessingMuteToggleAction),
|
||||
transitionValues: [false, true, false])
|
||||
context.send(viewAction: .processToggleMuteNotifications)
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
context.send(viewAction: .processToggleMuteNotifications)
|
||||
await fulfillment(of: [expectation])
|
||||
|
||||
XCTAssertFalse(context.viewState.isProcessingMuteToggleAction)
|
||||
|
||||
let deferred = deferFulfillment(context.observe(\.viewState.notificationSettingsState)) { state in
|
||||
switch state {
|
||||
@@ -649,16 +687,17 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
if case .loaded(let newNotificationSettingsState) = viewModel.state.notificationSettingsState {
|
||||
XCTAssertFalse(newNotificationSettingsState.isDefault)
|
||||
XCTAssertEqual(newNotificationSettingsState.mode, .allMessages)
|
||||
#expect(!newNotificationSettingsState.isDefault)
|
||||
#expect(newNotificationSettingsState.mode == .allMessages)
|
||||
} else {
|
||||
XCTFail("invalid state")
|
||||
Issue.record("invalid state")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Knock Requests
|
||||
|
||||
func testKnockRequestsCounter() async throws {
|
||||
@Test
|
||||
mutating func knockRequestsCounter() async throws {
|
||||
ServiceLocator.shared.settings.knockingEnabled = true
|
||||
let mockedRequests: [KnockRequestProxyMock] = [.init(), .init()]
|
||||
roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isDirect: false, knockRequestsState: .loaded(mockedRequests), joinRule: .knock))
|
||||
@@ -680,7 +719,8 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
||||
try await deferredAction.fulfill()
|
||||
}
|
||||
|
||||
func testKnockRequestsCounterIsLoading() async throws {
|
||||
@Test
|
||||
mutating func knockRequestsCounterIsLoading() async throws {
|
||||
ServiceLocator.shared.settings.knockingEnabled = true
|
||||
roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isDirect: false, knockRequestsState: .loading, joinRule: .knock))
|
||||
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
|
||||
@@ -698,7 +738,8 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testKnockRequestsCounterIsNotShownIfNoPermissions() async throws {
|
||||
@Test
|
||||
mutating func knockRequestsCounterIsNotShownIfNoPermissions() async throws {
|
||||
ServiceLocator.shared.settings.knockingEnabled = true
|
||||
let mockedRequests: [KnockRequestProxyMock] = [.init(), .init()]
|
||||
roomProxyMock = JoinedRoomProxyMock(.init(name: "Test",
|
||||
@@ -724,7 +765,8 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testKnockRequestsCounterIsNotShownIfDM() async throws {
|
||||
@Test
|
||||
mutating func knockRequestsCounterIsNotShownIfDM() async throws {
|
||||
ServiceLocator.shared.settings.knockingEnabled = true
|
||||
let mockedRequests: [KnockRequestProxyMock] = [.init(), .init()]
|
||||
let mockedMembers: [RoomMemberProxyMock] = [.mockMe, .mockAlice]
|
||||
@@ -749,7 +791,8 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
||||
|
||||
// MARK: - History Sharing
|
||||
|
||||
func testHistorySharingPillDoesNotAppearIfFeatureFlagNotSet() async throws {
|
||||
@Test
|
||||
mutating func historySharingPillDoesNotAppearIfFeatureFlagNotSet() async throws {
|
||||
ServiceLocator.shared.settings.enableKeyShareOnInvite = false
|
||||
|
||||
let configuration = JoinedRoomProxyMockConfiguration(historyVisibility: .shared)
|
||||
@@ -766,14 +809,15 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
||||
appSettings: ServiceLocator.shared.settings)
|
||||
|
||||
let deferredInvisible = deferFailure(context.observe(\.viewState),
|
||||
timeout: 1,
|
||||
timeout: .seconds(1),
|
||||
message: "The pill should not be shown as the feature flag is not set") { state in
|
||||
state.details.historySharingState != nil
|
||||
}
|
||||
try await deferredInvisible.fulfill()
|
||||
}
|
||||
|
||||
func testHistorySharingPillDisplayedIfHistoryVisibilityShared() async throws {
|
||||
@Test
|
||||
mutating func historySharingPillDisplayedIfHistoryVisibilityShared() async throws {
|
||||
ServiceLocator.shared.settings.enableKeyShareOnInvite = true
|
||||
|
||||
let configuration = JoinedRoomProxyMockConfiguration(historyVisibility: .shared)
|
||||
|
||||
@@ -9,161 +9,171 @@
|
||||
import Combine
|
||||
@testable import ElementX
|
||||
import MatrixRustSDKMocks
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@Suite
|
||||
@MainActor
|
||||
class RoomFlowCoordinatorTests: XCTestCase {
|
||||
final class RoomFlowCoordinatorTests {
|
||||
var clientProxy: ClientProxyMock!
|
||||
var timelineControllerFactory: TimelineControllerFactoryMock!
|
||||
var roomFlowCoordinator: RoomFlowCoordinator!
|
||||
var navigationStackCoordinator: NavigationStackCoordinator!
|
||||
var cancellables = Set<AnyCancellable>()
|
||||
|
||||
override func tearDown() {
|
||||
deinit {
|
||||
AppSettings.resetAllSettings()
|
||||
}
|
||||
|
||||
func testRoomPresentation() async throws {
|
||||
@Test
|
||||
func roomPresentation() async throws {
|
||||
setupRoomFlowCoordinator()
|
||||
|
||||
try await process(route: .room(roomID: "1", via: []))
|
||||
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||
|
||||
try await clearRoute(expectedActions: [.finished])
|
||||
XCTAssertNil(navigationStackCoordinator.rootCoordinator)
|
||||
#expect(navigationStackCoordinator.rootCoordinator == nil)
|
||||
}
|
||||
|
||||
func testRoomDetailsPresentation() async throws {
|
||||
@Test
|
||||
func roomDetailsPresentation() async throws {
|
||||
setupRoomFlowCoordinator()
|
||||
|
||||
try await process(route: .roomDetails(roomID: "1"))
|
||||
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomDetailsScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.rootCoordinator is RoomDetailsScreenCoordinator)
|
||||
|
||||
try await clearRoute(expectedActions: [.finished])
|
||||
XCTAssertNil(navigationStackCoordinator.rootCoordinator)
|
||||
#expect(navigationStackCoordinator.rootCoordinator == nil)
|
||||
}
|
||||
|
||||
func testNoOp() async throws {
|
||||
@Test
|
||||
func noOp() async throws {
|
||||
setupRoomFlowCoordinator()
|
||||
|
||||
try await process(route: .roomDetails(roomID: "1"))
|
||||
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomDetailsScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.rootCoordinator is RoomDetailsScreenCoordinator)
|
||||
let detailsCoordinator = navigationStackCoordinator.rootCoordinator
|
||||
|
||||
roomFlowCoordinator.handleAppRoute(.roomDetails(roomID: "1"), animated: true)
|
||||
await Task.yield()
|
||||
|
||||
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomDetailsScreenCoordinator)
|
||||
XCTAssert(navigationStackCoordinator.rootCoordinator === detailsCoordinator)
|
||||
#expect(navigationStackCoordinator.rootCoordinator is RoomDetailsScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.rootCoordinator === detailsCoordinator)
|
||||
}
|
||||
|
||||
func testPushDetails() async throws {
|
||||
@Test
|
||||
func pushDetails() async throws {
|
||||
setupRoomFlowCoordinator()
|
||||
|
||||
try await process(route: .room(roomID: "1", via: []))
|
||||
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 0)
|
||||
#expect(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.stackCoordinators.count == 0)
|
||||
|
||||
try await process(route: .roomDetails(roomID: "1"))
|
||||
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 1)
|
||||
XCTAssert(navigationStackCoordinator.stackCoordinators.first is RoomDetailsScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.stackCoordinators.count == 1)
|
||||
#expect(navigationStackCoordinator.stackCoordinators.first is RoomDetailsScreenCoordinator)
|
||||
}
|
||||
|
||||
func testChildRoomFlow() async throws {
|
||||
@Test
|
||||
func childRoomFlow() async throws {
|
||||
setupRoomFlowCoordinator()
|
||||
|
||||
try await process(route: .room(roomID: "1", via: []))
|
||||
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 0)
|
||||
#expect(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.stackCoordinators.count == 0)
|
||||
|
||||
try await process(route: .childRoom(roomID: "2", via: []))
|
||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 1)
|
||||
XCTAssert(navigationStackCoordinator.stackCoordinators.first is RoomScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.stackCoordinators.count == 1)
|
||||
#expect(navigationStackCoordinator.stackCoordinators.first is RoomScreenCoordinator)
|
||||
|
||||
try await process(route: .childRoom(roomID: "3", via: []))
|
||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 2)
|
||||
XCTAssert(navigationStackCoordinator.stackCoordinators.first is RoomScreenCoordinator)
|
||||
XCTAssert(navigationStackCoordinator.stackCoordinators.last is RoomScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.stackCoordinators.count == 2)
|
||||
#expect(navigationStackCoordinator.stackCoordinators.first is RoomScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.stackCoordinators.last is RoomScreenCoordinator)
|
||||
|
||||
try await clearRoute(expectedActions: [.finished])
|
||||
XCTAssertNil(navigationStackCoordinator.rootCoordinator)
|
||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 0)
|
||||
#expect(navigationStackCoordinator.rootCoordinator == nil)
|
||||
#expect(navigationStackCoordinator.stackCoordinators.count == 0)
|
||||
}
|
||||
|
||||
/// Tests the child flow teardown in isolation of it's parent.
|
||||
func testChildFlowTearDown() async throws {
|
||||
@Test
|
||||
func childFlowTearDown() async throws {
|
||||
setupRoomFlowCoordinator(asChildFlow: true)
|
||||
navigationStackCoordinator.setRootCoordinator(BlankFormCoordinator())
|
||||
|
||||
try await process(route: .room(roomID: "1", via: []))
|
||||
try await process(route: .roomDetails(roomID: "1"))
|
||||
XCTAssertTrue(navigationStackCoordinator.rootCoordinator is BlankFormCoordinator, "A child room flow should push onto the stack, leaving the root alone.")
|
||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 2)
|
||||
XCTAssertTrue(navigationStackCoordinator.stackCoordinators.first is RoomScreenCoordinator)
|
||||
XCTAssertTrue(navigationStackCoordinator.stackCoordinators.last is RoomDetailsScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.rootCoordinator is BlankFormCoordinator, "A child room flow should push onto the stack, leaving the root alone.")
|
||||
#expect(navigationStackCoordinator.stackCoordinators.count == 2)
|
||||
#expect(navigationStackCoordinator.stackCoordinators.first is RoomScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.stackCoordinators.last is RoomDetailsScreenCoordinator)
|
||||
|
||||
try await clearRoute(expectedActions: [.finished])
|
||||
XCTAssertTrue(navigationStackCoordinator.rootCoordinator is BlankFormCoordinator)
|
||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 2, "A child room flow should leave its parent to clean up the stack.")
|
||||
XCTAssertTrue(navigationStackCoordinator.stackCoordinators.first is RoomScreenCoordinator, "A child room flow should leave its parent to clean up the stack.")
|
||||
XCTAssertTrue(navigationStackCoordinator.stackCoordinators.last is RoomDetailsScreenCoordinator, "A child room flow should leave its parent to clean up the stack.")
|
||||
#expect(navigationStackCoordinator.rootCoordinator is BlankFormCoordinator)
|
||||
#expect(navigationStackCoordinator.stackCoordinators.count == 2, "A child room flow should leave its parent to clean up the stack.")
|
||||
#expect(navigationStackCoordinator.stackCoordinators.first is RoomScreenCoordinator, "A child room flow should leave its parent to clean up the stack.")
|
||||
#expect(navigationStackCoordinator.stackCoordinators.last is RoomDetailsScreenCoordinator, "A child room flow should leave its parent to clean up the stack.")
|
||||
}
|
||||
|
||||
func testChildRoomMemberDetails() async throws {
|
||||
@Test
|
||||
func childRoomMemberDetails() async throws {
|
||||
setupRoomFlowCoordinator()
|
||||
|
||||
try await process(route: .room(roomID: "1", via: []))
|
||||
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 0)
|
||||
#expect(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.stackCoordinators.count == 0)
|
||||
|
||||
try await process(route: .childRoom(roomID: "2", via: []))
|
||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 1)
|
||||
XCTAssert(navigationStackCoordinator.stackCoordinators.first is RoomScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.stackCoordinators.count == 1)
|
||||
#expect(navigationStackCoordinator.stackCoordinators.first is RoomScreenCoordinator)
|
||||
|
||||
try await process(route: .roomMemberDetails(userID: RoomMemberProxyMock.mockMe.userID))
|
||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 2)
|
||||
XCTAssert(navigationStackCoordinator.stackCoordinators.first is RoomScreenCoordinator)
|
||||
XCTAssert(navigationStackCoordinator.stackCoordinators.last is RoomMemberDetailsScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.stackCoordinators.count == 2)
|
||||
#expect(navigationStackCoordinator.stackCoordinators.first is RoomScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.stackCoordinators.last is RoomMemberDetailsScreenCoordinator)
|
||||
}
|
||||
|
||||
func testChildRoomIgnoresDirectDuplicate() async throws {
|
||||
@Test
|
||||
func childRoomIgnoresDirectDuplicate() async throws {
|
||||
setupRoomFlowCoordinator()
|
||||
|
||||
try await process(route: .room(roomID: "1", via: []))
|
||||
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 0)
|
||||
#expect(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.stackCoordinators.count == 0)
|
||||
|
||||
try await process(route: .childRoom(roomID: "1", via: []))
|
||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 0,
|
||||
"A room flow shouldn't present a direct child for the same room.")
|
||||
#expect(navigationStackCoordinator.stackCoordinators.count == 0,
|
||||
"A room flow shouldn't present a direct child for the same room.")
|
||||
|
||||
try await process(route: .childRoom(roomID: "2", via: []))
|
||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 1)
|
||||
XCTAssert(navigationStackCoordinator.stackCoordinators.first is RoomScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.stackCoordinators.count == 1)
|
||||
#expect(navigationStackCoordinator.stackCoordinators.first is RoomScreenCoordinator)
|
||||
|
||||
try await process(route: .childRoom(roomID: "1", via: []))
|
||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 2,
|
||||
"Presenting the same room multiple times should be allowed when it's not a direct child of itself.")
|
||||
XCTAssert(navigationStackCoordinator.stackCoordinators.first is RoomScreenCoordinator)
|
||||
XCTAssert(navigationStackCoordinator.stackCoordinators.last is RoomScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.stackCoordinators.count == 2,
|
||||
"Presenting the same room multiple times should be allowed when it's not a direct child of itself.")
|
||||
#expect(navigationStackCoordinator.stackCoordinators.first is RoomScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.stackCoordinators.last is RoomScreenCoordinator)
|
||||
}
|
||||
|
||||
func testRoomMembershipInvite() async throws {
|
||||
@Test
|
||||
func roomMembershipInvite() async throws {
|
||||
setupRoomFlowCoordinator(roomType: .invited(roomID: "InvitedRoomID"))
|
||||
|
||||
try await process(route: .room(roomID: "InvitedRoomID", via: []))
|
||||
XCTAssert(navigationStackCoordinator.rootCoordinator is JoinRoomScreenCoordinator)
|
||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 0)
|
||||
#expect(navigationStackCoordinator.rootCoordinator is JoinRoomScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.stackCoordinators.count == 0)
|
||||
|
||||
try await clearRoute(expectedActions: [.finished])
|
||||
XCTAssertNil(navigationStackCoordinator.rootCoordinator)
|
||||
#expect(navigationStackCoordinator.rootCoordinator == nil)
|
||||
|
||||
setupRoomFlowCoordinator(roomType: .invited(roomID: "InvitedRoomID"))
|
||||
|
||||
try await process(route: .room(roomID: "InvitedRoomID", via: []))
|
||||
XCTAssert(navigationStackCoordinator.rootCoordinator is JoinRoomScreenCoordinator)
|
||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 0)
|
||||
#expect(navigationStackCoordinator.rootCoordinator is JoinRoomScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.stackCoordinators.count == 0)
|
||||
|
||||
// "Join" the room
|
||||
clientProxy.roomForIdentifierClosure = { _ in
|
||||
@@ -171,29 +181,30 @@ class RoomFlowCoordinatorTests: XCTestCase {
|
||||
}
|
||||
|
||||
try await process(route: .room(roomID: "InvitedRoomID", via: []))
|
||||
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 0)
|
||||
#expect(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.stackCoordinators.count == 0)
|
||||
}
|
||||
|
||||
func testChildRoomMembershipInvite() async throws {
|
||||
@Test
|
||||
func childRoomMembershipInvite() async throws {
|
||||
setupRoomFlowCoordinator(asChildFlow: true, roomType: .invited(roomID: "InvitedRoomID"))
|
||||
navigationStackCoordinator.setRootCoordinator(BlankFormCoordinator())
|
||||
|
||||
try await process(route: .room(roomID: "InvitedRoomID", via: []))
|
||||
XCTAssertTrue(navigationStackCoordinator.rootCoordinator is BlankFormCoordinator, "A child room flow should push onto the stack, leaving the root alone.")
|
||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 1)
|
||||
XCTAssertTrue(navigationStackCoordinator.stackCoordinators.last is JoinRoomScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.rootCoordinator is BlankFormCoordinator, "A child room flow should push onto the stack, leaving the root alone.")
|
||||
#expect(navigationStackCoordinator.stackCoordinators.count == 1)
|
||||
#expect(navigationStackCoordinator.stackCoordinators.last is JoinRoomScreenCoordinator)
|
||||
|
||||
try await clearRoute(expectedActions: [.finished])
|
||||
XCTAssertNil(navigationStackCoordinator.stackCoordinators.last, "A child room flow should remove the join room scren on dismissal")
|
||||
#expect(navigationStackCoordinator.stackCoordinators.last == nil, "A child room flow should remove the join room scren on dismissal")
|
||||
|
||||
setupRoomFlowCoordinator(asChildFlow: true, roomType: .invited(roomID: "InvitedRoomID"))
|
||||
navigationStackCoordinator.setRootCoordinator(BlankFormCoordinator())
|
||||
|
||||
try await process(route: .room(roomID: "InvitedRoomID", via: []))
|
||||
XCTAssertTrue(navigationStackCoordinator.rootCoordinator is BlankFormCoordinator, "A child room flow should push onto the stack, leaving the root alone.")
|
||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 1)
|
||||
XCTAssertTrue(navigationStackCoordinator.stackCoordinators.last is JoinRoomScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.rootCoordinator is BlankFormCoordinator, "A child room flow should push onto the stack, leaving the root alone.")
|
||||
#expect(navigationStackCoordinator.stackCoordinators.count == 1)
|
||||
#expect(navigationStackCoordinator.stackCoordinators.last is JoinRoomScreenCoordinator)
|
||||
|
||||
// "Join" the room
|
||||
clientProxy.roomForIdentifierClosure = { _ in
|
||||
@@ -201,29 +212,31 @@ class RoomFlowCoordinatorTests: XCTestCase {
|
||||
}
|
||||
|
||||
try await process(route: .room(roomID: "InvitedRoomID", via: []))
|
||||
XCTAssertTrue(navigationStackCoordinator.rootCoordinator is BlankFormCoordinator, "A child room flow should push onto the stack, leaving the root alone.")
|
||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 1)
|
||||
XCTAssertTrue(navigationStackCoordinator.stackCoordinators.last is RoomScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.rootCoordinator is BlankFormCoordinator, "A child room flow should push onto the stack, leaving the root alone.")
|
||||
#expect(navigationStackCoordinator.stackCoordinators.count == 1)
|
||||
#expect(navigationStackCoordinator.stackCoordinators.last is RoomScreenCoordinator)
|
||||
}
|
||||
|
||||
func testEventRoute() async throws {
|
||||
@Test
|
||||
func eventRoute() async throws {
|
||||
setupRoomFlowCoordinator()
|
||||
|
||||
try await process(route: .event(eventID: "1", roomID: "1", via: []))
|
||||
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 0)
|
||||
#expect(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.stackCoordinators.count == 0)
|
||||
|
||||
try await process(route: .childEvent(eventID: "2", roomID: "1", via: []))
|
||||
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 0)
|
||||
#expect(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.stackCoordinators.count == 0)
|
||||
|
||||
try await process(route: .childEvent(eventID: "3", roomID: "2", via: []))
|
||||
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 1)
|
||||
XCTAssert(navigationStackCoordinator.stackCoordinators.first is RoomScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.stackCoordinators.count == 1)
|
||||
#expect(navigationStackCoordinator.stackCoordinators.first is RoomScreenCoordinator)
|
||||
}
|
||||
|
||||
func testThreadedEventRoutes() async throws {
|
||||
@Test
|
||||
func threadedEventRoutes() async throws {
|
||||
ServiceLocator.shared.settings.threadsEnabled = true
|
||||
setupRoomFlowCoordinator()
|
||||
|
||||
@@ -243,17 +256,17 @@ class RoomFlowCoordinatorTests: XCTestCase {
|
||||
}
|
||||
|
||||
try await process(route: .event(eventID: "2", roomID: "1", via: []))
|
||||
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 1)
|
||||
XCTAssert(navigationStackCoordinator.stackCoordinators[0] is ThreadTimelineScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||
try #require(navigationStackCoordinator.stackCoordinators.count == 1) // #require these counts so accessing by index is safe.
|
||||
#expect(navigationStackCoordinator.stackCoordinators[0] is ThreadTimelineScreenCoordinator)
|
||||
|
||||
// From the thread screen, navigate to another threaded event in the same room, and in the same thread.
|
||||
let threadCoordinator = navigationStackCoordinator.stackCoordinators[0] as? ThreadTimelineScreenCoordinator
|
||||
try await process(route: .childEvent(eventID: "3", roomID: "1", via: []))
|
||||
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 1)
|
||||
XCTAssert(navigationStackCoordinator.stackCoordinators[0] is ThreadTimelineScreenCoordinator)
|
||||
XCTAssertIdentical(navigationStackCoordinator.stackCoordinators[0], threadCoordinator)
|
||||
#expect(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||
try #require(navigationStackCoordinator.stackCoordinators.count == 1)
|
||||
#expect(navigationStackCoordinator.stackCoordinators[0] is ThreadTimelineScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.stackCoordinators[0] === threadCoordinator)
|
||||
// Would be nice to test if the focusEvent function has been called but there is no way to mock that.
|
||||
|
||||
// From the thread screen, navigate to another threaded event in the same room, but in a different thread.
|
||||
@@ -261,10 +274,10 @@ class RoomFlowCoordinatorTests: XCTestCase {
|
||||
mockedEvent.threadRootEventIdReturnValue = "4"
|
||||
roomProxy.loadOrFetchEventDetailsForReturnValue = .success(mockedEvent)
|
||||
try await process(route: .childEvent(eventID: "5", roomID: "1", via: []))
|
||||
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 2)
|
||||
XCTAssert(navigationStackCoordinator.stackCoordinators[0] is ThreadTimelineScreenCoordinator)
|
||||
XCTAssert(navigationStackCoordinator.stackCoordinators[1] is ThreadTimelineScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||
try #require(navigationStackCoordinator.stackCoordinators.count == 2)
|
||||
#expect(navigationStackCoordinator.stackCoordinators[0] is ThreadTimelineScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.stackCoordinators[1] is ThreadTimelineScreenCoordinator)
|
||||
|
||||
// From the thread screen, navigate to another threaded event in a different room.
|
||||
configuration = JoinedRoomProxyMockConfiguration(id: "2")
|
||||
@@ -282,12 +295,12 @@ class RoomFlowCoordinatorTests: XCTestCase {
|
||||
}
|
||||
|
||||
try await process(route: .childEvent(eventID: "2", roomID: "2", via: []))
|
||||
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 4)
|
||||
XCTAssert(navigationStackCoordinator.stackCoordinators[0] is ThreadTimelineScreenCoordinator)
|
||||
XCTAssert(navigationStackCoordinator.stackCoordinators[1] is ThreadTimelineScreenCoordinator)
|
||||
XCTAssert(navigationStackCoordinator.stackCoordinators[2] is RoomScreenCoordinator)
|
||||
XCTAssert(navigationStackCoordinator.stackCoordinators[3] is ThreadTimelineScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||
try #require(navigationStackCoordinator.stackCoordinators.count == 4)
|
||||
#expect(navigationStackCoordinator.stackCoordinators[0] is ThreadTimelineScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.stackCoordinators[1] is ThreadTimelineScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.stackCoordinators[2] is RoomScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.stackCoordinators[3] is ThreadTimelineScreenCoordinator)
|
||||
|
||||
// From the thread screen, navigate to an event of the same room that is not threaded
|
||||
mockedEvent = TimelineEventSDKMock()
|
||||
@@ -295,66 +308,69 @@ class RoomFlowCoordinatorTests: XCTestCase {
|
||||
roomProxy.loadOrFetchEventDetailsForReturnValue = .success(mockedEvent)
|
||||
|
||||
try await process(route: .childEvent(eventID: "3", roomID: "2", via: []))
|
||||
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 5)
|
||||
XCTAssert(navigationStackCoordinator.stackCoordinators[0] is ThreadTimelineScreenCoordinator)
|
||||
XCTAssert(navigationStackCoordinator.stackCoordinators[1] is ThreadTimelineScreenCoordinator)
|
||||
XCTAssert(navigationStackCoordinator.stackCoordinators[2] is RoomScreenCoordinator)
|
||||
XCTAssert(navigationStackCoordinator.stackCoordinators[3] is ThreadTimelineScreenCoordinator)
|
||||
XCTAssert(navigationStackCoordinator.stackCoordinators[4] is RoomScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||
try #require(navigationStackCoordinator.stackCoordinators.count == 5)
|
||||
#expect(navigationStackCoordinator.stackCoordinators[0] is ThreadTimelineScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.stackCoordinators[1] is ThreadTimelineScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.stackCoordinators[2] is RoomScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.stackCoordinators[3] is ThreadTimelineScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.stackCoordinators[4] is RoomScreenCoordinator)
|
||||
}
|
||||
|
||||
func testShareMediaRoute() async throws {
|
||||
@Test
|
||||
func shareMediaRoute() async throws {
|
||||
setupRoomFlowCoordinator()
|
||||
|
||||
try await process(route: .room(roomID: "1", via: []))
|
||||
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 0)
|
||||
#expect(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.stackCoordinators.count == 0)
|
||||
|
||||
let sharePayload: ShareExtensionPayload = .mediaFiles(roomID: "1", mediaFiles: [.init(url: .picturesDirectory, suggestedName: nil)])
|
||||
try await process(route: .share(sharePayload))
|
||||
|
||||
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 0)
|
||||
#expect(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.stackCoordinators.count == 0)
|
||||
|
||||
XCTAssertTrue((navigationStackCoordinator.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is MediaUploadPreviewScreenCoordinator)
|
||||
#expect((navigationStackCoordinator.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is MediaUploadPreviewScreenCoordinator)
|
||||
|
||||
try await process(route: .childRoom(roomID: "2", via: []))
|
||||
XCTAssertNil(navigationStackCoordinator.sheetCoordinator)
|
||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 1)
|
||||
#expect(navigationStackCoordinator.sheetCoordinator == nil)
|
||||
#expect(navigationStackCoordinator.stackCoordinators.count == 1)
|
||||
|
||||
try await process(route: .share(sharePayload))
|
||||
|
||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 0)
|
||||
XCTAssertTrue((navigationStackCoordinator.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is MediaUploadPreviewScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.stackCoordinators.count == 0)
|
||||
#expect((navigationStackCoordinator.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is MediaUploadPreviewScreenCoordinator)
|
||||
}
|
||||
|
||||
func testShareTextRoute() async throws {
|
||||
@Test
|
||||
func shareTextRoute() async throws {
|
||||
setupRoomFlowCoordinator()
|
||||
|
||||
try await process(route: .room(roomID: "1", via: []))
|
||||
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 0)
|
||||
#expect(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.stackCoordinators.count == 0)
|
||||
|
||||
let sharePayload: ShareExtensionPayload = .text(roomID: "1", text: "Important text")
|
||||
try await process(route: .share(sharePayload))
|
||||
|
||||
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 0)
|
||||
#expect(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.stackCoordinators.count == 0)
|
||||
|
||||
XCTAssertNil(navigationStackCoordinator.sheetCoordinator, "The media upload sheet shouldn't be shown when sharing text.")
|
||||
#expect(navigationStackCoordinator.sheetCoordinator == nil, "The media upload sheet shouldn't be shown when sharing text.")
|
||||
|
||||
try await process(route: .childRoom(roomID: "2", via: []))
|
||||
XCTAssertNil(navigationStackCoordinator.sheetCoordinator)
|
||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 1)
|
||||
#expect(navigationStackCoordinator.sheetCoordinator == nil)
|
||||
#expect(navigationStackCoordinator.stackCoordinators.count == 1)
|
||||
|
||||
try await process(route: .share(sharePayload))
|
||||
|
||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 0)
|
||||
XCTAssertNil(navigationStackCoordinator.sheetCoordinator, "The media upload sheet shouldn't be shown when sharing text.")
|
||||
#expect(navigationStackCoordinator.stackCoordinators.count == 0)
|
||||
#expect(navigationStackCoordinator.sheetCoordinator == nil, "The media upload sheet shouldn't be shown when sharing text.")
|
||||
}
|
||||
|
||||
func testLeavingRoom() async throws {
|
||||
@Test
|
||||
func leavingRoom() async throws {
|
||||
setupRoomFlowCoordinator()
|
||||
|
||||
var configuration = JoinedRoomProxyMockConfiguration()
|
||||
@@ -381,15 +397,16 @@ class RoomFlowCoordinatorTests: XCTestCase {
|
||||
|
||||
// MARK: - Spaces
|
||||
|
||||
func testSpacePermalink() async throws {
|
||||
@Test
|
||||
func spacePermalink() async throws {
|
||||
setupRoomFlowCoordinator()
|
||||
|
||||
try await process(route: .room(roomID: "1", via: []))
|
||||
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||
|
||||
try await process(route: .childRoom(roomID: "space1", via: []))
|
||||
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssert(navigationStackCoordinator.stackCoordinators.first is SpaceScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.stackCoordinators.first is SpaceScreenCoordinator)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
@@ -9,21 +9,22 @@
|
||||
import Combine
|
||||
@testable import ElementX
|
||||
import MatrixRustSDK
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@Suite
|
||||
@MainActor
|
||||
class RoomNotificationSettingsScreenViewModelTests: XCTestCase {
|
||||
struct RoomNotificationSettingsScreenViewModelTests {
|
||||
var roomProxyMock: JoinedRoomProxyMock!
|
||||
var notificationSettingsProxyMock: NotificationSettingsProxyMock!
|
||||
var cancellables = Set<AnyCancellable>()
|
||||
|
||||
override func setUpWithError() throws {
|
||||
cancellables.removeAll()
|
||||
init() {
|
||||
roomProxyMock = JoinedRoomProxyMock(.init(name: "Test"))
|
||||
notificationSettingsProxyMock = NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration())
|
||||
}
|
||||
|
||||
func testInitialStateDefaultModeEncryptedRoom() async throws {
|
||||
@Test
|
||||
func initialStateDefaultModeEncryptedRoom() async throws {
|
||||
let roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isEncrypted: true))
|
||||
let notificationSettingsProxyMock = NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration())
|
||||
|
||||
@@ -33,19 +34,20 @@ class RoomNotificationSettingsScreenViewModelTests: XCTestCase {
|
||||
roomProxy: roomProxyMock,
|
||||
displayAsUserDefinedRoomSettings: false)
|
||||
|
||||
let deferred = deferFulfillment(viewModel.context.$viewState) { state in
|
||||
let deferred = deferFulfillment(viewModel.context.observe(\.viewState)) { state in
|
||||
state.notificationSettingsState.isLoaded
|
||||
}
|
||||
|
||||
notificationSettingsProxyMock.callbacks.send(.settingsDidChange)
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertFalse(viewModel.context.allowCustomSetting)
|
||||
XCTAssertTrue(viewModel.context.viewState.shouldDisplayMentionsOnlyDisclaimer)
|
||||
XCTAssertNotNil(viewModel.context.viewState.description(mode: .mentionsAndKeywordsOnly))
|
||||
#expect(!viewModel.context.allowCustomSetting)
|
||||
#expect(viewModel.context.viewState.shouldDisplayMentionsOnlyDisclaimer)
|
||||
#expect(viewModel.context.viewState.description(mode: .mentionsAndKeywordsOnly) != nil)
|
||||
}
|
||||
|
||||
func testInitialStateDefaultModeEncryptedRoomWithCanPushEncrypted() async throws {
|
||||
@Test
|
||||
func initialStateDefaultModeEncryptedRoomWithCanPushEncrypted() async throws {
|
||||
let roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isEncrypted: true))
|
||||
let notificationSettingsProxyMock = NotificationSettingsProxyMock(with: .init(canPushEncryptedEvents: true))
|
||||
|
||||
@@ -55,19 +57,20 @@ class RoomNotificationSettingsScreenViewModelTests: XCTestCase {
|
||||
roomProxy: roomProxyMock,
|
||||
displayAsUserDefinedRoomSettings: false)
|
||||
|
||||
let deferred = deferFulfillment(viewModel.context.$viewState) { state in
|
||||
let deferred = deferFulfillment(viewModel.context.observe(\.viewState)) { state in
|
||||
state.notificationSettingsState.isLoaded
|
||||
}
|
||||
|
||||
notificationSettingsProxyMock.callbacks.send(.settingsDidChange)
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertFalse(viewModel.context.allowCustomSetting)
|
||||
XCTAssertFalse(viewModel.context.viewState.shouldDisplayMentionsOnlyDisclaimer)
|
||||
XCTAssertNil(viewModel.context.viewState.description(mode: .mentionsAndKeywordsOnly))
|
||||
#expect(!viewModel.context.allowCustomSetting)
|
||||
#expect(!viewModel.context.viewState.shouldDisplayMentionsOnlyDisclaimer)
|
||||
#expect(viewModel.context.viewState.description(mode: .mentionsAndKeywordsOnly) == nil)
|
||||
}
|
||||
|
||||
func testInitialStateDefaultModeUnencryptedRoom() async throws {
|
||||
@Test
|
||||
func initialStateDefaultModeUnencryptedRoom() async throws {
|
||||
let roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isEncrypted: false))
|
||||
let notificationSettingsProxyMock = NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration())
|
||||
|
||||
@@ -77,39 +80,41 @@ class RoomNotificationSettingsScreenViewModelTests: XCTestCase {
|
||||
roomProxy: roomProxyMock,
|
||||
displayAsUserDefinedRoomSettings: false)
|
||||
|
||||
let deferred = deferFulfillment(viewModel.context.$viewState) { state in
|
||||
let deferred = deferFulfillment(viewModel.context.observe(\.viewState)) { state in
|
||||
state.notificationSettingsState.isLoaded
|
||||
}
|
||||
|
||||
notificationSettingsProxyMock.callbacks.send(.settingsDidChange)
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertFalse(viewModel.context.allowCustomSetting)
|
||||
XCTAssertFalse(viewModel.context.viewState.shouldDisplayMentionsOnlyDisclaimer)
|
||||
XCTAssertNil(viewModel.context.viewState.description(mode: .mentionsAndKeywordsOnly))
|
||||
#expect(!viewModel.context.allowCustomSetting)
|
||||
#expect(!viewModel.context.viewState.shouldDisplayMentionsOnlyDisclaimer)
|
||||
#expect(viewModel.context.viewState.description(mode: .mentionsAndKeywordsOnly) == nil)
|
||||
}
|
||||
|
||||
func testInitialStateCustomMode() async throws {
|
||||
@Test
|
||||
func initialStateCustomMode() async throws {
|
||||
notificationSettingsProxyMock.getNotificationSettingsRoomIdIsEncryptedIsOneToOneReturnValue = RoomNotificationSettingsProxyMock(with: .init(mode: .mentionsAndKeywordsOnly, isDefault: false))
|
||||
let viewModel = RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: notificationSettingsProxyMock,
|
||||
roomProxy: roomProxyMock,
|
||||
displayAsUserDefinedRoomSettings: false)
|
||||
let deferred = deferFulfillment(viewModel.context.$viewState) { state in
|
||||
let deferred = deferFulfillment(viewModel.context.observe(\.viewState)) { state in
|
||||
state.notificationSettingsState.isLoaded
|
||||
}
|
||||
|
||||
notificationSettingsProxyMock.callbacks.send(.settingsDidChange)
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertTrue(viewModel.context.allowCustomSetting)
|
||||
#expect(viewModel.context.allowCustomSetting)
|
||||
}
|
||||
|
||||
func testInitialStateFailure() async throws {
|
||||
@Test
|
||||
func initialStateFailure() async throws {
|
||||
notificationSettingsProxyMock.getNotificationSettingsRoomIdIsEncryptedIsOneToOneThrowableError = NotificationSettingsError.Generic(msg: "error")
|
||||
let viewModel = RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: notificationSettingsProxyMock,
|
||||
roomProxy: roomProxyMock,
|
||||
displayAsUserDefinedRoomSettings: false)
|
||||
let deferred = deferFulfillment(viewModel.context.$viewState) { state in
|
||||
let deferred = deferFulfillment(viewModel.context.observe(\.viewState)) { state in
|
||||
state.notificationSettingsState.isError
|
||||
}
|
||||
|
||||
@@ -119,25 +124,25 @@ class RoomNotificationSettingsScreenViewModelTests: XCTestCase {
|
||||
let expectedAlertInfo = AlertInfo(id: RoomNotificationSettingsScreenErrorType.loadingSettingsFailed,
|
||||
title: L10n.commonError,
|
||||
message: L10n.screenRoomNotificationSettingsErrorLoadingSettings)
|
||||
XCTAssertEqual(viewModel.context.viewState.bindings.alertInfo?.id, expectedAlertInfo.id)
|
||||
XCTAssertEqual(viewModel.context.viewState.bindings.alertInfo?.title, expectedAlertInfo.title)
|
||||
XCTAssertEqual(viewModel.context.viewState.bindings.alertInfo?.message, expectedAlertInfo.message)
|
||||
#expect(viewModel.context.viewState.bindings.alertInfo?.id == expectedAlertInfo.id)
|
||||
#expect(viewModel.context.viewState.bindings.alertInfo?.title == expectedAlertInfo.title)
|
||||
#expect(viewModel.context.viewState.bindings.alertInfo?.message == expectedAlertInfo.message)
|
||||
}
|
||||
|
||||
func testToggleAllCustomSettingOff() async throws {
|
||||
@Test
|
||||
func toggleAllCustomSettingOff() async throws {
|
||||
notificationSettingsProxyMock.getNotificationSettingsRoomIdIsEncryptedIsOneToOneReturnValue = RoomNotificationSettingsProxyMock(with: .init(mode: .mentionsAndKeywordsOnly, isDefault: false))
|
||||
let viewModel = RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: notificationSettingsProxyMock,
|
||||
roomProxy: roomProxyMock,
|
||||
displayAsUserDefinedRoomSettings: false)
|
||||
let deferred = deferFulfillment(viewModel.context.$viewState) { state in
|
||||
let deferred = deferFulfillment(viewModel.context.observe(\.viewState)) { state in
|
||||
state.notificationSettingsState.isLoaded
|
||||
}
|
||||
|
||||
notificationSettingsProxyMock.callbacks.send(.settingsDidChange)
|
||||
try await deferred.fulfill()
|
||||
|
||||
let deferredIsRestoringDefaultSettings = deferFulfillment(viewModel.context.$viewState,
|
||||
keyPath: \.isRestoringDefaultSetting,
|
||||
let deferredIsRestoringDefaultSettings = deferFulfillment(viewModel.context.observe(\.viewState.isRestoringDefaultSetting),
|
||||
transitionValues: [false, true, false])
|
||||
|
||||
viewModel.state.bindings.allowCustomSetting = false
|
||||
@@ -145,18 +150,19 @@ class RoomNotificationSettingsScreenViewModelTests: XCTestCase {
|
||||
|
||||
try await deferredIsRestoringDefaultSettings.fulfill()
|
||||
|
||||
XCTAssertEqual(notificationSettingsProxyMock.restoreDefaultNotificationModeRoomIdReceivedRoomId, roomProxyMock.id)
|
||||
XCTAssertEqual(notificationSettingsProxyMock.restoreDefaultNotificationModeRoomIdCallsCount, 1)
|
||||
#expect(notificationSettingsProxyMock.restoreDefaultNotificationModeRoomIdReceivedRoomId == roomProxyMock.id)
|
||||
#expect(notificationSettingsProxyMock.restoreDefaultNotificationModeRoomIdCallsCount == 1)
|
||||
}
|
||||
|
||||
func testToggleAllCustomSettingOffOn() async throws {
|
||||
@Test
|
||||
func toggleAllCustomSettingOffOn() async throws {
|
||||
let notificationSettingsProxyMock = NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration())
|
||||
notificationSettingsProxyMock.getNotificationSettingsRoomIdIsEncryptedIsOneToOneReturnValue = RoomNotificationSettingsProxyMock(with: .init(mode: .mentionsAndKeywordsOnly, isDefault: true))
|
||||
let viewModel = RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: notificationSettingsProxyMock,
|
||||
roomProxy: roomProxyMock,
|
||||
displayAsUserDefinedRoomSettings: false)
|
||||
|
||||
var deferred = deferFulfillment(viewModel.context.$viewState) { state in
|
||||
var deferred = deferFulfillment(viewModel.context.observe(\.viewState)) { state in
|
||||
state.notificationSettingsState.isLoaded
|
||||
}
|
||||
|
||||
@@ -164,82 +170,75 @@ class RoomNotificationSettingsScreenViewModelTests: XCTestCase {
|
||||
|
||||
try await deferred.fulfill()
|
||||
|
||||
deferred = deferFulfillment(viewModel.context.$viewState) { state in
|
||||
deferred = deferFulfillment(viewModel.context.observe(\.viewState)) { state in
|
||||
state.notificationSettingsState.isLoaded
|
||||
}
|
||||
|
||||
viewModel.state.bindings.allowCustomSetting = true
|
||||
viewModel.context.send(viewAction: .changedAllowCustomSettings)
|
||||
|
||||
await waitForConfirmation { confirmation in
|
||||
notificationSettingsProxyMock.setNotificationModeRoomIdModeClosure = { id, mode in
|
||||
#expect(id == roomProxyMock.id)
|
||||
#expect(mode == .mentionsAndKeywordsOnly)
|
||||
confirmation()
|
||||
}
|
||||
}
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertEqual(notificationSettingsProxyMock.setNotificationModeRoomIdModeReceivedArguments?.0, roomProxyMock.id)
|
||||
XCTAssertEqual(notificationSettingsProxyMock.setNotificationModeRoomIdModeReceivedArguments?.1, .mentionsAndKeywordsOnly)
|
||||
XCTAssertEqual(notificationSettingsProxyMock.setNotificationModeRoomIdModeCallsCount, 1)
|
||||
}
|
||||
|
||||
func testSetCustomMode() async throws {
|
||||
@Test
|
||||
func setCustomMode() async throws {
|
||||
notificationSettingsProxyMock.getNotificationSettingsRoomIdIsEncryptedIsOneToOneReturnValue = RoomNotificationSettingsProxyMock(with: .init(mode: .mentionsAndKeywordsOnly, isDefault: false))
|
||||
let viewModel = RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: notificationSettingsProxyMock,
|
||||
roomProxy: roomProxyMock,
|
||||
displayAsUserDefinedRoomSettings: false)
|
||||
|
||||
let deferredState = deferFulfillment(viewModel.context.$viewState) { state in
|
||||
let deferred = deferFulfillment(viewModel.context.observe(\.viewState)) { state in
|
||||
state.notificationSettingsState.isLoaded
|
||||
}
|
||||
|
||||
notificationSettingsProxyMock.callbacks.send(.settingsDidChange)
|
||||
try await deferredState.fulfill()
|
||||
|
||||
do {
|
||||
viewModel.context.send(viewAction: .setCustomMode(.allMessages))
|
||||
|
||||
let deferredState = deferFulfillment(viewModel.context.$viewState) { state in
|
||||
state.pendingCustomMode == nil
|
||||
}
|
||||
|
||||
try await deferredState.fulfill()
|
||||
|
||||
XCTAssertEqual(notificationSettingsProxyMock.setNotificationModeRoomIdModeReceivedArguments?.0, roomProxyMock.id)
|
||||
XCTAssertEqual(notificationSettingsProxyMock.setNotificationModeRoomIdModeReceivedArguments?.1, .allMessages)
|
||||
XCTAssertEqual(notificationSettingsProxyMock.setNotificationModeRoomIdModeCallsCount, 1)
|
||||
}
|
||||
try await deferred.fulfill()
|
||||
|
||||
do {
|
||||
viewModel.context.send(viewAction: .setCustomMode(.mute))
|
||||
|
||||
let deferredState = deferFulfillment(viewModel.context.$viewState) { state in
|
||||
state.pendingCustomMode == nil
|
||||
}
|
||||
|
||||
try await deferredState.fulfill()
|
||||
|
||||
XCTAssertEqual(notificationSettingsProxyMock.setNotificationModeRoomIdModeReceivedArguments?.0, roomProxyMock.id)
|
||||
XCTAssertEqual(notificationSettingsProxyMock.setNotificationModeRoomIdModeReceivedArguments?.1, .mute)
|
||||
XCTAssertEqual(notificationSettingsProxyMock.setNotificationModeRoomIdModeCallsCount, 2)
|
||||
}
|
||||
var deferredMode = deferFulfillment(viewModel.context.observe(\.viewState.pendingCustomMode),
|
||||
transitionValues: [nil, .allMessages, nil])
|
||||
viewModel.context.send(viewAction: .setCustomMode(.allMessages))
|
||||
|
||||
do {
|
||||
viewModel.context.send(viewAction: .setCustomMode(.mentionsAndKeywordsOnly))
|
||||
|
||||
let deferredState = deferFulfillment(viewModel.context.$viewState) { state in
|
||||
state.pendingCustomMode == nil
|
||||
}
|
||||
|
||||
try await deferredState.fulfill()
|
||||
|
||||
XCTAssertEqual(notificationSettingsProxyMock.setNotificationModeRoomIdModeReceivedArguments?.0, roomProxyMock.id)
|
||||
XCTAssertEqual(notificationSettingsProxyMock.setNotificationModeRoomIdModeReceivedArguments?.1, .mentionsAndKeywordsOnly)
|
||||
XCTAssertEqual(notificationSettingsProxyMock.setNotificationModeRoomIdModeCallsCount, 3)
|
||||
}
|
||||
try await deferredMode.fulfill()
|
||||
|
||||
#expect(notificationSettingsProxyMock.setNotificationModeRoomIdModeReceivedArguments?.0 == roomProxyMock.id)
|
||||
#expect(notificationSettingsProxyMock.setNotificationModeRoomIdModeReceivedArguments?.1 == .allMessages)
|
||||
#expect(notificationSettingsProxyMock.setNotificationModeRoomIdModeCallsCount == 1)
|
||||
|
||||
deferredMode = deferFulfillment(viewModel.context.observe(\.viewState.pendingCustomMode),
|
||||
transitionValues: [nil, .mute, nil])
|
||||
viewModel.context.send(viewAction: .setCustomMode(.mute))
|
||||
|
||||
try await deferredMode.fulfill()
|
||||
|
||||
#expect(notificationSettingsProxyMock.setNotificationModeRoomIdModeReceivedArguments?.0 == roomProxyMock.id)
|
||||
#expect(notificationSettingsProxyMock.setNotificationModeRoomIdModeReceivedArguments?.1 == .mute)
|
||||
#expect(notificationSettingsProxyMock.setNotificationModeRoomIdModeCallsCount == 2)
|
||||
|
||||
deferredMode = deferFulfillment(viewModel.context.observe(\.viewState.pendingCustomMode),
|
||||
transitionValues: [nil, .mentionsAndKeywordsOnly, nil])
|
||||
viewModel.context.send(viewAction: .setCustomMode(.mentionsAndKeywordsOnly))
|
||||
|
||||
try await deferredMode.fulfill()
|
||||
|
||||
#expect(notificationSettingsProxyMock.setNotificationModeRoomIdModeReceivedArguments?.0 == roomProxyMock.id)
|
||||
#expect(notificationSettingsProxyMock.setNotificationModeRoomIdModeReceivedArguments?.1 == .mentionsAndKeywordsOnly)
|
||||
#expect(notificationSettingsProxyMock.setNotificationModeRoomIdModeCallsCount == 3)
|
||||
}
|
||||
|
||||
func testDeleteCustomSettingTapped() async throws {
|
||||
@Test
|
||||
mutating func deleteCustomSettingTapped() async throws {
|
||||
notificationSettingsProxyMock.getNotificationSettingsRoomIdIsEncryptedIsOneToOneReturnValue = RoomNotificationSettingsProxyMock(with: .init(mode: .mentionsAndKeywordsOnly, isDefault: false))
|
||||
let viewModel = RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: notificationSettingsProxyMock,
|
||||
roomProxy: roomProxyMock,
|
||||
displayAsUserDefinedRoomSettings: true)
|
||||
let deferred = deferFulfillment(viewModel.context.$viewState) { state in
|
||||
let deferred = deferFulfillment(viewModel.context.observe(\.viewState)) { state in
|
||||
state.notificationSettingsState.isLoaded
|
||||
}
|
||||
|
||||
@@ -253,8 +252,7 @@ class RoomNotificationSettingsScreenViewModelTests: XCTestCase {
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
let deferredViewState = deferFulfillment(viewModel.context.$viewState,
|
||||
keyPath: \.deletingCustomSetting,
|
||||
let deferredViewState = deferFulfillment(viewModel.context.observe(\.viewState.deletingCustomSetting),
|
||||
transitionValues: [false, true, false])
|
||||
|
||||
viewModel.context.send(viewAction: .deleteCustomSettingTapped)
|
||||
@@ -262,21 +260,22 @@ class RoomNotificationSettingsScreenViewModelTests: XCTestCase {
|
||||
try await deferredViewState.fulfill()
|
||||
|
||||
// the `dismiss` action must have been sent
|
||||
XCTAssertEqual(actionSent, .dismiss)
|
||||
#expect(actionSent == .dismiss)
|
||||
// `restoreDefaultNotificationMode` should have been called
|
||||
XCTAssert(notificationSettingsProxyMock.restoreDefaultNotificationModeRoomIdCalled)
|
||||
XCTAssertEqual(notificationSettingsProxyMock.restoreDefaultNotificationModeRoomIdReceivedInvocations, [roomProxyMock.id])
|
||||
#expect(notificationSettingsProxyMock.restoreDefaultNotificationModeRoomIdCalled)
|
||||
#expect(notificationSettingsProxyMock.restoreDefaultNotificationModeRoomIdReceivedInvocations == [roomProxyMock.id])
|
||||
// and no alert is expected
|
||||
XCTAssertNil(viewModel.context.alertInfo)
|
||||
#expect(viewModel.context.alertInfo == nil)
|
||||
}
|
||||
|
||||
func testDeleteCustomSettingTappedFailure() async throws {
|
||||
@Test
|
||||
mutating func deleteCustomSettingTappedFailure() async throws {
|
||||
notificationSettingsProxyMock.getNotificationSettingsRoomIdIsEncryptedIsOneToOneReturnValue = RoomNotificationSettingsProxyMock(with: .init(mode: .mentionsAndKeywordsOnly, isDefault: false))
|
||||
notificationSettingsProxyMock.restoreDefaultNotificationModeRoomIdThrowableError = NotificationSettingsError.Generic(msg: "error")
|
||||
let viewModel = RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: notificationSettingsProxyMock,
|
||||
roomProxy: roomProxyMock,
|
||||
displayAsUserDefinedRoomSettings: true)
|
||||
let deferred = deferFulfillment(viewModel.context.$viewState) { state in
|
||||
let deferred = deferFulfillment(viewModel.context.observe(\.viewState)) { state in
|
||||
state.notificationSettingsState.isLoaded
|
||||
}
|
||||
|
||||
@@ -290,8 +289,7 @@ class RoomNotificationSettingsScreenViewModelTests: XCTestCase {
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
let deferredViewState = deferFulfillment(viewModel.context.$viewState,
|
||||
keyPath: \.deletingCustomSetting,
|
||||
let deferredViewState = deferFulfillment(viewModel.context.observe(\.viewState.deletingCustomSetting),
|
||||
transitionValues: [false, true, false])
|
||||
|
||||
viewModel.context.send(viewAction: .deleteCustomSettingTapped)
|
||||
@@ -299,8 +297,8 @@ class RoomNotificationSettingsScreenViewModelTests: XCTestCase {
|
||||
try await deferredViewState.fulfill()
|
||||
|
||||
// an alert is expected
|
||||
XCTAssertEqual(viewModel.context.alertInfo?.id, .restoreDefaultFailed)
|
||||
#expect(viewModel.context.alertInfo?.id == .restoreDefaultFailed)
|
||||
// the `dismiss` action must not have been sent
|
||||
XCTAssertNil(actionSent)
|
||||
#expect(actionSent == nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,15 +7,17 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
@Suite
|
||||
@MainActor
|
||||
class RoomPollsHistoryScreenViewModelTests: XCTestCase {
|
||||
struct RoomPollsHistoryScreenViewModelTests {
|
||||
var viewModel: RoomPollsHistoryScreenViewModelProtocol!
|
||||
var interactionHandler: PollInteractionHandlerMock!
|
||||
var timelineController: MockTimelineController!
|
||||
|
||||
override func setUpWithError() throws {
|
||||
init() throws {
|
||||
interactionHandler = PollInteractionHandlerMock()
|
||||
timelineController = MockTimelineController()
|
||||
viewModel = RoomPollsHistoryScreenViewModel(pollInteractionHandler: interactionHandler,
|
||||
@@ -23,7 +25,8 @@ class RoomPollsHistoryScreenViewModelTests: XCTestCase {
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
}
|
||||
|
||||
func testBackPaginate() async throws {
|
||||
@Test
|
||||
func backPaginate() async throws {
|
||||
timelineController.backPaginationResponses = [
|
||||
[PollRoomTimelineItem.mock(poll: .emptyDisclosed, isEditable: true),
|
||||
PollRoomTimelineItem.mock(poll: .disclosed(createdByAccountOwner: true)),
|
||||
@@ -37,11 +40,12 @@ class RoomPollsHistoryScreenViewModelTests: XCTestCase {
|
||||
|
||||
try await deferredViewState.fulfill()
|
||||
|
||||
XCTAssertEqual(viewModel.context.viewState.pollTimelineItems.count, 3)
|
||||
XCTAssertFalse(viewModel.context.viewState.canBackPaginate)
|
||||
#expect(viewModel.context.viewState.pollTimelineItems.count == 3)
|
||||
#expect(!viewModel.context.viewState.canBackPaginate)
|
||||
}
|
||||
|
||||
func testBackPaginateCanBackPaginate() async throws {
|
||||
@Test
|
||||
func backPaginateCanBackPaginate() async throws {
|
||||
timelineController.backPaginationResponses = [
|
||||
[PollRoomTimelineItem.mock(poll: .emptyDisclosed, isEditable: true),
|
||||
PollRoomTimelineItem.mock(poll: .disclosed(createdByAccountOwner: true)),
|
||||
@@ -56,11 +60,12 @@ class RoomPollsHistoryScreenViewModelTests: XCTestCase {
|
||||
|
||||
try await deferredViewState.fulfill()
|
||||
|
||||
XCTAssertEqual(viewModel.context.viewState.pollTimelineItems.count, 3)
|
||||
XCTAssert(viewModel.context.viewState.canBackPaginate)
|
||||
#expect(viewModel.context.viewState.pollTimelineItems.count == 3)
|
||||
#expect(viewModel.context.viewState.canBackPaginate)
|
||||
}
|
||||
|
||||
func testBackPaginateTwice() async throws {
|
||||
@Test
|
||||
func backPaginateTwice() async throws {
|
||||
timelineController.backPaginationResponses = [
|
||||
[PollRoomTimelineItem.mock(poll: .emptyDisclosed, isEditable: true),
|
||||
PollRoomTimelineItem.mock(poll: .disclosed(createdByAccountOwner: true)),
|
||||
@@ -74,11 +79,12 @@ class RoomPollsHistoryScreenViewModelTests: XCTestCase {
|
||||
|
||||
try await deferredViewState.fulfill()
|
||||
|
||||
XCTAssertEqual(viewModel.context.viewState.pollTimelineItems.count, 3)
|
||||
XCTAssert(viewModel.context.viewState.canBackPaginate)
|
||||
#expect(viewModel.context.viewState.pollTimelineItems.count == 3)
|
||||
#expect(viewModel.context.viewState.canBackPaginate)
|
||||
}
|
||||
|
||||
func testFilters() async throws {
|
||||
@Test
|
||||
func filters() async throws {
|
||||
timelineController.backPaginationResponses = [
|
||||
[PollRoomTimelineItem.mock(poll: .emptyDisclosed, isEditable: true),
|
||||
PollRoomTimelineItem.mock(poll: .disclosed(createdByAccountOwner: true)),
|
||||
@@ -96,13 +102,14 @@ class RoomPollsHistoryScreenViewModelTests: XCTestCase {
|
||||
|
||||
try await deferredViewState.fulfill()
|
||||
|
||||
XCTAssertEqual(viewModel.context.viewState.pollTimelineItems.count, 3)
|
||||
#expect(viewModel.context.viewState.pollTimelineItems.count == 3)
|
||||
|
||||
viewModel.context.send(viewAction: .filter(.past))
|
||||
XCTAssertEqual(viewModel.context.viewState.pollTimelineItems.count, 1)
|
||||
#expect(viewModel.context.viewState.pollTimelineItems.count == 1)
|
||||
}
|
||||
|
||||
func testEndPoll() async throws {
|
||||
@Test
|
||||
func endPoll() async throws {
|
||||
let deferred = deferFulfillment(interactionHandler.publisher.delay(for: 0.1, scheduler: DispatchQueue.main)) { _ in true }
|
||||
|
||||
interactionHandler.endPollPollStartIDReturnValue = .success(())
|
||||
@@ -110,11 +117,12 @@ class RoomPollsHistoryScreenViewModelTests: XCTestCase {
|
||||
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssert(interactionHandler.endPollPollStartIDCalled)
|
||||
XCTAssertEqual(interactionHandler.endPollPollStartIDReceivedPollStartID, "somePollID")
|
||||
#expect(interactionHandler.endPollPollStartIDCalled)
|
||||
#expect(interactionHandler.endPollPollStartIDReceivedPollStartID == "somePollID")
|
||||
}
|
||||
|
||||
func testEndPollFailure() async throws {
|
||||
@Test
|
||||
func endPollFailure() async throws {
|
||||
let deferred = deferFulfillment(viewModel.context.$viewState) { value in
|
||||
value.bindings.alertInfo != nil
|
||||
}
|
||||
@@ -124,11 +132,12 @@ class RoomPollsHistoryScreenViewModelTests: XCTestCase {
|
||||
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssert(interactionHandler.endPollPollStartIDCalled)
|
||||
XCTAssertEqual(interactionHandler.endPollPollStartIDReceivedPollStartID, "somePollID")
|
||||
#expect(interactionHandler.endPollPollStartIDCalled)
|
||||
#expect(interactionHandler.endPollPollStartIDReceivedPollStartID == "somePollID")
|
||||
}
|
||||
|
||||
func testSendPollResponse() async throws {
|
||||
@Test
|
||||
func sendPollResponse() async throws {
|
||||
let deferred = deferFulfillment(interactionHandler.publisher.delay(for: 0.1, scheduler: DispatchQueue.main)) { _ in true }
|
||||
|
||||
interactionHandler.sendPollResponsePollStartIDOptionIDReturnValue = .success(())
|
||||
@@ -136,12 +145,13 @@ class RoomPollsHistoryScreenViewModelTests: XCTestCase {
|
||||
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssert(interactionHandler.sendPollResponsePollStartIDOptionIDCalled)
|
||||
XCTAssertEqual(interactionHandler.sendPollResponsePollStartIDOptionIDReceivedInvocations[0].pollStartID, "somePollID")
|
||||
XCTAssertEqual(interactionHandler.sendPollResponsePollStartIDOptionIDReceivedInvocations[0].optionID, "someOptionID")
|
||||
#expect(interactionHandler.sendPollResponsePollStartIDOptionIDCalled)
|
||||
#expect(interactionHandler.sendPollResponsePollStartIDOptionIDReceivedInvocations[0].pollStartID == "somePollID")
|
||||
#expect(interactionHandler.sendPollResponsePollStartIDOptionIDReceivedInvocations[0].optionID == "someOptionID")
|
||||
}
|
||||
|
||||
func testSendPollResponseFailure() async throws {
|
||||
@Test
|
||||
func sendPollResponseFailure() async throws {
|
||||
let deferred = deferFulfillment(viewModel.context.$viewState) { value in
|
||||
value.bindings.alertInfo != nil
|
||||
}
|
||||
@@ -151,12 +161,13 @@ class RoomPollsHistoryScreenViewModelTests: XCTestCase {
|
||||
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssert(interactionHandler.sendPollResponsePollStartIDOptionIDCalled)
|
||||
XCTAssertEqual(interactionHandler.sendPollResponsePollStartIDOptionIDReceivedInvocations[0].pollStartID, "somePollID")
|
||||
XCTAssertEqual(interactionHandler.sendPollResponsePollStartIDOptionIDReceivedInvocations[0].optionID, "someOptionID")
|
||||
#expect(interactionHandler.sendPollResponsePollStartIDOptionIDCalled)
|
||||
#expect(interactionHandler.sendPollResponsePollStartIDOptionIDReceivedInvocations[0].pollStartID == "somePollID")
|
||||
#expect(interactionHandler.sendPollResponsePollStartIDOptionIDReceivedInvocations[0].optionID == "someOptionID")
|
||||
}
|
||||
|
||||
func testEditPoll() async throws {
|
||||
@Test
|
||||
func editPoll() async throws {
|
||||
let expectedPoll: Poll = .emptyDisclosed
|
||||
let expectedPollStartID = "someEventID"
|
||||
|
||||
|
||||
@@ -8,31 +8,33 @@
|
||||
|
||||
import Combine
|
||||
@testable import ElementX
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
import MatrixRustSDKMocks
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@Suite
|
||||
@MainActor
|
||||
class RoomScreenViewModelTests: XCTestCase {
|
||||
final class RoomScreenViewModelTests {
|
||||
private var viewModel: RoomScreenViewModel!
|
||||
|
||||
override func setUp() async throws {
|
||||
init() async throws {
|
||||
AppSettings.resetAllSettings()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
viewModel = nil
|
||||
deinit {
|
||||
AppSettings.resetAllSettings()
|
||||
}
|
||||
|
||||
func testPinnedEventsBanner() async throws {
|
||||
@Test
|
||||
func pinnedEventsBanner() async throws {
|
||||
var configuration = JoinedRoomProxyMockConfiguration()
|
||||
let timelineSubject = PassthroughSubject<TimelineProxyProtocol, Never>()
|
||||
let (stream, continuation) = AsyncStream.makeStream(of: TimelineProxyProtocol.self)
|
||||
let infoSubject = CurrentValueSubject<RoomInfoProxyProtocol, Never>(RoomInfoProxyMock(configuration))
|
||||
let roomProxyMock = JoinedRoomProxyMock(configuration)
|
||||
// setup a way to inject the mock of the pinned events timeline
|
||||
roomProxyMock.pinnedEventsTimelineClosure = {
|
||||
guard let timeline = await timelineSubject.values.first() else {
|
||||
guard let timeline = await stream.first() else {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
@@ -55,8 +57,8 @@ class RoomScreenViewModelTests: XCTestCase {
|
||||
viewState.pinnedEventsBannerState.count == 0
|
||||
}
|
||||
try await deferred.fulfill()
|
||||
XCTAssertTrue(viewModel.context.viewState.pinnedEventsBannerState.isLoading)
|
||||
XCTAssertFalse(viewModel.context.viewState.shouldShowPinnedEventsBanner)
|
||||
#expect(viewModel.context.viewState.pinnedEventsBannerState.isLoading)
|
||||
#expect(!viewModel.context.viewState.shouldShowPinnedEventsBanner)
|
||||
|
||||
// check if if after the pinned event ids are set the banner is still in a loading state, but is both loading and showing with a counter
|
||||
deferred = deferFulfillment(viewModel.context.$viewState) { viewState in
|
||||
@@ -65,9 +67,9 @@ class RoomScreenViewModelTests: XCTestCase {
|
||||
configuration.pinnedEventIDs = ["test1", "test2"]
|
||||
infoSubject.send(RoomInfoProxyMock(configuration))
|
||||
try await deferred.fulfill()
|
||||
XCTAssertTrue(viewModel.context.viewState.pinnedEventsBannerState.isLoading)
|
||||
XCTAssertTrue(viewModel.context.viewState.shouldShowPinnedEventsBanner)
|
||||
XCTAssertEqual(viewModel.context.viewState.pinnedEventsBannerState.selectedPinnedIndex, 1)
|
||||
#expect(viewModel.context.viewState.pinnedEventsBannerState.isLoading)
|
||||
#expect(viewModel.context.viewState.shouldShowPinnedEventsBanner)
|
||||
#expect(viewModel.context.viewState.pinnedEventsBannerState.selectedPinnedIndex == 1)
|
||||
|
||||
// setup the loaded pinned events injection in the timeline
|
||||
let pinnedTimelineMock = TimelineProxyMock()
|
||||
@@ -82,11 +84,11 @@ class RoomScreenViewModelTests: XCTestCase {
|
||||
deferred = deferFulfillment(viewModel.context.$viewState) { viewState in
|
||||
!viewState.pinnedEventsBannerState.isLoading
|
||||
}
|
||||
timelineSubject.send(pinnedTimelineMock)
|
||||
continuation.yield(pinnedTimelineMock)
|
||||
try await deferred.fulfill()
|
||||
XCTAssertEqual(viewModel.context.viewState.pinnedEventsBannerState.count, 2)
|
||||
XCTAssertTrue(viewModel.context.viewState.shouldShowPinnedEventsBanner)
|
||||
XCTAssertEqual(viewModel.context.viewState.pinnedEventsBannerState.selectedPinnedIndex, 1)
|
||||
#expect(viewModel.context.viewState.pinnedEventsBannerState.count == 2)
|
||||
#expect(viewModel.context.viewState.shouldShowPinnedEventsBanner)
|
||||
#expect(viewModel.context.viewState.pinnedEventsBannerState.selectedPinnedIndex == 1)
|
||||
|
||||
// check if the banner is updating alongside the timeline
|
||||
deferred = deferFulfillment(viewModel.context.$viewState) { viewState in
|
||||
@@ -96,19 +98,20 @@ class RoomScreenViewModelTests: XCTestCase {
|
||||
.event(.init(item: EventTimelineItem(configuration: .init(eventID: "test2")), uniqueID: .init("2"))),
|
||||
.event(.init(item: EventTimelineItem(configuration: .init(eventID: "test3")), uniqueID: .init("3")))], .initial))
|
||||
try await deferred.fulfill()
|
||||
XCTAssertFalse(viewModel.context.viewState.pinnedEventsBannerState.isLoading)
|
||||
XCTAssertTrue(viewModel.context.viewState.shouldShowPinnedEventsBanner)
|
||||
XCTAssertEqual(viewModel.context.viewState.pinnedEventsBannerState.selectedPinnedIndex, 1)
|
||||
#expect(!viewModel.context.viewState.pinnedEventsBannerState.isLoading)
|
||||
#expect(viewModel.context.viewState.shouldShowPinnedEventsBanner)
|
||||
#expect(viewModel.context.viewState.pinnedEventsBannerState.selectedPinnedIndex == 1)
|
||||
|
||||
// check how the scrolling changes the banner visibility
|
||||
viewModel.timelineHasScrolled(direction: .top)
|
||||
XCTAssertFalse(viewModel.context.viewState.shouldShowPinnedEventsBanner)
|
||||
#expect(!viewModel.context.viewState.shouldShowPinnedEventsBanner)
|
||||
|
||||
viewModel.timelineHasScrolled(direction: .bottom)
|
||||
XCTAssertTrue(viewModel.context.viewState.shouldShowPinnedEventsBanner)
|
||||
#expect(viewModel.context.viewState.shouldShowPinnedEventsBanner)
|
||||
}
|
||||
|
||||
func testPinnedEventsBannerSelection() async throws {
|
||||
@Test
|
||||
func pinnedEventsBannerSelection() async throws {
|
||||
let roomProxyMock = JoinedRoomProxyMock(.init())
|
||||
roomProxyMock.loadOrFetchEventDetailsForReturnValue = .success(TimelineEventSDKMock())
|
||||
// setup a way to inject the mock of the pinned events timeline
|
||||
@@ -135,10 +138,10 @@ class RoomScreenViewModelTests: XCTestCase {
|
||||
!viewState.pinnedEventsBannerState.isLoading
|
||||
}
|
||||
try await deferred.fulfill()
|
||||
XCTAssertEqual(viewModel.context.viewState.pinnedEventsBannerState.count, 3)
|
||||
XCTAssertTrue(viewModel.context.viewState.shouldShowPinnedEventsBanner)
|
||||
#expect(viewModel.context.viewState.pinnedEventsBannerState.count == 3)
|
||||
#expect(viewModel.context.viewState.shouldShowPinnedEventsBanner)
|
||||
// And that is actually displaying the `initialSelectedPinEventID` which is gthe first one in the list
|
||||
XCTAssertEqual(viewModel.context.viewState.pinnedEventsBannerState.selectedPinnedIndex, 0)
|
||||
#expect(viewModel.context.viewState.pinnedEventsBannerState.selectedPinnedIndex == 0)
|
||||
|
||||
// check if the banner scrolls when tapping the previous pin
|
||||
deferred = deferFulfillment(viewModel.context.$viewState) { viewState in
|
||||
@@ -162,7 +165,8 @@ class RoomScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testPinnedEventsBannerThreadedSelection() async throws {
|
||||
@Test
|
||||
func pinnedEventsBannerThreadedSelection() async throws {
|
||||
ServiceLocator.shared.settings.threadsEnabled = true
|
||||
|
||||
let roomProxyMock = JoinedRoomProxyMock(.init())
|
||||
@@ -195,10 +199,10 @@ class RoomScreenViewModelTests: XCTestCase {
|
||||
!viewState.pinnedEventsBannerState.isLoading
|
||||
}
|
||||
try await deferred.fulfill()
|
||||
XCTAssertEqual(viewModel.context.viewState.pinnedEventsBannerState.count, 3)
|
||||
XCTAssertTrue(viewModel.context.viewState.shouldShowPinnedEventsBanner)
|
||||
#expect(viewModel.context.viewState.pinnedEventsBannerState.count == 3)
|
||||
#expect(viewModel.context.viewState.shouldShowPinnedEventsBanner)
|
||||
// And that is actually displaying the `initialSelectedPinEventID` which is gthe first one in the list
|
||||
XCTAssertEqual(viewModel.context.viewState.pinnedEventsBannerState.selectedPinnedIndex, 0)
|
||||
#expect(viewModel.context.viewState.pinnedEventsBannerState.selectedPinnedIndex == 0)
|
||||
|
||||
// check if the banner scrolls when tapping the previous pin
|
||||
deferred = deferFulfillment(viewModel.context.$viewState) { viewState in
|
||||
@@ -223,7 +227,8 @@ class RoomScreenViewModelTests: XCTestCase {
|
||||
try await deferredAction2.fulfill()
|
||||
}
|
||||
|
||||
func testRoomInfoUpdate() async throws {
|
||||
@Test
|
||||
func roomInfoUpdate() async throws {
|
||||
var configuration = JoinedRoomProxyMockConfiguration(id: "TestID", name: "StartingName", avatarURL: nil, hasOngoingCall: false)
|
||||
let roomProxyMock = JoinedRoomProxyMock(configuration)
|
||||
|
||||
@@ -248,10 +253,10 @@ class RoomScreenViewModelTests: XCTestCase {
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||
self.viewModel = viewModel
|
||||
|
||||
XCTAssertEqual(viewModel.state.roomTitle, "StartingName")
|
||||
XCTAssertEqual(viewModel.state.roomAvatar, .room(id: "TestID", name: "StartingName", avatarURL: nil))
|
||||
XCTAssertFalse(viewModel.state.canJoinCall)
|
||||
XCTAssertFalse(viewModel.state.hasOngoingCall)
|
||||
#expect(viewModel.state.roomTitle == "StartingName")
|
||||
#expect(viewModel.state.roomAvatar == .room(id: "TestID", name: "StartingName", avatarURL: nil))
|
||||
#expect(!viewModel.state.canJoinCall)
|
||||
#expect(!viewModel.state.hasOngoingCall)
|
||||
|
||||
let deferred = deferFulfillment(viewModel.context.$viewState) { viewState in
|
||||
viewState.roomTitle == "NewName" &&
|
||||
@@ -270,7 +275,8 @@ class RoomScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testCallButtonVisibility() async throws {
|
||||
@Test
|
||||
func callButtonVisibility() async throws {
|
||||
// Given a room screen with no ongoing call.
|
||||
let ongoingCallRoomIDSubject = CurrentValueSubject<String?, Never>(nil)
|
||||
let roomProxyMock = JoinedRoomProxyMock(.init(id: "MyRoomID"))
|
||||
@@ -283,7 +289,7 @@ class RoomScreenViewModelTests: XCTestCase {
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||
self.viewModel = viewModel
|
||||
XCTAssertTrue(viewModel.state.shouldShowCallButton)
|
||||
#expect(viewModel.state.shouldShowCallButton)
|
||||
|
||||
// When a call starts in this room.
|
||||
var deferred = deferFulfillment(viewModel.context.$viewState) { !$0.shouldShowCallButton }
|
||||
@@ -291,7 +297,7 @@ class RoomScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the call button should be hidden.
|
||||
XCTAssertFalse(viewModel.state.shouldShowCallButton)
|
||||
#expect(!viewModel.state.shouldShowCallButton)
|
||||
|
||||
// When a call starts in a different room.
|
||||
deferred = deferFulfillment(viewModel.context.$viewState) { $0.shouldShowCallButton }
|
||||
@@ -299,41 +305,43 @@ class RoomScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the call button should be shown again.
|
||||
XCTAssertTrue(viewModel.state.shouldShowCallButton)
|
||||
#expect(viewModel.state.shouldShowCallButton)
|
||||
|
||||
// When the call from the other room finishes.
|
||||
let deferredFailure = deferFailure(viewModel.context.$viewState, timeout: 1) { !$0.shouldShowCallButton }
|
||||
let deferredFailure = deferFailure(viewModel.context.$viewState, timeout: .seconds(1)) { !$0.shouldShowCallButton }
|
||||
ongoingCallRoomIDSubject.send(nil)
|
||||
try await deferredFailure.fulfill()
|
||||
|
||||
// Then the call button should remain visible shown.
|
||||
XCTAssertTrue(viewModel.state.shouldShowCallButton)
|
||||
#expect(viewModel.state.shouldShowCallButton)
|
||||
}
|
||||
|
||||
func testRoomFullyRead() async {
|
||||
let expectation = XCTestExpectation(description: "Wait for fully read")
|
||||
let roomProxyMock = JoinedRoomProxyMock(.init(id: "MyRoomID"))
|
||||
roomProxyMock.markAsReadReceiptTypeClosure = { readReceiptType in
|
||||
XCTAssertEqual(readReceiptType, .fullyRead)
|
||||
expectation.fulfill()
|
||||
return .success(())
|
||||
@Test
|
||||
func roomFullyRead() async {
|
||||
await waitForConfirmation("Wait for fully read") { confirm in
|
||||
let roomProxyMock = JoinedRoomProxyMock(.init(id: "MyRoomID"))
|
||||
roomProxyMock.markAsReadReceiptTypeClosure = { readReceiptType in
|
||||
#expect(readReceiptType == .fullyRead)
|
||||
confirm()
|
||||
return .success(())
|
||||
}
|
||||
let viewModel = RoomScreenViewModel(userSession: UserSessionMock(.init()),
|
||||
roomProxy: roomProxyMock,
|
||||
initialSelectedPinnedEventID: nil,
|
||||
ongoingCallRoomIDPublisher: .init(.init(nil)),
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
appHooks: AppHooks(),
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||
self.viewModel = viewModel
|
||||
viewModel.stop()
|
||||
}
|
||||
let viewModel = RoomScreenViewModel(userSession: UserSessionMock(.init()),
|
||||
roomProxy: roomProxyMock,
|
||||
initialSelectedPinnedEventID: nil,
|
||||
ongoingCallRoomIDPublisher: .init(.init(nil)),
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
appHooks: AppHooks(),
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||
self.viewModel = viewModel
|
||||
viewModel.stop()
|
||||
await fulfillment(of: [expectation])
|
||||
}
|
||||
|
||||
// MARK: - Knock Requests
|
||||
|
||||
func testKnockRequestBanner() async throws {
|
||||
@Test
|
||||
func knockRequestBanner() async throws {
|
||||
ServiceLocator.shared.settings.knockingEnabled = true
|
||||
let roomProxyMock = JoinedRoomProxyMock(.init(knockRequestsState: .loaded([KnockRequestProxyMock(.init(eventID: "1", userID: "@alice:matrix.org", displayName: "Alice", reason: "Hello World!")),
|
||||
// This one should be filtered
|
||||
@@ -367,7 +375,8 @@ class RoomScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testKnockRequestBannerMarkAsSeen() async throws {
|
||||
@Test
|
||||
func knockRequestBannerMarkAsSeen() async throws {
|
||||
ServiceLocator.shared.settings.knockingEnabled = true
|
||||
let roomProxyMock = JoinedRoomProxyMock(.init(knockRequestsState: .loaded([KnockRequestProxyMock(.init(eventID: "1", userID: "@alice:matrix.org", displayName: "Alice", reason: "Hello World!")),
|
||||
// This one should be filtered
|
||||
@@ -398,7 +407,8 @@ class RoomScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testLoadingKnockRequests() async throws {
|
||||
@Test
|
||||
func loadingKnockRequests() async throws {
|
||||
ServiceLocator.shared.settings.knockingEnabled = true
|
||||
let roomProxyMock = JoinedRoomProxyMock(.init(knockRequestsState: .loading,
|
||||
joinRule: .knock))
|
||||
@@ -417,7 +427,8 @@ class RoomScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testKnockRequestsBannerDoesNotAppearIfUserHasNoPermission() async throws {
|
||||
@Test
|
||||
func knockRequestsBannerDoesNotAppearIfUserHasNoPermission() async throws {
|
||||
ServiceLocator.shared.settings.knockingEnabled = true
|
||||
let roomProxyMock = JoinedRoomProxyMock(.init(knockRequestsState: .loaded([KnockRequestProxyMock(.init(eventID: "1", userID: "@alice:matrix.org", displayName: "Alice", reason: "Hello World!"))]),
|
||||
joinRule: .knock,
|
||||
@@ -441,7 +452,8 @@ class RoomScreenViewModelTests: XCTestCase {
|
||||
|
||||
// MARK: - History Sharing
|
||||
|
||||
func testRoomWithSharedHistoryDoesNotDisplayBadgeIfFeatureFlagNotSet() async throws {
|
||||
@Test
|
||||
func roomWithSharedHistoryDoesNotDisplayBadgeIfFeatureFlagNotSet() async throws {
|
||||
ServiceLocator.shared.settings.enableKeyShareOnInvite = false
|
||||
|
||||
var configuration = JoinedRoomProxyMockConfiguration(historyVisibility: .joined)
|
||||
@@ -461,7 +473,7 @@ class RoomScreenViewModelTests: XCTestCase {
|
||||
self.viewModel = viewModel
|
||||
|
||||
let deferredInvisible = deferFailure(viewModel.context.$viewState,
|
||||
timeout: 1,
|
||||
timeout: .seconds(1),
|
||||
message: "The icon should not be shown when the room history visibility is not .shared or .worldReadable") { viewState in
|
||||
viewState.roomHistorySharingState != nil
|
||||
}
|
||||
@@ -470,14 +482,15 @@ class RoomScreenViewModelTests: XCTestCase {
|
||||
configuration.historyVisibility = .shared
|
||||
infoSubject.send(RoomInfoProxyMock(configuration))
|
||||
let deferredShared = deferFailure(viewModel.context.$viewState,
|
||||
timeout: 1,
|
||||
timeout: .seconds(1),
|
||||
message: "The icon should not be shown when the room history visibility is .shared, since the flag isn't set") { viewState in
|
||||
viewState.roomHistorySharingState != nil
|
||||
}
|
||||
try await deferredShared.fulfill()
|
||||
}
|
||||
|
||||
func testRoomWithSharedHistoryDisplaysBadgeWhenFeatureFlagSet() async throws {
|
||||
@Test
|
||||
func roomWithSharedHistoryDisplaysBadgeWhenFeatureFlagSet() async throws {
|
||||
ServiceLocator.shared.settings.enableKeyShareOnInvite = true
|
||||
|
||||
var configuration = JoinedRoomProxyMockConfiguration(isEncrypted: false, historyVisibility: .joined)
|
||||
@@ -497,7 +510,7 @@ class RoomScreenViewModelTests: XCTestCase {
|
||||
self.viewModel = viewModel
|
||||
|
||||
let deferredInvisible = deferFailure(viewModel.context.$viewState,
|
||||
timeout: 1,
|
||||
timeout: .seconds(1),
|
||||
message: "The icon should be hidden when the room history visibility is not .shared or .worldReadable") { viewState in
|
||||
viewState.roomHistorySharingState != nil
|
||||
}
|
||||
@@ -506,7 +519,7 @@ class RoomScreenViewModelTests: XCTestCase {
|
||||
configuration.historyVisibility = .shared
|
||||
infoSubject.send(RoomInfoProxyMock(configuration))
|
||||
let deferredInvisibleUnencrypted = deferFailure(viewModel.context.$viewState,
|
||||
timeout: 1,
|
||||
timeout: .seconds(1),
|
||||
message: "The icon should not be shown when the room is unencrypted") { viewState in
|
||||
viewState.roomHistorySharingState != nil
|
||||
}
|
||||
|
||||
@@ -9,10 +9,11 @@
|
||||
import Combine
|
||||
@testable import ElementX
|
||||
import MatrixRustSDK
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@Suite
|
||||
@MainActor
|
||||
class SecurityAndPrivacyScreenViewModelTests: XCTestCase {
|
||||
final class SecurityAndPrivacyScreenViewModelTests {
|
||||
var viewModel: SecurityAndPrivacyScreenViewModelProtocol!
|
||||
var spaceServiceProxy: SpaceServiceProxyMock!
|
||||
var roomProxy: JoinedRoomProxyMock!
|
||||
@@ -21,13 +22,18 @@ class SecurityAndPrivacyScreenViewModelTests: XCTestCase {
|
||||
viewModel.context
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
init() {
|
||||
AppSettings.resetAllSettings()
|
||||
}
|
||||
|
||||
deinit {
|
||||
viewModel = nil
|
||||
roomProxy = nil
|
||||
AppSettings.resetAllSettings()
|
||||
}
|
||||
|
||||
func testSetSingleJoinedSpaceMembersAccess() async throws {
|
||||
@Test
|
||||
func setSingleJoinedSpaceMembersAccess() async throws {
|
||||
let singleRoom = [SpaceServiceRoom].mockSingleRoom
|
||||
let space = singleRoom[0]
|
||||
setupViewModel(joinedParentSpaces: singleRoom, joinRule: .public)
|
||||
@@ -35,30 +41,31 @@ class SecurityAndPrivacyScreenViewModelTests: XCTestCase {
|
||||
let deferred = deferFulfillment(context.$viewState) { $0.selectableJoinedSpaces.count == 1 }
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertEqual(context.viewState.currentSettings.accessType, .anyone)
|
||||
XCTAssertTrue(context.viewState.isSaveDisabled)
|
||||
XCTAssertTrue(context.viewState.isSpaceMembersOptionSelectable)
|
||||
#expect(context.viewState.currentSettings.accessType == .anyone)
|
||||
#expect(context.viewState.isSaveDisabled)
|
||||
#expect(context.viewState.isSpaceMembersOptionSelectable)
|
||||
guard case .singleJoined = context.viewState.spaceSelection else {
|
||||
XCTFail("Expected spaceSelection to be .singleJoined")
|
||||
Issue.record("Expected spaceSelection to be .singleJoined")
|
||||
return
|
||||
}
|
||||
|
||||
context.send(viewAction: .selectedSpaceMembersAccess)
|
||||
XCTAssertEqual(context.desiredSettings.accessType, .spaceMembers(spaceIDs: [space.id]))
|
||||
XCTAssertFalse(context.viewState.shouldShowAccessSectionFooter)
|
||||
XCTAssertFalse(context.viewState.isSaveDisabled)
|
||||
#expect(context.desiredSettings.accessType == .spaceMembers(spaceIDs: [space.id]))
|
||||
#expect(!context.viewState.shouldShowAccessSectionFooter)
|
||||
#expect(!context.viewState.isSaveDisabled)
|
||||
|
||||
let expectation = expectation(description: "Join rule has updated")
|
||||
roomProxy.updateJoinRuleClosure = { value in
|
||||
XCTAssertEqual(value, .restricted(rules: [.roomMembership(roomID: space.id)]))
|
||||
expectation.fulfill()
|
||||
return .success(())
|
||||
await waitForConfirmation("Join rule has updated") { confirm in
|
||||
roomProxy.updateJoinRuleClosure = { value in
|
||||
#expect(value == .restricted(rules: [.roomMembership(roomID: space.id)]))
|
||||
confirm()
|
||||
return .success(())
|
||||
}
|
||||
context.send(viewAction: .save)
|
||||
}
|
||||
context.send(viewAction: .save)
|
||||
await fulfillment(of: [expectation])
|
||||
}
|
||||
|
||||
func testSetSingleJoinedAskToJoinWithSpaceMembersAccess() async throws {
|
||||
@Test
|
||||
func setSingleJoinedAskToJoinWithSpaceMembersAccess() async throws {
|
||||
let singleRoom = [SpaceServiceRoom].mockSingleRoom
|
||||
let space = singleRoom[0]
|
||||
setupViewModel(joinedParentSpaces: singleRoom, joinRule: .public)
|
||||
@@ -66,30 +73,31 @@ class SecurityAndPrivacyScreenViewModelTests: XCTestCase {
|
||||
let deferred = deferFulfillment(context.$viewState) { $0.selectableJoinedSpaces.count == 1 }
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertEqual(context.viewState.currentSettings.accessType, .anyone)
|
||||
XCTAssertTrue(context.viewState.isSaveDisabled)
|
||||
XCTAssertTrue(context.viewState.isAskToJoinWithSpaceMembersOptionSelectable)
|
||||
#expect(context.viewState.currentSettings.accessType == .anyone)
|
||||
#expect(context.viewState.isSaveDisabled)
|
||||
#expect(context.viewState.isAskToJoinWithSpaceMembersOptionSelectable)
|
||||
guard case .singleJoined = context.viewState.spaceSelection else {
|
||||
XCTFail("Expected spaceSelection to be .singleJoined")
|
||||
Issue.record("Expected spaceSelection to be .singleJoined")
|
||||
return
|
||||
}
|
||||
|
||||
context.send(viewAction: .selectedAskToJoinWithSpaceMembersAccess)
|
||||
XCTAssertEqual(context.desiredSettings.accessType, .askToJoinWithSpaceMembers(spaceIDs: [space.id]))
|
||||
XCTAssertFalse(context.viewState.shouldShowAccessSectionFooter)
|
||||
XCTAssertFalse(context.viewState.isSaveDisabled)
|
||||
#expect(context.desiredSettings.accessType == .askToJoinWithSpaceMembers(spaceIDs: [space.id]))
|
||||
#expect(!context.viewState.shouldShowAccessSectionFooter)
|
||||
#expect(!context.viewState.isSaveDisabled)
|
||||
|
||||
let expectation = expectation(description: "Join rule has updated")
|
||||
roomProxy.updateJoinRuleClosure = { value in
|
||||
XCTAssertEqual(value, .knockRestricted(rules: [.roomMembership(roomID: space.id)]))
|
||||
expectation.fulfill()
|
||||
return .success(())
|
||||
await waitForConfirmation("Join rule has updated") { confirm in
|
||||
roomProxy.updateJoinRuleClosure = { value in
|
||||
#expect(value == .knockRestricted(rules: [.roomMembership(roomID: space.id)]))
|
||||
confirm()
|
||||
return .success(())
|
||||
}
|
||||
context.send(viewAction: .save)
|
||||
}
|
||||
context.send(viewAction: .save)
|
||||
await fulfillment(of: [expectation])
|
||||
}
|
||||
|
||||
func testSingleUnknownSpaceMembersAccessCanBeReselected() async throws {
|
||||
@Test
|
||||
func singleUnknownSpaceMembersAccessCanBeReselected() async throws {
|
||||
let singleRoom = [SpaceServiceRoom].mockSingleRoom
|
||||
let space = singleRoom[0]
|
||||
setupViewModel(joinedParentSpaces: [], joinRule: .restricted(rules: [.roomMembership(roomID: space.id)]))
|
||||
@@ -97,41 +105,43 @@ class SecurityAndPrivacyScreenViewModelTests: XCTestCase {
|
||||
let deferred = deferFulfillment(context.$viewState) { $0.selectableJoinedSpaces.count == 0 }
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertEqual(context.viewState.currentSettings.accessType, .spaceMembers(spaceIDs: [space.id]))
|
||||
XCTAssertEqual(context.desiredSettings, context.viewState.currentSettings)
|
||||
XCTAssertTrue(context.viewState.isSpaceMembersOptionSelectable)
|
||||
XCTAssertFalse(context.viewState.shouldShowAccessSectionFooter)
|
||||
XCTAssertTrue(context.viewState.isSaveDisabled)
|
||||
#expect(context.viewState.currentSettings.accessType == .spaceMembers(spaceIDs: [space.id]))
|
||||
#expect(context.desiredSettings == context.viewState.currentSettings)
|
||||
#expect(context.viewState.isSpaceMembersOptionSelectable)
|
||||
#expect(!context.viewState.shouldShowAccessSectionFooter)
|
||||
#expect(context.viewState.isSaveDisabled)
|
||||
guard case .singleUnknown = context.viewState.spaceSelection else {
|
||||
XCTFail("Expected spaceSelection to be .singleUnknown")
|
||||
Issue.record("Expected spaceSelection to be .singleUnknown")
|
||||
return
|
||||
}
|
||||
|
||||
let saveDeferred = deferFulfillment(context.$viewState) { !$0.isSaveDisabled }
|
||||
context.desiredSettings.accessType = .anyone
|
||||
XCTAssertTrue(context.viewState.isSpaceMembersOptionSelectable)
|
||||
XCTAssertFalse(context.viewState.isSaveDisabled)
|
||||
try await saveDeferred.fulfill()
|
||||
#expect(context.viewState.isSpaceMembersOptionSelectable)
|
||||
|
||||
context.send(viewAction: .selectedSpaceMembersAccess)
|
||||
XCTAssertTrue(context.viewState.isSaveDisabled)
|
||||
XCTAssertEqual(context.desiredSettings.accessType, .spaceMembers(spaceIDs: [space.id]))
|
||||
#expect(context.viewState.isSaveDisabled)
|
||||
#expect(context.desiredSettings.accessType == .spaceMembers(spaceIDs: [space.id]))
|
||||
guard case .singleUnknown = context.viewState.spaceSelection else {
|
||||
XCTFail("Expected spaceSelection to be .singleUnknown")
|
||||
Issue.record("Expected spaceSelection to be .singleUnknown")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func testMultipleKnownSpacesMembersSelection() async throws {
|
||||
@Test
|
||||
func multipleKnownSpacesMembersSelection() async throws {
|
||||
let spaces = [SpaceServiceRoom].mockJoinedSpaces2
|
||||
setupViewModel(joinedParentSpaces: spaces, joinRule: .public)
|
||||
|
||||
let deferred = deferFulfillment(context.$viewState) { $0.selectableJoinedSpaces.count == 3 }
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertEqual(context.viewState.currentSettings.accessType, .anyone)
|
||||
XCTAssertTrue(context.viewState.isSaveDisabled)
|
||||
XCTAssertTrue(context.viewState.isSpaceMembersOptionSelectable)
|
||||
#expect(context.viewState.currentSettings.accessType == .anyone)
|
||||
#expect(context.viewState.isSaveDisabled)
|
||||
#expect(context.viewState.isSpaceMembersOptionSelectable)
|
||||
guard case .multiple = context.viewState.spaceSelection else {
|
||||
XCTFail("Expected spaceSelection to be .multiple")
|
||||
Issue.record("Expected spaceSelection to be .multiple")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -150,32 +160,33 @@ class SecurityAndPrivacyScreenViewModelTests: XCTestCase {
|
||||
context.send(viewAction: .selectedSpaceMembersAccess)
|
||||
try await deferredAction.fulfill()
|
||||
selectedIDs.send([spaces[0].id])
|
||||
XCTAssertEqual(context.desiredSettings.accessType, .spaceMembers(spaceIDs: [spaces[0].id]))
|
||||
XCTAssertTrue(context.viewState.shouldShowAccessSectionFooter)
|
||||
XCTAssertFalse(context.viewState.isSaveDisabled)
|
||||
#expect(context.desiredSettings.accessType == .spaceMembers(spaceIDs: [spaces[0].id]))
|
||||
#expect(context.viewState.shouldShowAccessSectionFooter)
|
||||
#expect(!context.viewState.isSaveDisabled)
|
||||
|
||||
let expectation = expectation(description: "Join rule has updated")
|
||||
roomProxy.updateJoinRuleClosure = { value in
|
||||
XCTAssertEqual(value, .restricted(rules: [.roomMembership(roomID: spaces[0].id)]))
|
||||
expectation.fulfill()
|
||||
return .success(())
|
||||
await waitForConfirmation("Join rule has updated") { confirm in
|
||||
roomProxy.updateJoinRuleClosure = { value in
|
||||
#expect(value == .restricted(rules: [.roomMembership(roomID: spaces[0].id)]))
|
||||
confirm()
|
||||
return .success(())
|
||||
}
|
||||
context.send(viewAction: .save)
|
||||
}
|
||||
context.send(viewAction: .save)
|
||||
await fulfillment(of: [expectation])
|
||||
}
|
||||
|
||||
func testMultipleKnownAskToJoinSpacesMembersSelection() async throws {
|
||||
@Test
|
||||
func multipleKnownAskToJoinSpacesMembersSelection() async throws {
|
||||
let spaces = [SpaceServiceRoom].mockJoinedSpaces2
|
||||
setupViewModel(joinedParentSpaces: spaces, joinRule: .public)
|
||||
|
||||
let deferred = deferFulfillment(context.$viewState) { $0.selectableJoinedSpaces.count == 3 }
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertEqual(context.viewState.currentSettings.accessType, .anyone)
|
||||
XCTAssertTrue(context.viewState.isSaveDisabled)
|
||||
XCTAssertTrue(context.viewState.isAskToJoinWithSpaceMembersOptionSelectable)
|
||||
#expect(context.viewState.currentSettings.accessType == .anyone)
|
||||
#expect(context.viewState.isSaveDisabled)
|
||||
#expect(context.viewState.isAskToJoinWithSpaceMembersOptionSelectable)
|
||||
guard case .multiple = context.viewState.spaceSelection else {
|
||||
XCTFail("Expected spaceSelection to be .multiple")
|
||||
Issue.record("Expected spaceSelection to be .multiple")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -194,21 +205,22 @@ class SecurityAndPrivacyScreenViewModelTests: XCTestCase {
|
||||
context.send(viewAction: .selectedAskToJoinWithSpaceMembersAccess)
|
||||
try await deferredAction.fulfill()
|
||||
selectedIDs.send([spaces[0].id])
|
||||
XCTAssertEqual(context.desiredSettings.accessType, .askToJoinWithSpaceMembers(spaceIDs: [spaces[0].id]))
|
||||
XCTAssertTrue(context.viewState.shouldShowAccessSectionFooter)
|
||||
XCTAssertFalse(context.viewState.isSaveDisabled)
|
||||
#expect(context.desiredSettings.accessType == .askToJoinWithSpaceMembers(spaceIDs: [spaces[0].id]))
|
||||
#expect(context.viewState.shouldShowAccessSectionFooter)
|
||||
#expect(!context.viewState.isSaveDisabled)
|
||||
|
||||
let expectation = expectation(description: "Join rule has updated")
|
||||
roomProxy.updateJoinRuleClosure = { value in
|
||||
XCTAssertEqual(value, .knockRestricted(rules: [.roomMembership(roomID: spaces[0].id)]))
|
||||
expectation.fulfill()
|
||||
return .success(())
|
||||
await waitForConfirmation("Join rule has updated") { confirm in
|
||||
roomProxy.updateJoinRuleClosure = { value in
|
||||
#expect(value == .knockRestricted(rules: [.roomMembership(roomID: spaces[0].id)]))
|
||||
confirm()
|
||||
return .success(())
|
||||
}
|
||||
context.send(viewAction: .save)
|
||||
}
|
||||
context.send(viewAction: .save)
|
||||
await fulfillment(of: [expectation])
|
||||
}
|
||||
|
||||
func testMultipleSpacesMembersSelection() async throws {
|
||||
@Test
|
||||
func multipleSpacesMembersSelection() async throws {
|
||||
let spaces = [SpaceServiceRoom].mockJoinedSpaces2
|
||||
setupViewModel(joinedParentSpaces: spaces,
|
||||
joinRule: .restricted(rules: [.roomMembership(roomID: "unknownSpaceID")]))
|
||||
@@ -216,11 +228,11 @@ class SecurityAndPrivacyScreenViewModelTests: XCTestCase {
|
||||
let deferred = deferFulfillment(context.$viewState) { $0.selectableSpacesCount == 4 }
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertTrue(context.viewState.currentSettings.accessType.isSpaceMembers)
|
||||
XCTAssertTrue(context.viewState.isSaveDisabled)
|
||||
XCTAssertTrue(context.viewState.isSpaceMembersOptionSelectable)
|
||||
#expect(context.viewState.currentSettings.accessType.isSpaceMembers)
|
||||
#expect(context.viewState.isSaveDisabled)
|
||||
#expect(context.viewState.isSpaceMembersOptionSelectable)
|
||||
guard case .multiple = context.viewState.spaceSelection else {
|
||||
XCTFail("Expected spaceSelection to be .multiple")
|
||||
Issue.record("Expected spaceSelection to be .multiple")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -240,21 +252,22 @@ class SecurityAndPrivacyScreenViewModelTests: XCTestCase {
|
||||
context.send(viewAction: .manageSpaces)
|
||||
try await deferredAction.fulfill()
|
||||
selectedIDs.send([spaces[0].id, "unknownSpaceID"])
|
||||
XCTAssertEqual(context.desiredSettings.accessType, .spaceMembers(spaceIDs: [spaces[0].id, "unknownSpaceID"]))
|
||||
XCTAssertTrue(context.viewState.shouldShowAccessSectionFooter)
|
||||
XCTAssertFalse(context.viewState.isSaveDisabled)
|
||||
#expect(context.desiredSettings.accessType == .spaceMembers(spaceIDs: [spaces[0].id, "unknownSpaceID"]))
|
||||
#expect(context.viewState.shouldShowAccessSectionFooter)
|
||||
#expect(!context.viewState.isSaveDisabled)
|
||||
|
||||
let expectation = expectation(description: "Join rule has updated")
|
||||
roomProxy.updateJoinRuleClosure = { value in
|
||||
XCTAssertEqual(value, .restricted(rules: [.roomMembership(roomID: spaces[0].id), .roomMembership(roomID: "unknownSpaceID")]))
|
||||
expectation.fulfill()
|
||||
return .success(())
|
||||
await waitForConfirmation("Join rule has updated") { confirm in
|
||||
roomProxy.updateJoinRuleClosure = { value in
|
||||
#expect(value == .restricted(rules: [.roomMembership(roomID: spaces[0].id), .roomMembership(roomID: "unknownSpaceID")]))
|
||||
confirm()
|
||||
return .success(())
|
||||
}
|
||||
context.send(viewAction: .save)
|
||||
}
|
||||
context.send(viewAction: .save)
|
||||
await fulfillment(of: [expectation])
|
||||
}
|
||||
|
||||
func testMultipleSpacesMembersSelectionWithAnExistingNonParentButJoinedSpace() async throws {
|
||||
@Test
|
||||
func multipleSpacesMembersSelectionWithAnExistingNonParentButJoinedSpace() async throws {
|
||||
let joinedParentSpaces = [SpaceServiceRoom].mockJoinedSpaces2
|
||||
let singleRoom = [SpaceServiceRoom].mockSingleRoom
|
||||
let space = singleRoom[0]
|
||||
@@ -267,11 +280,11 @@ class SecurityAndPrivacyScreenViewModelTests: XCTestCase {
|
||||
let deferred = deferFulfillment(context.$viewState) { $0.selectableSpacesCount == 5 }
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertTrue(context.viewState.currentSettings.accessType.isSpaceMembers)
|
||||
XCTAssertTrue(context.viewState.isSaveDisabled)
|
||||
XCTAssertTrue(context.viewState.isSpaceMembersOptionSelectable)
|
||||
#expect(context.viewState.currentSettings.accessType.isSpaceMembers)
|
||||
#expect(context.viewState.isSaveDisabled)
|
||||
#expect(context.viewState.isSpaceMembersOptionSelectable)
|
||||
guard case .multiple = context.viewState.spaceSelection else {
|
||||
XCTFail("Expected spaceSelection to be .multiple")
|
||||
Issue.record("Expected spaceSelection to be .multiple")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -291,12 +304,13 @@ class SecurityAndPrivacyScreenViewModelTests: XCTestCase {
|
||||
context.send(viewAction: .manageSpaces)
|
||||
try await deferredAction.fulfill()
|
||||
selectedIDs.send([allSpaces[0].id, "unknownSpaceID"])
|
||||
XCTAssertEqual(context.desiredSettings.accessType, .spaceMembers(spaceIDs: [allSpaces[0].id, "unknownSpaceID"]))
|
||||
XCTAssertTrue(context.viewState.shouldShowAccessSectionFooter)
|
||||
XCTAssertFalse(context.viewState.isSaveDisabled)
|
||||
#expect(context.desiredSettings.accessType == .spaceMembers(spaceIDs: [allSpaces[0].id, "unknownSpaceID"]))
|
||||
#expect(context.viewState.shouldShowAccessSectionFooter)
|
||||
#expect(!context.viewState.isSaveDisabled)
|
||||
}
|
||||
|
||||
func testEmptySpaceMembersSelectionEdgeCase() async throws {
|
||||
@Test
|
||||
func emptySpaceMembersSelectionEdgeCase() async throws {
|
||||
// Edge case where there is no available joined parents and the room has a restricted join rule.
|
||||
// With no space ids in it
|
||||
setupViewModel(joinedParentSpaces: [],
|
||||
@@ -305,17 +319,18 @@ class SecurityAndPrivacyScreenViewModelTests: XCTestCase {
|
||||
let deferred = deferFulfillment(context.$viewState) { $0.selectableSpacesCount == 0 }
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertTrue(context.viewState.currentSettings.accessType.isSpaceMembers)
|
||||
XCTAssertTrue(context.viewState.isSaveDisabled)
|
||||
XCTAssertFalse(context.viewState.isSpaceMembersOptionSelectable)
|
||||
XCTAssertFalse(context.viewState.shouldShowAccessSectionFooter)
|
||||
#expect(context.viewState.currentSettings.accessType.isSpaceMembers)
|
||||
#expect(context.viewState.isSaveDisabled)
|
||||
#expect(!context.viewState.isSpaceMembersOptionSelectable)
|
||||
#expect(!context.viewState.shouldShowAccessSectionFooter)
|
||||
guard case .empty = context.viewState.spaceSelection else {
|
||||
XCTFail("Expected spaceSelection to be .empty")
|
||||
Issue.record("Expected spaceSelection to be .empty")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func testEmptySpaceMembersSelectionWithJoinedParentEdgeCase() async throws {
|
||||
@Test
|
||||
func emptySpaceMembersSelectionWithJoinedParentEdgeCase() async throws {
|
||||
// Edge case where there is one available joined parent but the room has a restricted join rule.
|
||||
// With no space ids in it
|
||||
let singleRoom = [SpaceServiceRoom].mockSingleRoom
|
||||
@@ -325,12 +340,12 @@ class SecurityAndPrivacyScreenViewModelTests: XCTestCase {
|
||||
let deferred = deferFulfillment(context.$viewState) { $0.selectableSpacesCount == 1 }
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertTrue(context.viewState.currentSettings.accessType.isSpaceMembers)
|
||||
XCTAssertTrue(context.viewState.isSaveDisabled)
|
||||
XCTAssertTrue(context.viewState.isSpaceMembersOptionSelectable)
|
||||
XCTAssertTrue(context.viewState.shouldShowAccessSectionFooter)
|
||||
#expect(context.viewState.currentSettings.accessType.isSpaceMembers)
|
||||
#expect(context.viewState.isSaveDisabled)
|
||||
#expect(context.viewState.isSpaceMembersOptionSelectable)
|
||||
#expect(context.viewState.shouldShowAccessSectionFooter)
|
||||
guard case .multiple = context.viewState.spaceSelection else {
|
||||
XCTFail("Expected spaceSelection to be .multiple")
|
||||
Issue.record("Expected spaceSelection to be .multiple")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -348,11 +363,12 @@ class SecurityAndPrivacyScreenViewModelTests: XCTestCase {
|
||||
try await deferredAction.fulfill()
|
||||
}
|
||||
|
||||
func testSave() async throws {
|
||||
@Test
|
||||
func save() async throws {
|
||||
setupViewModel(joinedParentSpaces: [], joinRule: .public)
|
||||
|
||||
// Saving shouldn't dismiss this screen (or trigger any other action).
|
||||
let deferred = deferFailure(viewModel.actionsPublisher, timeout: 1) { _ in true }
|
||||
let deferred = deferFailure(viewModel.actionsPublisher, timeout: .seconds(1)) { _ in true }
|
||||
|
||||
context.desiredSettings.accessType = .inviteOnly
|
||||
context.send(viewAction: .save)
|
||||
@@ -360,15 +376,16 @@ class SecurityAndPrivacyScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testCancelWithChangesAndDiscard() async throws {
|
||||
@Test
|
||||
func cancelWithChangesAndDiscard() async throws {
|
||||
setupViewModel(joinedParentSpaces: [], joinRule: .public)
|
||||
context.desiredSettings.accessType = .inviteOnly
|
||||
XCTAssertFalse(context.viewState.isSaveDisabled)
|
||||
XCTAssertNil(context.alertInfo)
|
||||
#expect(!context.viewState.isSaveDisabled)
|
||||
#expect(context.alertInfo == nil)
|
||||
|
||||
context.send(viewAction: .cancel)
|
||||
|
||||
XCTAssertNotNil(context.alertInfo)
|
||||
#expect(context.alertInfo != nil)
|
||||
|
||||
let deferred = deferFulfillment(viewModel.actionsPublisher) {
|
||||
switch $0 {
|
||||
@@ -382,15 +399,16 @@ class SecurityAndPrivacyScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testCancelWithChangesAndSave() async throws {
|
||||
@Test
|
||||
func cancelWithChangesAndSave() async throws {
|
||||
setupViewModel(joinedParentSpaces: [], joinRule: .public)
|
||||
context.desiredSettings.accessType = .inviteOnly
|
||||
XCTAssertFalse(context.viewState.isSaveDisabled)
|
||||
XCTAssertNil(context.alertInfo)
|
||||
#expect(!context.viewState.isSaveDisabled)
|
||||
#expect(context.alertInfo == nil)
|
||||
|
||||
context.send(viewAction: .cancel)
|
||||
|
||||
XCTAssertNotNil(context.alertInfo)
|
||||
#expect(context.alertInfo != nil)
|
||||
|
||||
let deferred = deferFulfillment(viewModel.actionsPublisher) {
|
||||
switch $0 {
|
||||
@@ -404,19 +422,20 @@ class SecurityAndPrivacyScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testCancelWithChangesAndSaveWithFailure() async throws {
|
||||
@Test
|
||||
func cancelWithChangesAndSaveWithFailure() async throws {
|
||||
setupViewModel(joinedParentSpaces: [], joinRule: .public)
|
||||
roomProxy.updateJoinRuleReturnValue = .failure(.sdkError(RoomProxyMockError.generic))
|
||||
context.desiredSettings.accessType = .inviteOnly
|
||||
XCTAssertFalse(context.viewState.isSaveDisabled)
|
||||
XCTAssertNil(context.alertInfo)
|
||||
#expect(!context.viewState.isSaveDisabled)
|
||||
#expect(context.alertInfo == nil)
|
||||
|
||||
context.send(viewAction: .cancel)
|
||||
|
||||
XCTAssertNotNil(context.alertInfo)
|
||||
#expect(context.alertInfo != nil)
|
||||
|
||||
// The screen should not be dismissed if a failure occurred.
|
||||
let deferred = deferFailure(viewModel.actionsPublisher, timeout: 1) { _ in true }
|
||||
let deferred = deferFailure(viewModel.actionsPublisher, timeout: .seconds(1)) { _ in true }
|
||||
context.alertInfo?.primaryButton.action?() // Save
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
@@ -8,10 +8,12 @@
|
||||
|
||||
@testable import ElementX
|
||||
import MatrixRustSDKMocks
|
||||
import XCTest
|
||||
import SwiftUI
|
||||
import Testing
|
||||
|
||||
@Suite
|
||||
@MainActor
|
||||
class ServerConfirmationScreenViewModelTests: XCTestCase {
|
||||
final class ServerConfirmationScreenViewModelTests {
|
||||
var clientFactory: AuthenticationClientFactoryMock!
|
||||
var client: ClientSDKMock!
|
||||
var service: AuthenticationServiceProtocol!
|
||||
@@ -22,26 +24,27 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
|
||||
viewModel.context
|
||||
}
|
||||
|
||||
override func setUp() {
|
||||
init() {
|
||||
AppSettings.resetAllSettings()
|
||||
appSettings = AppSettings()
|
||||
// These app settings are kept local to the tests on purpose as if they are registered in the
|
||||
// ServiceLocator, the providers override that we apply will break other tests in the suite.
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
deinit {
|
||||
AppSettings.resetAllSettings()
|
||||
}
|
||||
|
||||
// MARK: - Confirmation mode
|
||||
|
||||
func testConfirmLoginWithoutConfiguration() async throws {
|
||||
@Test
|
||||
func confirmLoginWithoutConfiguration() async throws {
|
||||
// Given a view model for login using a service that hasn't been configured.
|
||||
setupViewModel(authenticationFlow: .login)
|
||||
XCTAssertEqual(service.homeserver.value.loginMode, .unknown)
|
||||
XCTAssertEqual(context.viewState.mode, .confirmation(service.homeserver.value.address))
|
||||
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount, 0)
|
||||
#expect(service.homeserver.value.loginMode == .unknown)
|
||||
#expect(context.viewState.mode == .confirmation(service.homeserver.value.address))
|
||||
#expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0)
|
||||
#expect(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount == 0)
|
||||
|
||||
// When continuing from the confirmation screen.
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0.isContinueWithOIDC }
|
||||
@@ -49,23 +52,24 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then a call to configure service should be made.
|
||||
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount, 1)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesReceivedArguments?.prompt, .consent)
|
||||
XCTAssertEqual(service.homeserver.value.loginMode, .oidc(supportsCreatePrompt: true))
|
||||
#expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
|
||||
#expect(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount == 1)
|
||||
#expect(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesReceivedArguments?.prompt == .consent)
|
||||
#expect(service.homeserver.value.loginMode == .oidc(supportsCreatePrompt: true))
|
||||
}
|
||||
|
||||
func testConfirmLoginAfterConfiguration() async throws {
|
||||
@Test
|
||||
func confirmLoginAfterConfiguration() async throws {
|
||||
// Given a view model for login using a service that has already been configured (via the server selection screen).
|
||||
setupViewModel(authenticationFlow: .login)
|
||||
guard case .success = await service.configure(for: viewModel.state.homeserverAddress, flow: .login) else {
|
||||
XCTFail("The configuration should succeed.")
|
||||
Issue.record("The configuration should succeed.")
|
||||
return
|
||||
}
|
||||
XCTAssertEqual(service.homeserver.value.loginMode, .oidc(supportsCreatePrompt: true))
|
||||
XCTAssertEqual(context.viewState.mode, .confirmation(service.homeserver.value.address))
|
||||
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount, 0)
|
||||
#expect(service.homeserver.value.loginMode == .oidc(supportsCreatePrompt: true))
|
||||
#expect(context.viewState.mode == .confirmation(service.homeserver.value.address))
|
||||
#expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
|
||||
#expect(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount == 0)
|
||||
|
||||
// When continuing from the confirmation screen.
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0.isContinueWithOIDC }
|
||||
@@ -73,18 +77,19 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the configured homeserver should be used and no additional client should be built.
|
||||
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount, 1)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesReceivedArguments?.prompt, .consent)
|
||||
#expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
|
||||
#expect(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount == 1)
|
||||
#expect(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesReceivedArguments?.prompt == .consent)
|
||||
}
|
||||
|
||||
func testConfirmRegisterWithoutConfiguration() async throws {
|
||||
@Test
|
||||
func confirmRegisterWithoutConfiguration() async throws {
|
||||
// Given a view model for registration using a service that hasn't been configured.
|
||||
setupViewModel(authenticationFlow: .register)
|
||||
XCTAssertEqual(service.homeserver.value.loginMode, .unknown)
|
||||
XCTAssertEqual(context.viewState.mode, .confirmation(service.homeserver.value.address))
|
||||
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount, 0)
|
||||
#expect(service.homeserver.value.loginMode == .unknown)
|
||||
#expect(context.viewState.mode == .confirmation(service.homeserver.value.address))
|
||||
#expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0)
|
||||
#expect(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount == 0)
|
||||
|
||||
// When continuing from the confirmation screen.
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0.isContinueWithOIDC }
|
||||
@@ -92,24 +97,25 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then a call to configure service should be made.
|
||||
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount, 1)
|
||||
#expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
|
||||
#expect(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount == 1)
|
||||
// The create prompt is broken: https://github.com/element-hq/matrix-authentication-service/issues/3429
|
||||
// XCTAssertEqual(client.urlForOidcOidcConfigurationPromptReceivedArguments?.prompt, .create)
|
||||
XCTAssertEqual(service.homeserver.value.loginMode, .oidc(supportsCreatePrompt: true))
|
||||
// #expect(client.urlForOidcOidcConfigurationPromptReceivedArguments?.prompt == .create)
|
||||
#expect(service.homeserver.value.loginMode == .oidc(supportsCreatePrompt: true))
|
||||
}
|
||||
|
||||
func testConfirmRegisterAfterConfiguration() async throws {
|
||||
@Test
|
||||
func confirmRegisterAfterConfiguration() async throws {
|
||||
// Given a view model for registration using a service that has already been configured (via the server selection screen).
|
||||
setupViewModel(authenticationFlow: .register)
|
||||
guard case .success = await service.configure(for: viewModel.state.homeserverAddress, flow: .register) else {
|
||||
XCTFail("The configuration should succeed.")
|
||||
Issue.record("The configuration should succeed.")
|
||||
return
|
||||
}
|
||||
XCTAssertEqual(service.homeserver.value.loginMode, .oidc(supportsCreatePrompt: true))
|
||||
XCTAssertEqual(context.viewState.mode, .confirmation(service.homeserver.value.address))
|
||||
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount, 0)
|
||||
#expect(service.homeserver.value.loginMode == .oidc(supportsCreatePrompt: true))
|
||||
#expect(context.viewState.mode == .confirmation(service.homeserver.value.address))
|
||||
#expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
|
||||
#expect(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount == 0)
|
||||
|
||||
// When continuing from the confirmation screen.
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0.isContinueWithOIDC }
|
||||
@@ -117,19 +123,20 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the configured homeserver should be used and no additional client should be built.
|
||||
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||
#expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
|
||||
// The create prompt is broken: https://github.com/element-hq/matrix-authentication-service/issues/3429
|
||||
// XCTAssertEqual(client.urlForOidcOidcConfigurationPromptReceivedArguments?.prompt, .create)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount, 1)
|
||||
// #expect(client.urlForOidcOidcConfigurationPromptReceivedArguments?.prompt == .create)
|
||||
#expect(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount == 1)
|
||||
}
|
||||
|
||||
func testConfirmPasswordLoginWithoutConfiguration() async throws {
|
||||
@Test
|
||||
func confirmPasswordLoginWithoutConfiguration() async throws {
|
||||
// Given a view model for login using a service that hasn't been configured (against a server that doesn't support OIDC).
|
||||
setupViewModel(authenticationFlow: .login, supportsOIDC: false)
|
||||
XCTAssertEqual(service.homeserver.value.loginMode, .unknown)
|
||||
XCTAssertEqual(context.viewState.mode, .confirmation(service.homeserver.value.address))
|
||||
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount, 0)
|
||||
#expect(service.homeserver.value.loginMode == .unknown)
|
||||
#expect(context.viewState.mode == .confirmation(service.homeserver.value.address))
|
||||
#expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0)
|
||||
#expect(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount == 0)
|
||||
|
||||
// When continuing from the confirmation screen.
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0.isContinueWithPassword }
|
||||
@@ -137,22 +144,23 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then a call to configure service should be made, but not for the OIDC URL.
|
||||
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount, 0)
|
||||
XCTAssertEqual(service.homeserver.value.loginMode, .password)
|
||||
#expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
|
||||
#expect(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount == 0)
|
||||
#expect(service.homeserver.value.loginMode == .password)
|
||||
}
|
||||
|
||||
func testConfirmPasswordLoginAfterConfiguration() async throws {
|
||||
@Test
|
||||
func confirmPasswordLoginAfterConfiguration() async throws {
|
||||
// Given a view model for login using a service that has already been configured (via the server selection screen).
|
||||
setupViewModel(authenticationFlow: .login, supportsOIDC: false)
|
||||
guard case .success = await service.configure(for: viewModel.state.homeserverAddress, flow: .login) else {
|
||||
XCTFail("The configuration should succeed.")
|
||||
Issue.record("The configuration should succeed.")
|
||||
return
|
||||
}
|
||||
XCTAssertEqual(service.homeserver.value.loginMode, .password)
|
||||
XCTAssertEqual(context.viewState.mode, .confirmation(service.homeserver.value.address))
|
||||
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount, 0)
|
||||
#expect(service.homeserver.value.loginMode == .password)
|
||||
#expect(context.viewState.mode == .confirmation(service.homeserver.value.address))
|
||||
#expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
|
||||
#expect(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount == 0)
|
||||
|
||||
// When continuing from the confirmation screen.
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0.isContinueWithPassword }
|
||||
@@ -160,17 +168,18 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the configured homeserver should be used and no additional client should be built, nor a call to get the OIDC URL.
|
||||
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount, 0)
|
||||
#expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
|
||||
#expect(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount == 0)
|
||||
}
|
||||
|
||||
func testRegistrationNotSupportedAlert() async throws {
|
||||
@Test
|
||||
func registrationNotSupportedAlert() async throws {
|
||||
// Given a view model for registration using a service that hasn't been configured and the default server doesn't support registration.
|
||||
// Note: We don't currently take the create prompt into account when determining registration support.
|
||||
setupViewModel(authenticationFlow: .register, supportsOIDC: false, supportsOIDCCreatePrompt: false)
|
||||
XCTAssertEqual(service.homeserver.value.loginMode, .unknown)
|
||||
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0)
|
||||
XCTAssertNil(context.alertInfo)
|
||||
#expect(service.homeserver.value.loginMode == .unknown)
|
||||
#expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0)
|
||||
#expect(context.alertInfo == nil)
|
||||
|
||||
// When continuing from the confirmation screen.
|
||||
let deferred = deferFulfillment(context.observe(\.alertInfo)) { $0 != nil }
|
||||
@@ -178,16 +187,17 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the configuration should fail with an alert about not supporting registration.
|
||||
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||
XCTAssertEqual(context.alertInfo?.id, .registration)
|
||||
#expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
|
||||
#expect(context.alertInfo?.id == .registration)
|
||||
}
|
||||
|
||||
func testLoginNotSupportedAlert() async throws {
|
||||
@Test
|
||||
func loginNotSupportedAlert() async throws {
|
||||
// Given a view model for login using a service that hasn't been configured and the default server doesn't support login.
|
||||
setupViewModel(authenticationFlow: .login, supportsOIDC: false, supportsOIDCCreatePrompt: false, supportsPasswordLogin: false)
|
||||
XCTAssertEqual(service.homeserver.value.loginMode, .unknown)
|
||||
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0)
|
||||
XCTAssertNil(context.alertInfo)
|
||||
#expect(service.homeserver.value.loginMode == .unknown)
|
||||
#expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0)
|
||||
#expect(context.alertInfo == nil)
|
||||
|
||||
// When continuing from the confirmation screen.
|
||||
let deferred = deferFulfillment(context.observe(\.alertInfo)) { $0 != nil }
|
||||
@@ -195,16 +205,17 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the configuration should fail with an alert about not supporting login.
|
||||
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||
XCTAssertEqual(context.alertInfo?.id, .login)
|
||||
#expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
|
||||
#expect(context.alertInfo?.id == .login)
|
||||
}
|
||||
|
||||
func testElementProRequired() async throws {
|
||||
@Test
|
||||
func elementProRequired() async throws {
|
||||
// Given a view model for login using a service that hasn't been configured and the default server requires Element Pro.
|
||||
setupViewModel(authenticationFlow: .login, supportsOIDC: false, supportsOIDCCreatePrompt: false, supportsPasswordLogin: false, requiresElementPro: true)
|
||||
XCTAssertEqual(service.homeserver.value.loginMode, .unknown)
|
||||
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0)
|
||||
XCTAssertNil(context.alertInfo)
|
||||
#expect(service.homeserver.value.loginMode == .unknown)
|
||||
#expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0)
|
||||
#expect(context.alertInfo == nil)
|
||||
|
||||
// When continuing from the confirmation screen.
|
||||
let deferred = deferFulfillment(context.observe(\.alertInfo)) { $0 != nil }
|
||||
@@ -212,19 +223,20 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the configuration should fail with an alert telling the user to download Element Pro.
|
||||
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||
XCTAssertEqual(context.alertInfo?.id, .elementProRequired(serverName: "matrix.org"))
|
||||
#expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
|
||||
#expect(context.alertInfo?.id == .elementProRequired(serverName: "matrix.org"))
|
||||
}
|
||||
|
||||
// MARK: - Picker mode
|
||||
|
||||
func testPickerWithoutConfiguration() async throws {
|
||||
@Test
|
||||
func pickerWithoutConfiguration() async throws {
|
||||
// Given a view model for login using a service that hasn't been configured.
|
||||
setupViewModel(authenticationFlow: .login, restrictedFlow: true)
|
||||
XCTAssertEqual(service.homeserver.value.loginMode, .unknown)
|
||||
XCTAssertEqual(context.viewState.mode, .picker(appSettings.accountProviders))
|
||||
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount, 0)
|
||||
#expect(service.homeserver.value.loginMode == .unknown)
|
||||
#expect(context.viewState.mode == .picker(appSettings.accountProviders))
|
||||
#expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0)
|
||||
#expect(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount == 0)
|
||||
|
||||
// When continuing from the confirmation screen.
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0.isContinueWithOIDC }
|
||||
@@ -232,23 +244,24 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then a call to configure service should be made.
|
||||
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount, 1)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesReceivedArguments?.prompt, .consent)
|
||||
XCTAssertEqual(service.homeserver.value.loginMode, .oidc(supportsCreatePrompt: true))
|
||||
#expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
|
||||
#expect(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount == 1)
|
||||
#expect(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesReceivedArguments?.prompt == .consent)
|
||||
#expect(service.homeserver.value.loginMode == .oidc(supportsCreatePrompt: true))
|
||||
}
|
||||
|
||||
func testPickerAfterConfiguration() async throws {
|
||||
@Test
|
||||
func pickerAfterConfiguration() async throws {
|
||||
// Given a view model for login using a service that has already been configured (via the server selection screen).
|
||||
setupViewModel(authenticationFlow: .login, restrictedFlow: true)
|
||||
guard case .success = await service.configure(for: appSettings.accountProviders[0], flow: .login) else {
|
||||
XCTFail("The configuration should succeed.")
|
||||
Issue.record("The configuration should succeed.")
|
||||
return
|
||||
}
|
||||
XCTAssertEqual(service.homeserver.value.loginMode, .oidc(supportsCreatePrompt: true))
|
||||
XCTAssertEqual(context.viewState.mode, .picker(appSettings.accountProviders))
|
||||
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount, 0)
|
||||
#expect(service.homeserver.value.loginMode == .oidc(supportsCreatePrompt: true))
|
||||
#expect(context.viewState.mode == .picker(appSettings.accountProviders))
|
||||
#expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
|
||||
#expect(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount == 0)
|
||||
|
||||
// When continuing from the confirmation screen.
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0.isContinueWithOIDC }
|
||||
@@ -256,18 +269,19 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the configured homeserver should be used and no additional client should be built.
|
||||
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount, 1)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesReceivedArguments?.prompt, .consent)
|
||||
#expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
|
||||
#expect(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount == 1)
|
||||
#expect(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesReceivedArguments?.prompt == .consent)
|
||||
}
|
||||
|
||||
func testPickerForPasswordLoginWithoutConfiguration() async throws {
|
||||
@Test
|
||||
func pickerForPasswordLoginWithoutConfiguration() async throws {
|
||||
// Given a view model for login using a service that hasn't been configured (against a server that doesn't support OIDC).
|
||||
setupViewModel(authenticationFlow: .login, supportsOIDC: false, restrictedFlow: true)
|
||||
XCTAssertEqual(service.homeserver.value.loginMode, .unknown)
|
||||
XCTAssertEqual(context.viewState.mode, .picker(appSettings.accountProviders))
|
||||
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount, 0)
|
||||
#expect(service.homeserver.value.loginMode == .unknown)
|
||||
#expect(context.viewState.mode == .picker(appSettings.accountProviders))
|
||||
#expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0)
|
||||
#expect(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount == 0)
|
||||
|
||||
// When continuing from the confirmation screen.
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0.isContinueWithPassword }
|
||||
@@ -275,22 +289,23 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then a call to configure service should be made, but not for the OIDC URL.
|
||||
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount, 0)
|
||||
XCTAssertEqual(service.homeserver.value.loginMode, .password)
|
||||
#expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
|
||||
#expect(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount == 0)
|
||||
#expect(service.homeserver.value.loginMode == .password)
|
||||
}
|
||||
|
||||
func testPickerForPasswordLoginAfterConfiguration() async throws {
|
||||
@Test
|
||||
func pickerForPasswordLoginAfterConfiguration() async throws {
|
||||
// Given a view model for login using a service that has already been configured (via the server selection screen).
|
||||
setupViewModel(authenticationFlow: .login, supportsOIDC: false, restrictedFlow: true)
|
||||
guard case .success = await service.configure(for: appSettings.accountProviders[0], flow: .login) else {
|
||||
XCTFail("The configuration should succeed.")
|
||||
Issue.record("The configuration should succeed.")
|
||||
return
|
||||
}
|
||||
XCTAssertEqual(service.homeserver.value.loginMode, .password)
|
||||
XCTAssertEqual(context.viewState.mode, .picker(appSettings.accountProviders))
|
||||
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount, 0)
|
||||
#expect(service.homeserver.value.loginMode == .password)
|
||||
#expect(context.viewState.mode == .picker(appSettings.accountProviders))
|
||||
#expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
|
||||
#expect(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount == 0)
|
||||
|
||||
// When continuing from the confirmation screen.
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0.isContinueWithPassword }
|
||||
@@ -298,8 +313,8 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the configured homeserver should be used and no additional client should be built, nor a call to get the OIDC URL.
|
||||
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount, 0)
|
||||
#expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
|
||||
#expect(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount == 0)
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
@@ -8,15 +8,17 @@
|
||||
|
||||
import Combine
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
@Suite
|
||||
@MainActor
|
||||
class SessionVerificationViewModelTests: XCTestCase {
|
||||
struct SessionVerificationViewModelTests {
|
||||
var viewModel: SessionVerificationScreenViewModelProtocol!
|
||||
var context: SessionVerificationViewModelType.Context!
|
||||
var sessionVerificationController: SessionVerificationControllerProxyMock!
|
||||
|
||||
override func setUpWithError() throws {
|
||||
init() throws {
|
||||
sessionVerificationController = SessionVerificationControllerProxyMock.configureMock()
|
||||
viewModel = SessionVerificationScreenViewModel(sessionVerificationControllerProxy: sessionVerificationController,
|
||||
flow: .deviceInitiator,
|
||||
@@ -25,24 +27,26 @@ class SessionVerificationViewModelTests: XCTestCase {
|
||||
context = viewModel.context
|
||||
}
|
||||
|
||||
func testRequestVerification() async throws {
|
||||
XCTAssertEqual(context.viewState.verificationState, .initial)
|
||||
@Test
|
||||
func requestVerification() async throws {
|
||||
#expect(context.viewState.verificationState == .initial)
|
||||
|
||||
context.send(viewAction: .requestVerification)
|
||||
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
XCTAssert(sessionVerificationController.requestDeviceVerificationCallsCount == 1)
|
||||
XCTAssertEqual(context.viewState.verificationState, .requestingVerification)
|
||||
#expect(sessionVerificationController.requestDeviceVerificationCallsCount == 1)
|
||||
#expect(context.viewState.verificationState == .requestingVerification)
|
||||
}
|
||||
|
||||
func testVerificationCancellation() async throws {
|
||||
XCTAssertEqual(context.viewState.verificationState, .initial)
|
||||
@Test
|
||||
func verificationCancellation() async throws {
|
||||
#expect(context.viewState.verificationState == .initial)
|
||||
|
||||
context.send(viewAction: .requestVerification)
|
||||
|
||||
viewModel.stop()
|
||||
|
||||
XCTAssertEqual(context.viewState.verificationState, .cancelling)
|
||||
#expect(context.viewState.verificationState == .cancelling)
|
||||
|
||||
let deferred = deferFulfillment(context.$viewState) { state in
|
||||
state.verificationState == .cancelled
|
||||
@@ -50,114 +54,80 @@ class SessionVerificationViewModelTests: XCTestCase {
|
||||
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertEqual(context.viewState.verificationState, .cancelled)
|
||||
#expect(context.viewState.verificationState == .cancelled)
|
||||
|
||||
context.send(viewAction: .restart)
|
||||
|
||||
XCTAssertEqual(context.viewState.verificationState, .initial)
|
||||
#expect(context.viewState.verificationState == .initial)
|
||||
|
||||
XCTAssert(sessionVerificationController.requestDeviceVerificationCallsCount == 1)
|
||||
XCTAssert(sessionVerificationController.cancelVerificationCallsCount == 1)
|
||||
#expect(sessionVerificationController.requestDeviceVerificationCallsCount == 1)
|
||||
#expect(sessionVerificationController.cancelVerificationCallsCount == 1)
|
||||
}
|
||||
|
||||
func testReceiveChallenge() {
|
||||
setupChallengeReceived()
|
||||
@Test
|
||||
mutating func receiveChallenge() async throws {
|
||||
try await setupChallengeReceived()
|
||||
}
|
||||
|
||||
func testAcceptChallenge() {
|
||||
setupChallengeReceived()
|
||||
@Test
|
||||
mutating func acceptChallenge() async throws {
|
||||
try await setupChallengeReceived()
|
||||
|
||||
let waitForAcceptance = XCTestExpectation(description: "Wait for acceptance")
|
||||
|
||||
let cancellable = sessionVerificationController.actions
|
||||
.delay(for: .seconds(0.1), scheduler: DispatchQueue.main) // Allow the view model to process the callback first.
|
||||
.sink { callback in
|
||||
switch callback {
|
||||
case .finished:
|
||||
waitForAcceptance.fulfill()
|
||||
default:
|
||||
XCTFail("Unexpected session verification controller callback")
|
||||
}
|
||||
let deferred = deferFulfillment(sessionVerificationController.actions
|
||||
.delay(for: .seconds(0.1), scheduler: DispatchQueue.main)) { callback in
|
||||
if case .finished = callback { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
defer {
|
||||
cancellable.cancel()
|
||||
}
|
||||
|
||||
context.send(viewAction: .accept)
|
||||
|
||||
wait(for: [waitForAcceptance], timeout: 10.0)
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertEqual(context.viewState.verificationState, .verified)
|
||||
XCTAssert(sessionVerificationController.approveVerificationCallsCount == 1)
|
||||
#expect(context.viewState.verificationState == .verified)
|
||||
#expect(sessionVerificationController.approveVerificationCallsCount == 1)
|
||||
}
|
||||
|
||||
func testDeclineChallenge() {
|
||||
setupChallengeReceived()
|
||||
@Test
|
||||
mutating func declineChallenge() async throws {
|
||||
try await setupChallengeReceived()
|
||||
|
||||
let expectation = XCTestExpectation(description: "Wait for cancellation")
|
||||
|
||||
let cancellable = sessionVerificationController.actions
|
||||
.delay(for: .seconds(0.1), scheduler: DispatchQueue.main) // Allow the view model to process the callback first.
|
||||
.sink { callback in
|
||||
switch callback {
|
||||
case .cancelled:
|
||||
expectation.fulfill()
|
||||
default:
|
||||
XCTFail("Unexpected session verification controller callback")
|
||||
}
|
||||
let deferred = deferFulfillment(sessionVerificationController.actions
|
||||
.delay(for: .seconds(0.1), scheduler: DispatchQueue.main)) { callback in
|
||||
if case .cancelled = callback { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
defer {
|
||||
cancellable.cancel()
|
||||
}
|
||||
|
||||
context.send(viewAction: .decline)
|
||||
|
||||
wait(for: [expectation], timeout: 10.0)
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertEqual(context.viewState.verificationState, .cancelled)
|
||||
XCTAssert(sessionVerificationController.declineVerificationCallsCount == 1)
|
||||
#expect(context.viewState.verificationState == .cancelled)
|
||||
#expect(sessionVerificationController.declineVerificationCallsCount == 1)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func setupChallengeReceived() {
|
||||
let requestAcceptanceExpectation = XCTestExpectation(description: "Wait for request acceptance")
|
||||
let sasVerificationStartExpectation = XCTestExpectation(description: "Wait for SaS verification start")
|
||||
let verificationDataReceivalExpectation = XCTestExpectation(description: "Wait for Emoji data")
|
||||
|
||||
let cancellable = sessionVerificationController.actions
|
||||
.delay(for: .seconds(0.1), scheduler: DispatchQueue.main) // Allow the view model to process the callback first.
|
||||
.sink { callback in
|
||||
switch callback {
|
||||
case .acceptedVerificationRequest:
|
||||
requestAcceptanceExpectation.fulfill()
|
||||
case .startedSasVerification:
|
||||
sasVerificationStartExpectation.fulfill()
|
||||
case .receivedVerificationData:
|
||||
verificationDataReceivalExpectation.fulfill()
|
||||
default:
|
||||
break
|
||||
private mutating func setupChallengeReceived() async throws {
|
||||
let actionsPublisher = sessionVerificationController.actions.delay(for: .seconds(0.1), scheduler: DispatchQueue.main)
|
||||
let cancellable = actionsPublisher
|
||||
.sink { [context] action in
|
||||
if case .acceptedVerificationRequest = action {
|
||||
context?.send(viewAction: .startSasVerification)
|
||||
}
|
||||
}
|
||||
|
||||
defer {
|
||||
cancellable.cancel()
|
||||
}
|
||||
|
||||
let deferred = deferFulfillment(actionsPublisher,
|
||||
keyPath: \.self,
|
||||
transitionValues: [.acceptedVerificationRequest,
|
||||
.startedSasVerification,
|
||||
.receivedVerificationData(SessionVerificationControllerProxyMock.emojis)])
|
||||
context.send(viewAction: .requestVerification)
|
||||
wait(for: [requestAcceptanceExpectation], timeout: 10.0)
|
||||
XCTAssertEqual(context.viewState.verificationState, .verificationRequestAccepted)
|
||||
try await deferred.fulfill()
|
||||
|
||||
context.send(viewAction: .startSasVerification)
|
||||
wait(for: [sasVerificationStartExpectation], timeout: 10.0)
|
||||
XCTAssertEqual(context.viewState.verificationState, .sasVerificationStarted)
|
||||
#expect(context.viewState.verificationState == .showingChallenge(emojis: SessionVerificationControllerProxyMock.emojis))
|
||||
#expect(sessionVerificationController.requestDeviceVerificationCallsCount == 1)
|
||||
#expect(sessionVerificationController.startSasVerificationCallsCount == 1)
|
||||
|
||||
wait(for: [verificationDataReceivalExpectation], timeout: 10.0)
|
||||
XCTAssertEqual(context.viewState.verificationState, .showingChallenge(emojis: SessionVerificationControllerProxyMock.emojis))
|
||||
|
||||
XCTAssert(sessionVerificationController.requestDeviceVerificationCallsCount == 1)
|
||||
XCTAssert(sessionVerificationController.startSasVerificationCallsCount == 1)
|
||||
cancellable.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,11 @@ import Combine
|
||||
@testable import ElementX
|
||||
import MatrixRustSDK
|
||||
import MatrixRustSDKMocks
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@Suite
|
||||
@MainActor
|
||||
class SpaceScreenViewModelTests: XCTestCase {
|
||||
struct SpaceScreenViewModelTests {
|
||||
var spaceRoomListProxy: SpaceRoomListProxyMock!
|
||||
var spaceServiceProxy: SpaceServiceProxyMock!
|
||||
let mockSpaceRooms = [SpaceServiceRoom].mockSpaceList
|
||||
@@ -27,23 +28,25 @@ class SpaceScreenViewModelTests: XCTestCase {
|
||||
viewModel.context
|
||||
}
|
||||
|
||||
func testInitialState() {
|
||||
@Test
|
||||
mutating func initialState() {
|
||||
setupViewModel()
|
||||
|
||||
XCTAssertEqual(context.viewState.paginationState, .idle)
|
||||
XCTAssertTrue(context.viewState.rooms.isEmpty)
|
||||
XCTAssertFalse(spaceRoomListProxy.paginateCalled)
|
||||
#expect(context.viewState.paginationState == .idle)
|
||||
#expect(context.viewState.rooms.isEmpty)
|
||||
#expect(!spaceRoomListProxy.paginateCalled)
|
||||
}
|
||||
|
||||
func testSinglePagination() async throws {
|
||||
@Test
|
||||
mutating func singlePagination() async throws {
|
||||
// Given a space screen view model for a space with a single paginations worth of children.
|
||||
let response = mockSpaceRooms.prefix(3)
|
||||
setupViewModel(paginationResponses: [Array(response)])
|
||||
|
||||
XCTAssertEqual(context.viewState.paginationState, .idle)
|
||||
XCTAssertTrue(context.viewState.rooms.isEmpty)
|
||||
XCTAssertFalse(spaceRoomListProxy.paginateCalled)
|
||||
XCTAssertFalse(response.isEmpty, "There should be some test rooms.")
|
||||
#expect(context.viewState.paginationState == .idle)
|
||||
#expect(context.viewState.rooms.isEmpty)
|
||||
#expect(!spaceRoomListProxy.paginateCalled)
|
||||
#expect(!response.isEmpty, "There should be some test rooms.")
|
||||
|
||||
// When the pagination is triggered.
|
||||
var deferred = deferFulfillment(spaceRoomListProxy.paginationStatePublisher) { $0 == .loading }
|
||||
@@ -51,30 +54,31 @@ class SpaceScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the screen should show a paginating indicator.
|
||||
XCTAssertEqual(context.viewState.paginationState, .paginating)
|
||||
XCTAssertEqual(spaceRoomListProxy.paginateCallsCount, 1)
|
||||
#expect(context.viewState.paginationState == .paginating)
|
||||
#expect(spaceRoomListProxy.paginateCallsCount == 1)
|
||||
|
||||
// When waiting for the pagination to finish.
|
||||
deferred = deferFulfillment(spaceRoomListProxy.paginationStatePublisher) { $0 == .idle(endReached: true) }
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then no more pagination requests should be made the the space rooms should be populated.
|
||||
XCTAssertEqual(context.viewState.paginationState, .endReached)
|
||||
XCTAssertEqual(spaceRoomListProxy.paginateCallsCount, 1)
|
||||
XCTAssertEqual(context.viewState.rooms.map(\.id), response.map(\.id))
|
||||
#expect(context.viewState.paginationState == .endReached)
|
||||
#expect(spaceRoomListProxy.paginateCallsCount == 1)
|
||||
#expect(context.viewState.rooms.map(\.id) == response.map(\.id))
|
||||
}
|
||||
|
||||
func testMultiplePaginations() async throws {
|
||||
@Test
|
||||
mutating func multiplePaginations() async throws {
|
||||
// Given a space screen view model for a space with two distinct paginations worth of children.
|
||||
let response1 = mockSpaceRooms.prefix(3)
|
||||
let response2 = mockSpaceRooms.suffix(mockSpaceRooms.count - 3)
|
||||
setupViewModel(paginationResponses: [Array(response1), Array(response2)])
|
||||
|
||||
XCTAssertEqual(context.viewState.paginationState, .idle)
|
||||
XCTAssertTrue(context.viewState.rooms.isEmpty)
|
||||
XCTAssertFalse(spaceRoomListProxy.paginateCalled)
|
||||
XCTAssertFalse(response1.isEmpty, "There should be some test rooms.")
|
||||
XCTAssertFalse(response2.isEmpty, "There should be more test rooms.")
|
||||
#expect(context.viewState.paginationState == .idle)
|
||||
#expect(context.viewState.rooms.isEmpty)
|
||||
#expect(!spaceRoomListProxy.paginateCalled)
|
||||
#expect(!response1.isEmpty, "There should be some test rooms.")
|
||||
#expect(!response2.isEmpty, "There should be more test rooms.")
|
||||
|
||||
// When the pagination is triggered.
|
||||
let deferredIsPaginating = deferFulfillment(context.observe(\.viewState.paginationState), transitionValues: [.paginating, .idle, .paginating, .endReached])
|
||||
@@ -88,15 +92,16 @@ class SpaceScreenViewModelTests: XCTestCase {
|
||||
try await deferredIsPaginating.fulfill()
|
||||
try await deferredState.fulfill()
|
||||
|
||||
XCTAssertEqual(context.viewState.paginationState, .endReached)
|
||||
XCTAssertEqual(spaceRoomListProxy.paginateCallsCount, 2)
|
||||
XCTAssertEqual(context.viewState.rooms.map(\.id), mockSpaceRooms.map(\.id))
|
||||
#expect(context.viewState.paginationState == .endReached)
|
||||
#expect(spaceRoomListProxy.paginateCallsCount == 2)
|
||||
#expect(context.viewState.rooms.map(\.id) == mockSpaceRooms.map(\.id))
|
||||
}
|
||||
|
||||
func testSelectingSpace() async throws {
|
||||
@Test
|
||||
mutating func selectingSpace() async throws {
|
||||
setupViewModel()
|
||||
|
||||
let selectedSpace = try XCTUnwrap(mockSpaceRooms.first { $0.isSpace && $0.state == .joined }, "There should be a space to select.")
|
||||
let selectedSpace = try #require(mockSpaceRooms.first { $0.isSpace && $0.state == .joined }, "There should be a space to select.")
|
||||
let deferred = deferFulfillment(viewModel.actionsPublisher) { _ in true }
|
||||
viewModel.context.send(viewAction: .spaceAction(.select(selectedSpace)))
|
||||
let action = try await deferred.fulfill()
|
||||
@@ -105,14 +110,15 @@ class SpaceScreenViewModelTests: XCTestCase {
|
||||
case .selectSpace(let spaceRoomListProxy) where spaceRoomListProxy.id == selectedSpace.id:
|
||||
break
|
||||
default:
|
||||
XCTFail("The action should select the space.")
|
||||
Issue.record("The action should select the space.")
|
||||
}
|
||||
}
|
||||
|
||||
func testSelectingUnjoinedSpace() async throws {
|
||||
@Test
|
||||
mutating func selectingUnjoinedSpace() async throws {
|
||||
setupViewModel()
|
||||
|
||||
let selectedSpace = try XCTUnwrap(mockSpaceRooms.first { $0.isSpace && $0.state != .joined }, "There should be a space to select.")
|
||||
let selectedSpace = try #require(mockSpaceRooms.first { $0.isSpace && $0.state != .joined }, "There should be a space to select.")
|
||||
let deferred = deferFulfillment(viewModel.actionsPublisher) { _ in true }
|
||||
viewModel.context.send(viewAction: .spaceAction(.select(selectedSpace)))
|
||||
let action = try await deferred.fulfill()
|
||||
@@ -121,14 +127,15 @@ class SpaceScreenViewModelTests: XCTestCase {
|
||||
case .selectUnjoinedSpace(let spaceServiceRoom) where spaceServiceRoom.id == selectedSpace.id:
|
||||
break
|
||||
default:
|
||||
XCTFail("The action should select the space.")
|
||||
Issue.record("The action should select the space.")
|
||||
}
|
||||
}
|
||||
|
||||
func testSelectingRoom() async throws {
|
||||
@Test
|
||||
mutating func selectingRoom() async throws {
|
||||
setupViewModel()
|
||||
|
||||
let selectedRoom = try XCTUnwrap(mockSpaceRooms.first { !$0.isSpace }, "There should be a room to select.")
|
||||
let selectedRoom = try #require(mockSpaceRooms.first { !$0.isSpace }, "There should be a room to select.")
|
||||
let deferred = deferFulfillment(viewModel.actionsPublisher) { _ in true }
|
||||
viewModel.context.send(viewAction: .spaceAction(.select(selectedRoom)))
|
||||
let action = try await deferred.fulfill()
|
||||
@@ -137,106 +144,109 @@ class SpaceScreenViewModelTests: XCTestCase {
|
||||
case .selectRoom(let roomID) where roomID == selectedRoom.id:
|
||||
break
|
||||
default:
|
||||
XCTFail("The action should select the room.")
|
||||
Issue.record("The action should select the room.")
|
||||
}
|
||||
}
|
||||
|
||||
func testJoiningSpace() async throws {
|
||||
@Test
|
||||
mutating func joiningSpace() async throws {
|
||||
setupViewModel()
|
||||
|
||||
let selectedSpace = try XCTUnwrap(mockSpaceRooms.first { $0.isSpace && $0.state != .joined }, "There should be a space to select.")
|
||||
let selectedSpace = try #require(mockSpaceRooms.first { $0.isSpace && $0.state != .joined }, "There should be a space to select.")
|
||||
|
||||
let expectation = XCTestExpectation(description: "Join room")
|
||||
clientProxy.joinRoomViaClosure = { _, _ in
|
||||
expectation.fulfill()
|
||||
return .success(())
|
||||
}
|
||||
let deferredState = deferFulfillment(viewModel.context.observe(\.viewState.joiningRoomIDs), transitionValues: [[selectedSpace.id], []])
|
||||
|
||||
viewModel.context.send(viewAction: .spaceAction(.join(selectedSpace)))
|
||||
|
||||
await fulfillment(of: [expectation])
|
||||
try await deferredState.fulfill()
|
||||
try await confirmation("Join room") { confirm in
|
||||
clientProxy.joinRoomViaClosure = { _, _ in
|
||||
confirm()
|
||||
return .success(())
|
||||
}
|
||||
viewModel.context.send(viewAction: .spaceAction(.join(selectedSpace)))
|
||||
try await deferredState.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
func testJoiningRoom() async throws {
|
||||
@Test
|
||||
mutating func joiningRoom() async throws {
|
||||
setupViewModel()
|
||||
|
||||
let selectedRoom = try XCTUnwrap(mockSpaceRooms.first { !$0.isSpace }, "There should be a room to select.")
|
||||
let selectedRoom = try #require(mockSpaceRooms.first { !$0.isSpace }, "There should be a room to select.")
|
||||
|
||||
let expectation = XCTestExpectation(description: "Join room")
|
||||
clientProxy.joinRoomViaClosure = { _, _ in
|
||||
expectation.fulfill()
|
||||
return .success(())
|
||||
}
|
||||
let deferredState = deferFulfillment(viewModel.context.observe(\.viewState.joiningRoomIDs), transitionValues: [[selectedRoom.id], []])
|
||||
|
||||
viewModel.context.send(viewAction: .spaceAction(.join(selectedRoom)))
|
||||
|
||||
await fulfillment(of: [expectation])
|
||||
try await deferredState.fulfill()
|
||||
try await confirmation("Join room") { confirm in
|
||||
clientProxy.joinRoomViaClosure = { _, _ in
|
||||
confirm()
|
||||
return .success(())
|
||||
}
|
||||
viewModel.context.send(viewAction: .spaceAction(.join(selectedRoom)))
|
||||
try await deferredState.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
func testManageRoomsWithoutRemoving() throws {
|
||||
@Test
|
||||
mutating func manageRoomsWithoutRemoving() throws {
|
||||
setupViewModel(initialSpaceRooms: mockSpaceRooms)
|
||||
XCTAssertEqual(context.viewState.editMode, .inactive)
|
||||
XCTAssertTrue(context.viewState.editModeSelectedIDs.isEmpty)
|
||||
XCTAssertTrue(context.viewState.visibleRooms.contains { $0.isSpace })
|
||||
#expect(context.viewState.editMode == .inactive)
|
||||
#expect(context.viewState.editModeSelectedIDs.isEmpty)
|
||||
#expect(context.viewState.visibleRooms.contains { $0.isSpace })
|
||||
|
||||
context.send(viewAction: .manageChildren)
|
||||
XCTAssertEqual(context.viewState.editMode, .transient, "Managing rooms should enable edit mode.")
|
||||
XCTAssertTrue(context.viewState.editModeSelectedIDs.isEmpty, "No rooms should be selected to begin with.")
|
||||
XCTAssertFalse(context.viewState.visibleRooms.contains { $0.isSpace }, "Spaces should be filtered out when managing rooms.")
|
||||
#expect(context.viewState.editMode == .transient, "Managing rooms should enable edit mode.")
|
||||
#expect(context.viewState.editModeSelectedIDs.isEmpty, "No rooms should be selected to begin with.")
|
||||
#expect(!context.viewState.visibleRooms.contains { $0.isSpace }, "Spaces should be filtered out when managing rooms.")
|
||||
|
||||
let selectedRoom = try XCTUnwrap(mockSpaceRooms.first { !$0.isSpace }, "There should be a room to select.")
|
||||
XCTAssertFalse(context.viewState.isSpaceIDSelected(selectedRoom.id))
|
||||
let selectedRoom = try #require(mockSpaceRooms.first { !$0.isSpace }, "There should be a room to select.")
|
||||
#expect(!context.viewState.isSpaceIDSelected(selectedRoom.id))
|
||||
context.send(viewAction: .spaceAction(.select(selectedRoom)))
|
||||
XCTAssertEqual(context.viewState.editModeSelectedIDs.count, 1, "The selected room should be included.")
|
||||
XCTAssertTrue(context.viewState.isSpaceIDSelected(selectedRoom.id), "The room should be selected.")
|
||||
#expect(context.viewState.editModeSelectedIDs.count == 1, "The selected room should be included.")
|
||||
#expect(context.viewState.isSpaceIDSelected(selectedRoom.id), "The room should be selected.")
|
||||
|
||||
context.send(viewAction: .finishManagingChildren)
|
||||
XCTAssertEqual(context.viewState.editMode, .inactive, "Cancelling should disable edit mode.")
|
||||
XCTAssertTrue(context.viewState.editModeSelectedIDs.isEmpty, "Cancelling should clear all selected rooms.")
|
||||
XCTAssertTrue(context.viewState.visibleRooms.contains { $0.isSpace }, "Cancelling should restore the hidden spaces.")
|
||||
#expect(context.viewState.editMode == .inactive, "Cancelling should disable edit mode.")
|
||||
#expect(context.viewState.editModeSelectedIDs.isEmpty, "Cancelling should clear all selected rooms.")
|
||||
#expect(context.viewState.visibleRooms.contains { $0.isSpace }, "Cancelling should restore the hidden spaces.")
|
||||
|
||||
XCTAssertFalse(spaceServiceProxy.removeChildFromCalled, "There should be no attempt to remove children when cancelling.")
|
||||
#expect(!spaceServiceProxy.removeChildFromCalled, "There should be no attempt to remove children when cancelling.")
|
||||
}
|
||||
|
||||
func testManageRoomsRemovingChildren() async throws {
|
||||
@Test
|
||||
mutating func manageRoomsRemovingChildren() async throws {
|
||||
setupViewModel(initialSpaceRooms: mockSpaceRooms)
|
||||
XCTAssertEqual(context.viewState.editMode, .inactive)
|
||||
XCTAssertTrue(context.viewState.editModeSelectedIDs.isEmpty)
|
||||
XCTAssertTrue(context.viewState.visibleRooms.contains { $0.isSpace })
|
||||
#expect(context.viewState.editMode == .inactive)
|
||||
#expect(context.viewState.editModeSelectedIDs.isEmpty)
|
||||
#expect(context.viewState.visibleRooms.contains { $0.isSpace })
|
||||
|
||||
context.send(viewAction: .manageChildren)
|
||||
XCTAssertEqual(context.viewState.editMode, .transient, "Managing rooms should enable edit mode.")
|
||||
XCTAssertTrue(context.viewState.editModeSelectedIDs.isEmpty, "No rooms should be selected to begin with.")
|
||||
XCTAssertFalse(context.viewState.visibleRooms.contains { $0.isSpace }, "Spaces should be filtered out when managing rooms.")
|
||||
#expect(context.viewState.editMode == .transient, "Managing rooms should enable edit mode.")
|
||||
#expect(context.viewState.editModeSelectedIDs.isEmpty, "No rooms should be selected to begin with.")
|
||||
#expect(!context.viewState.visibleRooms.contains { $0.isSpace }, "Spaces should be filtered out when managing rooms.")
|
||||
|
||||
let firstRoom = try XCTUnwrap(mockSpaceRooms.first { !$0.isSpace }, "There should be a room to select.")
|
||||
let lastRoom = try XCTUnwrap(mockSpaceRooms.last { !$0.isSpace }, "There should be a room to select.")
|
||||
XCTAssertNotEqual(firstRoom.id, lastRoom.id, "There should be more than one room in the list.")
|
||||
let firstRoom = try #require(mockSpaceRooms.first { !$0.isSpace }, "There should be a room to select.")
|
||||
let lastRoom = try #require(mockSpaceRooms.last { !$0.isSpace }, "There should be a room to select.")
|
||||
#expect(firstRoom.id != lastRoom.id, "There should be more than one room in the list.")
|
||||
context.send(viewAction: .spaceAction(.select(firstRoom)))
|
||||
context.send(viewAction: .spaceAction(.select(lastRoom)))
|
||||
XCTAssertEqual(context.viewState.editModeSelectedIDs.count, 2, "The selected rooms should be included.")
|
||||
#expect(context.viewState.editModeSelectedIDs.count == 2, "The selected rooms should be included.")
|
||||
|
||||
context.send(viewAction: .removeSelectedChildren)
|
||||
XCTAssertTrue(context.isPresentingRemoveChildrenConfirmation, "A confirmation prompt should be shown before removing children.")
|
||||
XCTAssertFalse(spaceServiceProxy.removeChildFromCalled, "There should be no attempt to remove children before confirming.")
|
||||
#expect(context.isPresentingRemoveChildrenConfirmation, "A confirmation prompt should be shown before removing children.")
|
||||
#expect(!spaceServiceProxy.removeChildFromCalled, "There should be no attempt to remove children before confirming.")
|
||||
|
||||
let deferred = deferFulfillment(context.observe(\.viewState.editMode)) { $0 == .inactive }
|
||||
context.send(viewAction: .confirmRemoveSelectedChildren)
|
||||
try await deferred.fulfill()
|
||||
XCTAssertFalse(context.isPresentingRemoveChildrenConfirmation, "Confirming should dismiss the confirmation prompt.")
|
||||
XCTAssertEqual(context.viewState.editMode, .inactive, "Confirming should disable edit mode when done.")
|
||||
XCTAssertTrue(context.viewState.editModeSelectedIDs.isEmpty, "Confirming should clear all selected rooms when done.")
|
||||
XCTAssertTrue(context.viewState.visibleRooms.contains { $0.isSpace }, "Confirming should restore the hidden spaces when done.")
|
||||
#expect(!context.isPresentingRemoveChildrenConfirmation, "Confirming should dismiss the confirmation prompt.")
|
||||
#expect(context.viewState.editMode == .inactive, "Confirming should disable edit mode when done.")
|
||||
#expect(context.viewState.editModeSelectedIDs.isEmpty, "Confirming should clear all selected rooms when done.")
|
||||
#expect(context.viewState.visibleRooms.contains { $0.isSpace }, "Confirming should restore the hidden spaces when done.")
|
||||
|
||||
XCTAssertEqual(spaceServiceProxy.removeChildFromCallsCount, 2, "Each selected room should have been removed.")
|
||||
XCTAssertTrue(spaceRoomListProxy.resetCalled, "The room list should be reset to pick up the changes.")
|
||||
#expect(spaceServiceProxy.removeChildFromCallsCount == 2, "Each selected room should have been removed.")
|
||||
#expect(spaceRoomListProxy.resetCalled, "The room list should be reset to pick up the changes.")
|
||||
}
|
||||
|
||||
func testManageRoomsRemovingChildrenWithFailure() async throws {
|
||||
@Test
|
||||
mutating func manageRoomsRemovingChildrenWithFailure() async throws {
|
||||
setupViewModel(initialSpaceRooms: mockSpaceRooms)
|
||||
|
||||
context.send(viewAction: .manageChildren)
|
||||
@@ -245,10 +255,10 @@ class SpaceScreenViewModelTests: XCTestCase {
|
||||
}
|
||||
context.send(viewAction: .removeSelectedChildren)
|
||||
|
||||
XCTAssertEqual(context.viewState.editMode, .transient, "Managing rooms should enable edit mode.")
|
||||
XCTAssertEqual(context.viewState.visibleRooms.count, 3, "There should be 3 rooms to begin with.")
|
||||
XCTAssertEqual(context.viewState.editModeSelectedIDs.count, 3, "All of the visible rooms should be selected.")
|
||||
XCTAssertTrue(context.isPresentingRemoveChildrenConfirmation, "A confirmation prompt should be shown before removing children.")
|
||||
#expect(context.viewState.editMode == .transient, "Managing rooms should enable edit mode.")
|
||||
#expect(context.viewState.visibleRooms.count == 3, "There should be 3 rooms to begin with.")
|
||||
#expect(context.viewState.editModeSelectedIDs.count == 3, "All of the visible rooms should be selected.")
|
||||
#expect(context.isPresentingRemoveChildrenConfirmation, "A confirmation prompt should be shown before removing children.")
|
||||
|
||||
let successfulIDs = context.viewState.editModeSelectedIDs.prefix(1)
|
||||
spaceServiceProxy.removeChildFromClosure = { childID, _ in
|
||||
@@ -260,54 +270,55 @@ class SpaceScreenViewModelTests: XCTestCase {
|
||||
}
|
||||
|
||||
let deferred = deferFulfillment(context.observe(\.viewState.visibleRooms.count)) { $0 == 2 }
|
||||
let deferredFailure = deferFailure(context.observe(\.viewState.editMode), timeout: 1) { $0 == .inactive }
|
||||
let deferredFailure = deferFailure(context.observe(\.viewState.editMode), timeout: .seconds(1)) { $0 == .inactive }
|
||||
context.send(viewAction: .confirmRemoveSelectedChildren)
|
||||
try await deferred.fulfill()
|
||||
try await deferredFailure.fulfill()
|
||||
|
||||
XCTAssertEqual(context.viewState.editMode, .transient, "The screen should remain in edit mode.")
|
||||
XCTAssertEqual(context.viewState.visibleRooms.count, 2, "The removed rooms should no longer be listed for selection.")
|
||||
XCTAssertEqual(context.viewState.editModeSelectedIDs.count, 2, "The removed rooms should no longer be selected.")
|
||||
#expect(context.viewState.editMode == .transient, "The screen should remain in edit mode.")
|
||||
#expect(context.viewState.visibleRooms.count == 2, "The removed rooms should no longer be listed for selection.")
|
||||
#expect(context.viewState.editModeSelectedIDs.count == 2, "The removed rooms should no longer be selected.")
|
||||
|
||||
XCTAssertEqual(spaceServiceProxy.removeChildFromCallsCount, 2, "Each selected room should have been removed.")
|
||||
XCTAssertFalse(spaceRoomListProxy.resetCalled, "The room list should be reset to pick up the changes.")
|
||||
#expect(spaceServiceProxy.removeChildFromCallsCount == 2, "Each selected room should have been removed.")
|
||||
#expect(!spaceRoomListProxy.resetCalled, "The room list should be reset to pick up the changes.")
|
||||
}
|
||||
|
||||
func testLeavingSpace() async throws {
|
||||
@Test
|
||||
mutating func leavingSpace() async throws {
|
||||
setupViewModel()
|
||||
XCTAssertNil(context.leaveSpaceViewModel)
|
||||
#expect(context.leaveSpaceViewModel == nil)
|
||||
|
||||
let deferredHandle = deferFulfillment(context.observe(\.leaveSpaceViewModel)) { $0 != nil }
|
||||
context.send(viewAction: .leaveSpace)
|
||||
try await deferredHandle.fulfill()
|
||||
XCTAssertNotNil(context.leaveSpaceViewModel, "The leave action should show the leave view.")
|
||||
#expect(context.leaveSpaceViewModel != nil, "The leave action should show the leave view.")
|
||||
|
||||
let leaveSpaceViewModel = try XCTUnwrap(context.leaveSpaceViewModel)
|
||||
let handle = try XCTUnwrap(context.leaveSpaceViewModel?.state.leaveHandle)
|
||||
let leaveSpaceViewModel = try #require(context.leaveSpaceViewModel)
|
||||
let handle = try #require(context.leaveSpaceViewModel?.state.leaveHandle)
|
||||
let selectedCount = handle.selectedCount
|
||||
let firstSelectedRoom = try XCTUnwrap(handle.rooms.first { $0.isSelected })
|
||||
XCTAssertGreaterThan(selectedCount, 0, "The leave view should have selected rooms to begin with")
|
||||
let firstSelectedRoom = try #require(handle.rooms.first { $0.isSelected })
|
||||
#expect(selectedCount > 0, "The leave view should have selected rooms to begin with")
|
||||
|
||||
leaveSpaceViewModel.context.send(viewAction: .deselectAll)
|
||||
XCTAssertEqual(handle.selectedCount, 0, "Deselecting all should result in no selected rooms.")
|
||||
#expect(handle.selectedCount == 0, "Deselecting all should result in no selected rooms.")
|
||||
|
||||
leaveSpaceViewModel.context.send(viewAction: .toggleRoom(roomID: firstSelectedRoom.spaceServiceRoom.id))
|
||||
XCTAssertEqual(handle.selectedCount, 1, "Toggling a room should result in 1 selected room")
|
||||
#expect(handle.selectedCount == 1, "Toggling a room should result in 1 selected room")
|
||||
|
||||
// Confirming the leave should leave the selected room and then the space.
|
||||
let deferredAction = deferFulfillment(viewModel.actionsPublisher) { $0.isLeftSpace }
|
||||
leaveSpaceViewModel.context.send(viewAction: .confirmLeaveSpace)
|
||||
try await deferredAction.fulfill()
|
||||
XCTAssertNil(context.leaveSpaceViewModel)
|
||||
XCTAssertTrue(rustLeaveHandle.leaveRoomIdsCalled)
|
||||
XCTAssertEqual(rustLeaveHandle.leaveRoomIdsReceivedRoomIds,
|
||||
[firstSelectedRoom.spaceServiceRoom.id, spaceRoomListProxy.id],
|
||||
"Confirming the leave should first leave the selected room and then the space.")
|
||||
#expect(context.leaveSpaceViewModel == nil)
|
||||
#expect(rustLeaveHandle.leaveRoomIdsCalled)
|
||||
#expect(rustLeaveHandle.leaveRoomIdsReceivedRoomIds ==
|
||||
[firstSelectedRoom.spaceServiceRoom.id, spaceRoomListProxy.id],
|
||||
"Confirming the leave should first leave the selected room and then the space.")
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func setupViewModel(initialSpaceRooms: [SpaceServiceRoom] = [], paginationResponses: [[SpaceServiceRoom]] = []) {
|
||||
private mutating func setupViewModel(initialSpaceRooms: [SpaceServiceRoom] = [], paginationResponses: [[SpaceServiceRoom]] = []) {
|
||||
spaceRoomListProxy = SpaceRoomListProxyMock(.init(spaceServiceRoom: SpaceServiceRoom.mock(isSpace: true),
|
||||
initialSpaceRooms: initialSpaceRooms,
|
||||
paginationStateSubject: paginationStateSubject,
|
||||
|
||||
@@ -133,7 +133,7 @@ func deferFulfillment<Value>(_ asyncSequence: any AsyncSequence<Value, Never>,
|
||||
|
||||
defer { group.cancelAll() }
|
||||
|
||||
return try #require(try await group.next())
|
||||
return try #require(await group.next())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,11 @@
|
||||
|
||||
@testable import ElementX
|
||||
import QuickLook
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@Suite
|
||||
@MainActor
|
||||
class TimelineMediaPreviewDataSourceTests: XCTestCase {
|
||||
struct TimelineMediaPreviewDataSourceTests {
|
||||
var initialMediaItems: [EventBasedMessageTimelineItemProtocol]!
|
||||
var initialMediaViewStates: [RoomTimelineItemViewState]!
|
||||
let initialItemIndex = 2
|
||||
@@ -19,82 +20,67 @@ class TimelineMediaPreviewDataSourceTests: XCTestCase {
|
||||
var initialPadding = 100
|
||||
let previewController = QLPreviewController()
|
||||
|
||||
override func setUp() {
|
||||
init() {
|
||||
initialMediaItems = newChunk()
|
||||
initialMediaViewStates = initialMediaItems.map { RoomTimelineItemViewState(item: $0, groupStyle: .single) }
|
||||
}
|
||||
|
||||
func testInitialItems() throws -> TimelineMediaPreviewDataSource {
|
||||
// Given a data source built with the initial items.
|
||||
let dataSource = TimelineMediaPreviewDataSource(itemViewStates: initialMediaViewStates,
|
||||
initialItem: initialMediaItems[initialItemIndex],
|
||||
initialPadding: initialPadding,
|
||||
paginationState: .initial)
|
||||
|
||||
// When the preview controller displays the data.
|
||||
let previewItemCount = dataSource.numberOfPreviewItems(in: previewController)
|
||||
let displayedItem = try XCTUnwrap(dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem.Media,
|
||||
"A preview item should be found.")
|
||||
|
||||
// Then the preview controller should be showing the initial item and the data source should reflect this.
|
||||
XCTAssertEqual(dataSource.initialItemIndex, initialItemIndex + initialPadding, "The initial item index should be padded for the preview controller.")
|
||||
XCTAssertEqual(displayedItem.id, initialMediaItems[initialItemIndex].id.eventOrTransactionID, "The displayed item should be the initial item.")
|
||||
XCTAssertEqual(dataSource.currentMediaItemID, initialMediaItems[initialItemIndex].id.eventOrTransactionID, "The current item should also be the initial item.")
|
||||
|
||||
XCTAssertEqual(dataSource.previewItems.count, initialMediaViewStates.count, "The initial count of preview items should be correct.")
|
||||
XCTAssertEqual(previewItemCount, initialMediaViewStates.count + (2 * initialPadding), "The initial item count should be padded for the preview controller.")
|
||||
|
||||
return dataSource
|
||||
@Test
|
||||
func initialItems() throws {
|
||||
try assertInitialDataSource()
|
||||
}
|
||||
|
||||
func testCurrentUpdateItem() throws {
|
||||
@Test
|
||||
func currentUpdateItem() throws {
|
||||
// Given a data source built with the initial items.
|
||||
let dataSource = TimelineMediaPreviewDataSource(itemViewStates: initialMediaViewStates,
|
||||
initialItem: initialMediaItems[initialItemIndex],
|
||||
paginationState: .initial)
|
||||
|
||||
// When a different item is displayed.
|
||||
let previewItem = try XCTUnwrap(dataSource.previewController(previewController, previewItemAt: 1 + initialPadding) as? TimelineMediaPreviewItem.Media,
|
||||
"A preview item should be found.")
|
||||
let previewItem = try #require(dataSource.previewController(previewController, previewItemAt: 1 + initialPadding) as? TimelineMediaPreviewItem.Media,
|
||||
"A preview item should be found.")
|
||||
dataSource.updateCurrentItem(.media(previewItem))
|
||||
|
||||
// Then the data source should reflect the change of item.
|
||||
XCTAssertEqual(dataSource.currentMediaItemID, previewItem.id, "The displayed item should be the initial item.")
|
||||
#expect(dataSource.currentMediaItemID == previewItem.id, "The displayed item should be the initial item.")
|
||||
|
||||
// When a loading item is displayed.
|
||||
guard let loadingItem = dataSource.previewController(previewController, previewItemAt: initialPadding - 1) as? TimelineMediaPreviewItem.Loading else {
|
||||
XCTFail("A loading item should be be returned.")
|
||||
Issue.record("A loading item should be be returned.")
|
||||
return
|
||||
}
|
||||
dataSource.updateCurrentItem(.loading(loadingItem))
|
||||
|
||||
// Then the data source should show a loading item
|
||||
XCTAssertEqual(dataSource.currentItem, .loading(loadingItem), "The displayed item should be the loading item.")
|
||||
#expect(dataSource.currentItem == .loading(loadingItem), "The displayed item should be the loading item.")
|
||||
}
|
||||
|
||||
func testUpdatedItems() async throws {
|
||||
@Test
|
||||
func updatedItems() async throws {
|
||||
// Given a data source built with the initial items.
|
||||
let dataSource = try testInitialItems()
|
||||
let dataSource = try assertInitialDataSource()
|
||||
|
||||
// When one of the items changes but no pagination has occurred.
|
||||
let deferred = deferFailure(dataSource.previewItemsPaginationPublisher, timeout: 1) { _ in true }
|
||||
let deferred = deferFailure(dataSource.previewItemsPaginationPublisher, timeout: .seconds(1)) { _ in true }
|
||||
dataSource.updatePreviewItems(itemViewStates: initialMediaViewStates)
|
||||
|
||||
// Then no pagination should be detected and none of the data should have changed.
|
||||
try await deferred.fulfill()
|
||||
|
||||
let previewItemCount = dataSource.numberOfPreviewItems(in: previewController)
|
||||
let displayedItem = try XCTUnwrap(dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem.Media)
|
||||
XCTAssertEqual(displayedItem.id, initialMediaItems[initialItemIndex].id.eventOrTransactionID, "The displayed item should not change.")
|
||||
XCTAssertEqual(dataSource.currentMediaItemID, initialMediaItems[initialItemIndex].id.eventOrTransactionID, "The current item should not change.")
|
||||
let displayedItem = try #require(dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem.Media)
|
||||
#expect(displayedItem.id == initialMediaItems[initialItemIndex].id.eventOrTransactionID, "The displayed item should not change.")
|
||||
#expect(dataSource.currentMediaItemID == initialMediaItems[initialItemIndex].id.eventOrTransactionID, "The current item should not change.")
|
||||
|
||||
XCTAssertEqual(dataSource.previewItems.count, initialMediaViewStates.count, "The number of items should not change.")
|
||||
XCTAssertEqual(previewItemCount, initialMediaViewStates.count + (2 * initialPadding), "The padded number of items should not change.")
|
||||
#expect(dataSource.previewItems.count == initialMediaViewStates.count, "The number of items should not change.")
|
||||
#expect(previewItemCount == initialMediaViewStates.count + (2 * initialPadding), "The padded number of items should not change.")
|
||||
}
|
||||
|
||||
func testPagination() async throws {
|
||||
@Test
|
||||
func pagination() async throws {
|
||||
// Given a data source built with the initial items.
|
||||
let dataSource = try testInitialItems()
|
||||
let dataSource = try assertInitialDataSource()
|
||||
|
||||
// When more items are loaded in a back pagination.
|
||||
var deferred = deferFulfillment(dataSource.previewItemsPaginationPublisher) { _ in true }
|
||||
@@ -104,13 +90,13 @@ class TimelineMediaPreviewDataSourceTests: XCTestCase {
|
||||
|
||||
// Then the new items should be added but the displayed item should not change or move in the array.
|
||||
try await deferred.fulfill()
|
||||
XCTAssertEqual(dataSource.previewItems.count, newViewStates.count, "The new items should be added.")
|
||||
#expect(dataSource.previewItems.count == newViewStates.count, "The new items should be added.")
|
||||
|
||||
var previewItemCount = dataSource.numberOfPreviewItems(in: previewController)
|
||||
var displayedItem = try XCTUnwrap(dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem.Media)
|
||||
XCTAssertEqual(displayedItem.id, initialMediaItems[initialItemIndex].id.eventOrTransactionID, "The displayed item should not change.")
|
||||
XCTAssertEqual(dataSource.currentMediaItemID, initialMediaItems[initialItemIndex].id.eventOrTransactionID, "The current item should not change.")
|
||||
XCTAssertEqual(previewItemCount, initialMediaViewStates.count + (2 * initialPadding), "The number of items should not change")
|
||||
var displayedItem = try #require(dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem.Media)
|
||||
#expect(displayedItem.id == initialMediaItems[initialItemIndex].id.eventOrTransactionID, "The displayed item should not change.")
|
||||
#expect(dataSource.currentMediaItemID == initialMediaItems[initialItemIndex].id.eventOrTransactionID, "The current item should not change.")
|
||||
#expect(previewItemCount == initialMediaViewStates.count + (2 * initialPadding), "The number of items should not change")
|
||||
|
||||
// When more items are loaded in a forward pagination or sync.
|
||||
deferred = deferFulfillment(dataSource.previewItemsPaginationPublisher) { _ in true }
|
||||
@@ -120,36 +106,37 @@ class TimelineMediaPreviewDataSourceTests: XCTestCase {
|
||||
|
||||
// Then the new items should be added but the displayed item should not change or move in the array.
|
||||
try await deferred.fulfill()
|
||||
XCTAssertEqual(dataSource.previewItems.count, newViewStates.count, "The new items should be added.")
|
||||
#expect(dataSource.previewItems.count == newViewStates.count, "The new items should be added.")
|
||||
|
||||
previewItemCount = dataSource.numberOfPreviewItems(in: previewController)
|
||||
displayedItem = try XCTUnwrap(dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem.Media)
|
||||
XCTAssertEqual(displayedItem.id, initialMediaItems[initialItemIndex].id.eventOrTransactionID, "The displayed item should not change.")
|
||||
XCTAssertEqual(dataSource.currentMediaItemID, initialMediaItems[initialItemIndex].id.eventOrTransactionID, "The current item should not change.")
|
||||
XCTAssertEqual(previewItemCount, initialMediaViewStates.count + (2 * initialPadding), "The number of items should not change")
|
||||
displayedItem = try #require(dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem.Media)
|
||||
#expect(displayedItem.id == initialMediaItems[initialItemIndex].id.eventOrTransactionID, "The displayed item should not change.")
|
||||
#expect(dataSource.currentMediaItemID == initialMediaItems[initialItemIndex].id.eventOrTransactionID, "The current item should not change.")
|
||||
#expect(previewItemCount == initialMediaViewStates.count + (2 * initialPadding), "The number of items should not change")
|
||||
}
|
||||
|
||||
func testPaginationLimits() async throws {
|
||||
@Test
|
||||
mutating func paginationLimits() async throws {
|
||||
// Given a data source with a small amount of padding remaining.
|
||||
initialPadding = 2
|
||||
let dataSource = try testInitialItems()
|
||||
let dataSource = try assertInitialDataSource()
|
||||
|
||||
// When paginating backwards by more than the available padding.
|
||||
var deferred = deferFulfillment(dataSource.previewItemsPaginationPublisher) { _ in true }
|
||||
let backPaginationChunk = newChunk().map { RoomTimelineItemViewState(item: $0, groupStyle: .single) }
|
||||
var newViewStates = backPaginationChunk + initialMediaViewStates
|
||||
XCTAssertTrue(newViewStates.count > initialPadding)
|
||||
#expect(newViewStates.count > initialPadding)
|
||||
dataSource.updatePreviewItems(itemViewStates: newViewStates)
|
||||
|
||||
// Then all the items should be added but the preview-able count shouldn't grow and displayed item should not change or move.
|
||||
try await deferred.fulfill()
|
||||
XCTAssertEqual(dataSource.previewItems.count, newViewStates.count, "The new items should be added.")
|
||||
#expect(dataSource.previewItems.count == newViewStates.count, "The new items should be added.")
|
||||
|
||||
var previewItemCount = dataSource.numberOfPreviewItems(in: previewController)
|
||||
var displayedItem = try XCTUnwrap(dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem.Media)
|
||||
XCTAssertEqual(displayedItem.id, initialMediaItems[initialItemIndex].id.eventOrTransactionID, "The displayed item should not change.")
|
||||
XCTAssertEqual(dataSource.currentMediaItemID, initialMediaItems[initialItemIndex].id.eventOrTransactionID, "The current item should not change.")
|
||||
XCTAssertEqual(previewItemCount, initialMediaViewStates.count + (2 * initialPadding), "The number of items should not change")
|
||||
var displayedItem = try #require(dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem.Media)
|
||||
#expect(displayedItem.id == initialMediaItems[initialItemIndex].id.eventOrTransactionID, "The displayed item should not change.")
|
||||
#expect(dataSource.currentMediaItemID == initialMediaItems[initialItemIndex].id.eventOrTransactionID, "The current item should not change.")
|
||||
#expect(previewItemCount == initialMediaViewStates.count + (2 * initialPadding), "The number of items should not change")
|
||||
|
||||
// When paginating forwards by more than the available padding.
|
||||
deferred = deferFulfillment(dataSource.previewItemsPaginationPublisher) { _ in true }
|
||||
@@ -159,16 +146,17 @@ class TimelineMediaPreviewDataSourceTests: XCTestCase {
|
||||
|
||||
// Then all the items should be added but the preview-able count shouldn't grow and displayed item should not change or move.
|
||||
try await deferred.fulfill()
|
||||
XCTAssertEqual(dataSource.previewItems.count, newViewStates.count, "The new items should be added.")
|
||||
#expect(dataSource.previewItems.count == newViewStates.count, "The new items should be added.")
|
||||
|
||||
previewItemCount = dataSource.numberOfPreviewItems(in: previewController)
|
||||
displayedItem = try XCTUnwrap(dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem.Media)
|
||||
XCTAssertEqual(displayedItem.id, initialMediaItems[initialItemIndex].id.eventOrTransactionID, "The displayed item should not change.")
|
||||
XCTAssertEqual(dataSource.currentMediaItemID, initialMediaItems[initialItemIndex].id.eventOrTransactionID, "The current item should not change.")
|
||||
XCTAssertEqual(previewItemCount, initialMediaViewStates.count + (2 * initialPadding), "The number of items should not change")
|
||||
displayedItem = try #require(dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem.Media)
|
||||
#expect(displayedItem.id == initialMediaItems[initialItemIndex].id.eventOrTransactionID, "The displayed item should not change.")
|
||||
#expect(dataSource.currentMediaItemID == initialMediaItems[initialItemIndex].id.eventOrTransactionID, "The current item should not change.")
|
||||
#expect(previewItemCount == initialMediaViewStates.count + (2 * initialPadding), "The number of items should not change")
|
||||
}
|
||||
|
||||
func testEmptyTimeline() async throws {
|
||||
@Test
|
||||
func emptyTimeline() async throws {
|
||||
// Given a data source built with no timeline items loaded.
|
||||
let initialItem = initialMediaItems[initialItemIndex]
|
||||
let dataSource = TimelineMediaPreviewDataSource(itemViewStates: [],
|
||||
@@ -178,16 +166,16 @@ class TimelineMediaPreviewDataSourceTests: XCTestCase {
|
||||
|
||||
// When the preview controller displays the data.
|
||||
var previewItemCount = dataSource.numberOfPreviewItems(in: previewController)
|
||||
var displayedItem = try XCTUnwrap(dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem.Media,
|
||||
"A preview item should be found.")
|
||||
var displayedItem = try #require(dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem.Media,
|
||||
"A preview item should be found.")
|
||||
|
||||
// Then the preview controller should always show the initial item.
|
||||
XCTAssertEqual(dataSource.previewItems.count, 1, "The initial item should be in the preview items array.")
|
||||
XCTAssertEqual(previewItemCount, 1 + (2 * initialPadding), "The initial item count should be padded for the preview controller.")
|
||||
XCTAssertEqual(dataSource.initialItemIndex, initialPadding, "The initial item index should be padded for the preview controller.")
|
||||
#expect(dataSource.previewItems.count == 1, "The initial item should be in the preview items array.")
|
||||
#expect(previewItemCount == 1 + (2 * initialPadding), "The initial item count should be padded for the preview controller.")
|
||||
#expect(dataSource.initialItemIndex == initialPadding, "The initial item index should be padded for the preview controller.")
|
||||
|
||||
XCTAssertEqual(displayedItem.id, initialItem.id.eventOrTransactionID, "The displayed item should be the initial item.")
|
||||
XCTAssertEqual(dataSource.currentMediaItemID, initialItem.id.eventOrTransactionID, "The current item should also be the initial item.")
|
||||
#expect(displayedItem.id == initialItem.id.eventOrTransactionID, "The displayed item should be the initial item.")
|
||||
#expect(dataSource.currentMediaItemID == initialItem.id.eventOrTransactionID, "The current item should also be the initial item.")
|
||||
|
||||
// When the timeline loads the initial items.
|
||||
let deferred = deferFulfillment(dataSource.previewItemsPaginationPublisher) { _ in true }
|
||||
@@ -197,18 +185,19 @@ class TimelineMediaPreviewDataSourceTests: XCTestCase {
|
||||
|
||||
// Then the preview controller should still show the initial item with the other items loaded around it.
|
||||
previewItemCount = dataSource.numberOfPreviewItems(in: previewController)
|
||||
displayedItem = try XCTUnwrap(dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem.Media,
|
||||
"A preview item should be found.")
|
||||
displayedItem = try #require(dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem.Media,
|
||||
"A preview item should be found.")
|
||||
|
||||
XCTAssertEqual(dataSource.previewItems.count, initialMediaViewStates.count, "The preview items should now be loaded.")
|
||||
XCTAssertEqual(previewItemCount, 1 + (2 * initialPadding), "The item count should not change as the padding will be reduced.")
|
||||
XCTAssertEqual(dataSource.initialItemIndex, initialPadding, "The item index should not change.")
|
||||
#expect(dataSource.previewItems.count == initialMediaViewStates.count, "The preview items should now be loaded.")
|
||||
#expect(previewItemCount == 1 + (2 * initialPadding), "The item count should not change as the padding will be reduced.")
|
||||
#expect(dataSource.initialItemIndex == initialPadding, "The item index should not change.")
|
||||
|
||||
XCTAssertEqual(displayedItem.id, initialMediaItems[initialItemIndex].id.eventOrTransactionID, "The displayed item should not change.")
|
||||
XCTAssertEqual(dataSource.currentMediaItemID, initialMediaItems[initialItemIndex].id.eventOrTransactionID, "The current item should not change.")
|
||||
#expect(displayedItem.id == initialMediaItems[initialItemIndex].id.eventOrTransactionID, "The displayed item should not change.")
|
||||
#expect(dataSource.currentMediaItemID == initialMediaItems[initialItemIndex].id.eventOrTransactionID, "The current item should not change.")
|
||||
}
|
||||
|
||||
func testTimelineUpdateWithoutInitialItem() async throws {
|
||||
@Test
|
||||
func timelineUpdateWithoutInitialItem() async throws {
|
||||
// Given a data source built with no timeline items loaded.
|
||||
let initialItem = initialMediaItems[initialItemIndex]
|
||||
let dataSource = TimelineMediaPreviewDataSource(itemViewStates: [],
|
||||
@@ -218,34 +207,34 @@ class TimelineMediaPreviewDataSourceTests: XCTestCase {
|
||||
|
||||
// When the preview controller displays the data.
|
||||
var previewItemCount = dataSource.numberOfPreviewItems(in: previewController)
|
||||
var displayedItem = try XCTUnwrap(dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem.Media,
|
||||
"A preview item should be found.")
|
||||
var displayedItem = try #require(dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem.Media,
|
||||
"A preview item should be found.")
|
||||
|
||||
// Then the preview controller should always show the initial item.
|
||||
XCTAssertEqual(dataSource.previewItems.count, 1, "The initial item should be in the preview items array.")
|
||||
XCTAssertEqual(previewItemCount, 1 + (2 * initialPadding), "The initial item count should be padded for the preview controller.")
|
||||
XCTAssertEqual(dataSource.initialItemIndex, initialPadding, "The initial item index should be padded for the preview controller.")
|
||||
#expect(dataSource.previewItems.count == 1, "The initial item should be in the preview items array.")
|
||||
#expect(previewItemCount == 1 + (2 * initialPadding), "The initial item count should be padded for the preview controller.")
|
||||
#expect(dataSource.initialItemIndex == initialPadding, "The initial item index should be padded for the preview controller.")
|
||||
|
||||
XCTAssertEqual(displayedItem.id, initialItem.id.eventOrTransactionID, "The displayed item should be the initial item.")
|
||||
XCTAssertEqual(dataSource.currentMediaItemID, initialItem.id.eventOrTransactionID, "The current item should also be the initial item.")
|
||||
#expect(displayedItem.id == initialItem.id.eventOrTransactionID, "The displayed item should be the initial item.")
|
||||
#expect(dataSource.currentMediaItemID == initialItem.id.eventOrTransactionID, "The current item should also be the initial item.")
|
||||
|
||||
// When the timeline loads more items but still doesn't include the initial item.
|
||||
let failure = deferFailure(dataSource.previewItemsPaginationPublisher, timeout: 1) { _ in true }
|
||||
let failure = deferFailure(dataSource.previewItemsPaginationPublisher, timeout: .seconds(1)) { _ in true }
|
||||
let loadedItems = newChunk().map { RoomTimelineItemViewState(item: $0, groupStyle: .single) }
|
||||
dataSource.updatePreviewItems(itemViewStates: loadedItems)
|
||||
try await failure.fulfill()
|
||||
|
||||
// Then the preview controller shouldn't update the available preview items.
|
||||
previewItemCount = dataSource.numberOfPreviewItems(in: previewController)
|
||||
displayedItem = try XCTUnwrap(dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem.Media,
|
||||
"A preview item should be found.")
|
||||
displayedItem = try #require(dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem.Media,
|
||||
"A preview item should be found.")
|
||||
|
||||
XCTAssertEqual(dataSource.previewItems.count, 1, "No new items should have been added to the array.")
|
||||
XCTAssertEqual(previewItemCount, 1 + (2 * initialPadding), "The initial item count should not change.")
|
||||
XCTAssertEqual(dataSource.initialItemIndex, initialPadding, "The initial item index should not change.")
|
||||
#expect(dataSource.previewItems.count == 1, "No new items should have been added to the array.")
|
||||
#expect(previewItemCount == 1 + (2 * initialPadding), "The initial item count should not change.")
|
||||
#expect(dataSource.initialItemIndex == initialPadding, "The initial item index should not change.")
|
||||
|
||||
XCTAssertEqual(displayedItem.id, initialItem.id.eventOrTransactionID, "The displayed item should not change.")
|
||||
XCTAssertEqual(dataSource.currentMediaItemID, initialItem.id.eventOrTransactionID, "The current item not change.")
|
||||
#expect(displayedItem.id == initialItem.id.eventOrTransactionID, "The displayed item should not change.")
|
||||
#expect(dataSource.currentMediaItemID == initialItem.id.eventOrTransactionID, "The current item not change.")
|
||||
}
|
||||
|
||||
// MARK: Helpers
|
||||
@@ -255,6 +244,30 @@ class TimelineMediaPreviewDataSourceTests: XCTestCase {
|
||||
.compactMap { $0 as? EventBasedMessageTimelineItemProtocol }
|
||||
.filter(\.supportsMediaCaption) // Voice messages can't be previewed (and don't support captions).
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private func assertInitialDataSource() throws -> TimelineMediaPreviewDataSource {
|
||||
// Given a data source built with the initial items.
|
||||
let dataSource = TimelineMediaPreviewDataSource(itemViewStates: initialMediaViewStates,
|
||||
initialItem: initialMediaItems[initialItemIndex],
|
||||
initialPadding: initialPadding,
|
||||
paginationState: .initial)
|
||||
|
||||
// When the preview controller displays the data.
|
||||
let previewItemCount = dataSource.numberOfPreviewItems(in: previewController)
|
||||
let displayedItem = try #require(dataSource.previewController(previewController, previewItemAt: dataSource.initialItemIndex) as? TimelineMediaPreviewItem.Media,
|
||||
"A preview item should be found.")
|
||||
|
||||
// Then the preview controller should be showing the initial item and the data source should reflect this.
|
||||
#expect(dataSource.initialItemIndex == initialItemIndex + initialPadding, "The initial item index should be padded for the preview controller.")
|
||||
#expect(displayedItem.id == initialMediaItems[initialItemIndex].id.eventOrTransactionID, "The displayed item should be the initial item.")
|
||||
#expect(dataSource.currentMediaItemID == initialMediaItems[initialItemIndex].id.eventOrTransactionID, "The current item should also be the initial item.")
|
||||
|
||||
#expect(dataSource.previewItems.count == initialMediaViewStates.count, "The initial count of preview items should be correct.")
|
||||
#expect(previewItemCount == initialMediaViewStates.count + (2 * initialPadding), "The initial item count should be padded for the preview controller.")
|
||||
|
||||
return dataSource
|
||||
}
|
||||
}
|
||||
|
||||
private extension TimelineMediaPreviewDataSource {
|
||||
|
||||
@@ -11,10 +11,11 @@ import Combine
|
||||
import MatrixRustSDK
|
||||
import QuickLook
|
||||
import SwiftUI
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@Suite
|
||||
@MainActor
|
||||
class TimelineMediaPreviewViewModelTests: XCTestCase {
|
||||
struct TimelineMediaPreviewViewModelTests {
|
||||
var viewModel: TimelineMediaPreviewViewModel!
|
||||
var context: TimelineMediaPreviewViewModel.Context {
|
||||
viewModel.context
|
||||
@@ -24,49 +25,52 @@ class TimelineMediaPreviewViewModelTests: XCTestCase {
|
||||
var photoLibraryManager: PhotoLibraryManagerMock!
|
||||
var timelineController: MockTimelineController!
|
||||
|
||||
func testLoadingItem() async throws {
|
||||
@Test
|
||||
mutating func loadingItem() async throws {
|
||||
// Given a fresh view model.
|
||||
setupViewModel()
|
||||
XCTAssertFalse(mediaProvider.loadFileFromSourceFilenameCalled)
|
||||
XCTAssertEqual(context.viewState.currentItem, .media(context.viewState.dataSource.previewItems[0]))
|
||||
XCTAssertNotNil(context.viewState.currentItemActions)
|
||||
#expect(!mediaProvider.loadFileFromSourceFilenameCalled)
|
||||
#expect(context.viewState.currentItem == .media(context.viewState.dataSource.previewItems[0]))
|
||||
#expect(context.viewState.currentItemActions != nil)
|
||||
|
||||
// When the preview controller sets the current item.
|
||||
try await loadInitialItem()
|
||||
|
||||
// Then the view model should load the item and update its view state.
|
||||
XCTAssertTrue(mediaProvider.loadFileFromSourceFilenameCalled)
|
||||
XCTAssertEqual(context.viewState.currentItem, .media(context.viewState.dataSource.previewItems[0]))
|
||||
XCTAssertNotNil(context.viewState.currentItemActions)
|
||||
#expect(mediaProvider.loadFileFromSourceFilenameCalled)
|
||||
#expect(context.viewState.currentItem == .media(context.viewState.dataSource.previewItems[0]))
|
||||
#expect(context.viewState.currentItemActions != nil)
|
||||
}
|
||||
|
||||
func testLoadingItemFailure() async throws {
|
||||
@Test
|
||||
mutating func loadingItemFailure() async throws {
|
||||
// Given a fresh view model.
|
||||
setupViewModel()
|
||||
guard case let .media(mediaItem) = context.viewState.currentItem else {
|
||||
XCTFail("There should be a current item")
|
||||
Issue.record("There should be a current item")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertFalse(mediaProvider.loadFileFromSourceFilenameCalled)
|
||||
XCTAssertEqual(mediaItem, context.viewState.dataSource.previewItems[0])
|
||||
XCTAssertNil(mediaItem.downloadError)
|
||||
#expect(!mediaProvider.loadFileFromSourceFilenameCalled)
|
||||
#expect(mediaItem == context.viewState.dataSource.previewItems[0])
|
||||
#expect(mediaItem.downloadError == nil)
|
||||
|
||||
// When the preview controller sets an item that fails to load.
|
||||
mediaProvider.loadFileFromSourceFilenameClosure = { _, _ in .failure(.failedRetrievingFile) }
|
||||
let failure = deferFailure(viewModel.state.previewControllerDriver, timeout: 1) { $0.isItemLoaded }
|
||||
let failure = deferFailure(viewModel.state.previewControllerDriver, timeout: .seconds(1)) { $0.isItemLoaded }
|
||||
context.send(viewAction: .updateCurrentItem(.media(context.viewState.dataSource.previewItems[0])))
|
||||
try await failure.fulfill()
|
||||
|
||||
// Then the view model should load the item and update its view state.
|
||||
XCTAssertTrue(mediaProvider.loadFileFromSourceFilenameCalled)
|
||||
XCTAssertEqual(mediaItem, context.viewState.dataSource.previewItems[0])
|
||||
XCTAssertNotNil(mediaItem.downloadError)
|
||||
#expect(mediaProvider.loadFileFromSourceFilenameCalled)
|
||||
#expect(mediaItem == context.viewState.dataSource.previewItems[0])
|
||||
#expect(mediaItem.downloadError != nil)
|
||||
}
|
||||
|
||||
func testSwipingBetweenItems() async throws {
|
||||
@Test
|
||||
mutating func swipingBetweenItems() async throws {
|
||||
// Given a view model with a loaded item.
|
||||
try await testLoadingItem()
|
||||
try await loadingItem()
|
||||
|
||||
// When swiping to another item.
|
||||
let deferred = deferFulfillment(viewModel.state.previewControllerDriver) { $0.isItemLoaded }
|
||||
@@ -74,41 +78,43 @@ class TimelineMediaPreviewViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the view model should load the item and update its view state.
|
||||
XCTAssertEqual(mediaProvider.loadFileFromSourceFilenameCallsCount, 2)
|
||||
XCTAssertEqual(context.viewState.currentItem, .media(context.viewState.dataSource.previewItems[1]))
|
||||
#expect(mediaProvider.loadFileFromSourceFilenameCallsCount == 2)
|
||||
#expect(context.viewState.currentItem == .media(context.viewState.dataSource.previewItems[1]))
|
||||
|
||||
// When swiping back to the first item.
|
||||
let failure = deferFailure(viewModel.state.previewControllerDriver, timeout: 1) { $0.isItemLoaded }
|
||||
let failure = deferFailure(viewModel.state.previewControllerDriver, timeout: .seconds(1)) { $0.isItemLoaded }
|
||||
context.send(viewAction: .updateCurrentItem(.media(context.viewState.dataSource.previewItems[0])))
|
||||
try await failure.fulfill()
|
||||
|
||||
// Then the view model should not need to load the item, but should still update its view state.
|
||||
XCTAssertEqual(mediaProvider.loadFileFromSourceFilenameCallsCount, 2)
|
||||
XCTAssertEqual(context.viewState.currentItem, .media(context.viewState.dataSource.previewItems[0]))
|
||||
#expect(mediaProvider.loadFileFromSourceFilenameCallsCount == 2)
|
||||
#expect(context.viewState.currentItem == .media(context.viewState.dataSource.previewItems[0]))
|
||||
}
|
||||
|
||||
func testLoadingMoreItems() async throws {
|
||||
@Test
|
||||
mutating func loadingMoreItems() async throws {
|
||||
// Given a view model with a loaded item.
|
||||
try await testLoadingItem()
|
||||
XCTAssertEqual(timelineController.paginateBackwardsCallCount, 0)
|
||||
try await loadingItem()
|
||||
#expect(timelineController.paginateBackwardsCallCount == 0)
|
||||
|
||||
// When swiping to a "loading more" item and there are more media items to load.
|
||||
timelineController.paginationState = .init(backward: .idle, forward: .endReached)
|
||||
timelineController.backPaginationResponses.append(RoomTimelineItemFixtures.mediaChunk)
|
||||
let failure = deferFailure(viewModel.state.previewControllerDriver, timeout: 1) { $0.isItemLoaded }
|
||||
let failure = deferFailure(viewModel.state.previewControllerDriver, timeout: .seconds(1)) { $0.isItemLoaded }
|
||||
context.send(viewAction: .updateCurrentItem(.loading(.paginatingBackwards)))
|
||||
try await failure.fulfill()
|
||||
|
||||
// Then there should no longer be a media preview and instead of loading any media, a pagination request should be made.
|
||||
XCTAssertEqual(mediaProvider.loadFileFromSourceFilenameCallsCount, 1)
|
||||
XCTAssertEqual(context.viewState.currentItem, .loading(.paginatingBackwards)) // Note: This item only changes when the preview controller handles the new items.
|
||||
XCTAssertEqual(timelineController.paginateBackwardsCallCount, 1)
|
||||
#expect(mediaProvider.loadFileFromSourceFilenameCallsCount == 1)
|
||||
#expect(context.viewState.currentItem == .loading(.paginatingBackwards)) // Note: This item only changes when the preview controller handles the new items.
|
||||
#expect(timelineController.paginateBackwardsCallCount == 1)
|
||||
}
|
||||
|
||||
func testPagination() async throws {
|
||||
@Test
|
||||
mutating func pagination() async throws {
|
||||
// Given a view model with a loaded item.
|
||||
try await testLoadingItem()
|
||||
XCTAssertEqual(context.viewState.dataSource.previewItems.count, 3)
|
||||
try await loadingItem()
|
||||
#expect(context.viewState.dataSource.previewItems.count == 3)
|
||||
|
||||
// When more items are added via a back pagination.
|
||||
let deferred = deferFulfillment(context.viewState.dataSource.previewItemsPaginationPublisher) { _ in true }
|
||||
@@ -118,22 +124,23 @@ class TimelineMediaPreviewViewModelTests: XCTestCase {
|
||||
|
||||
// And the preview controller attempts to update the current item (now at a new index in the array but it hasn't changed in the data source).
|
||||
mediaProvider.loadFileFromSourceFilenameClosure = { _, _ in .failure(.failedRetrievingFile) }
|
||||
let failure = deferFailure(viewModel.state.previewControllerDriver, timeout: 1) { $0.isItemLoaded }
|
||||
let failure = deferFailure(viewModel.state.previewControllerDriver, timeout: .seconds(1)) { $0.isItemLoaded }
|
||||
context.send(viewAction: .updateCurrentItem(.media(context.viewState.dataSource.previewItems[3])))
|
||||
try await failure.fulfill()
|
||||
|
||||
// Then the current item shouldn't need to be reloaded.
|
||||
XCTAssertEqual(context.viewState.dataSource.previewItems.count, 6)
|
||||
XCTAssertEqual(mediaProvider.loadFileFromSourceFilenameCallsCount, 1)
|
||||
#expect(context.viewState.dataSource.previewItems.count == 6)
|
||||
#expect(mediaProvider.loadFileFromSourceFilenameCallsCount == 1)
|
||||
}
|
||||
|
||||
func testViewInRoomTimeline() async throws {
|
||||
@Test
|
||||
mutating func viewInRoomTimeline() async throws {
|
||||
// Given a view model with a loaded item.
|
||||
try await testLoadingItem()
|
||||
try await loadingItem()
|
||||
|
||||
// When choosing to view the current item in the timeline.
|
||||
guard case let .media(mediaItem) = context.viewState.currentItem else {
|
||||
XCTFail("There should be a current item.")
|
||||
Issue.record("There should be a current item.")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -144,13 +151,14 @@ class TimelineMediaPreviewViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testRedactConfirmation() async throws {
|
||||
@Test
|
||||
mutating func redactConfirmation() async throws {
|
||||
// Given a view model with a loaded item.
|
||||
try await testLoadingItem()
|
||||
XCTAssertNil(context.redactConfirmationItem)
|
||||
XCTAssertFalse(timelineController.redactCalled)
|
||||
try await loadingItem()
|
||||
#expect(context.redactConfirmationItem == nil)
|
||||
#expect(!timelineController.redactCalled)
|
||||
guard case let .media(mediaItem) = context.viewState.currentItem else {
|
||||
XCTFail("There should be a current item.")
|
||||
Issue.record("There should be a current item.")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -161,17 +169,17 @@ class TimelineMediaPreviewViewModelTests: XCTestCase {
|
||||
// Then the details sheet should be presented.
|
||||
let action = try await deferredDriver.fulfill()
|
||||
guard case let .showItemDetails(mediaDetailsItem) = action else {
|
||||
XCTFail("The action should include the media item.")
|
||||
Issue.record("The action should include the media item.")
|
||||
return
|
||||
}
|
||||
XCTAssertEqual(.media(mediaDetailsItem), context.viewState.currentItem)
|
||||
#expect(.media(mediaDetailsItem) == context.viewState.currentItem)
|
||||
|
||||
// When choosing to redact the item.
|
||||
context.send(viewAction: .menuAction(.redact, item: mediaItem))
|
||||
|
||||
// Then the confirmation sheet should be presented.
|
||||
XCTAssertEqual(context.redactConfirmationItem, mediaItem)
|
||||
XCTAssertFalse(timelineController.redactCalled)
|
||||
#expect(context.redactConfirmationItem == mediaItem)
|
||||
#expect(!timelineController.redactCalled)
|
||||
|
||||
// When confirming the redaction.
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0 == .dismiss }
|
||||
@@ -179,37 +187,39 @@ class TimelineMediaPreviewViewModelTests: XCTestCase {
|
||||
|
||||
// Then the item should be redacted and the view should be dismissed.
|
||||
try await deferred.fulfill()
|
||||
XCTAssertTrue(timelineController.redactCalled)
|
||||
#expect(timelineController.redactCalled)
|
||||
}
|
||||
|
||||
func testSaveImage() async throws {
|
||||
@Test
|
||||
mutating func saveImage() async throws {
|
||||
// Given a view model with a loaded image.
|
||||
try await testLoadingItem()
|
||||
try await loadingItem()
|
||||
guard case let .media(mediaItem) = context.viewState.currentItem else {
|
||||
XCTFail("There should be a current item")
|
||||
Issue.record("There should be a current item")
|
||||
return
|
||||
}
|
||||
XCTAssertEqual(mediaItem.contentType, "JPEG image")
|
||||
#expect(mediaItem.contentType == "JPEG image")
|
||||
|
||||
// When choosing to save the image.
|
||||
context.send(viewAction: .menuAction(.save, item: mediaItem))
|
||||
try await Task.sleep(for: .seconds(0.5))
|
||||
|
||||
// Then the image should be saved as a photo to the user's photo library.
|
||||
XCTAssertTrue(photoLibraryManager.addResourceAtCalled)
|
||||
XCTAssertEqual(photoLibraryManager.addResourceAtReceivedArguments?.type, .photo)
|
||||
XCTAssertEqual(photoLibraryManager.addResourceAtReceivedArguments?.url, mediaItem.fileHandle?.url)
|
||||
#expect(photoLibraryManager.addResourceAtCalled)
|
||||
#expect(photoLibraryManager.addResourceAtReceivedArguments?.type == .photo)
|
||||
#expect(photoLibraryManager.addResourceAtReceivedArguments?.url == mediaItem.fileHandle?.url)
|
||||
}
|
||||
|
||||
func testSaveImageWithoutAuthorization() async throws {
|
||||
@Test
|
||||
mutating func saveImageWithoutAuthorization() async throws {
|
||||
// Given a view model with a loaded image where the user has denied access to the photo library.
|
||||
setupViewModel(photoLibraryAuthorizationDenied: true)
|
||||
try await loadInitialItem()
|
||||
guard case let .media(mediaItem) = context.viewState.currentItem else {
|
||||
XCTFail("There should be a current item")
|
||||
Issue.record("There should be a current item")
|
||||
return
|
||||
}
|
||||
XCTAssertEqual(mediaItem.contentType, "JPEG image")
|
||||
#expect(mediaItem.contentType == "JPEG image")
|
||||
|
||||
// When choosing to save the image.
|
||||
let deferred = deferFulfillment(context.viewState.previewControllerDriver) { $0.isAuthorizationRequired }
|
||||
@@ -217,38 +227,40 @@ class TimelineMediaPreviewViewModelTests: XCTestCase {
|
||||
|
||||
// Then the user should be prompted to allow access.
|
||||
try await deferred.fulfill()
|
||||
XCTAssertTrue(photoLibraryManager.addResourceAtCalled)
|
||||
#expect(photoLibraryManager.addResourceAtCalled)
|
||||
}
|
||||
|
||||
func testSaveVideo() async throws {
|
||||
@Test
|
||||
mutating func saveVideo() async throws {
|
||||
// Given a view model with a loaded video.
|
||||
setupViewModel(initialItemIndex: 1)
|
||||
try await loadInitialItem()
|
||||
guard case let .media(mediaItem) = context.viewState.currentItem else {
|
||||
XCTFail("There should be a current item")
|
||||
Issue.record("There should be a current item")
|
||||
return
|
||||
}
|
||||
XCTAssertEqual(mediaItem.contentType, "MPEG-4 movie")
|
||||
#expect(mediaItem.contentType == "MPEG-4 movie")
|
||||
|
||||
// When choosing to save the video.
|
||||
context.send(viewAction: .menuAction(.save, item: mediaItem))
|
||||
try await Task.sleep(for: .seconds(0.5))
|
||||
|
||||
// Then the video should be saved as a video in the user's photo library.
|
||||
XCTAssertTrue(photoLibraryManager.addResourceAtCalled)
|
||||
XCTAssertEqual(photoLibraryManager.addResourceAtReceivedArguments?.type, .video)
|
||||
XCTAssertEqual(photoLibraryManager.addResourceAtReceivedArguments?.url, mediaItem.fileHandle?.url)
|
||||
#expect(photoLibraryManager.addResourceAtCalled)
|
||||
#expect(photoLibraryManager.addResourceAtReceivedArguments?.type == .video)
|
||||
#expect(photoLibraryManager.addResourceAtReceivedArguments?.url == mediaItem.fileHandle?.url)
|
||||
}
|
||||
|
||||
func testSaveFile() async throws {
|
||||
@Test
|
||||
mutating func saveFile() async throws {
|
||||
// Given a view model with a loaded file.
|
||||
setupViewModel(initialItemIndex: 2)
|
||||
try await loadInitialItem()
|
||||
guard case let .media(mediaItem) = context.viewState.currentItem else {
|
||||
XCTFail("There should be a current item")
|
||||
Issue.record("There should be a current item")
|
||||
return
|
||||
}
|
||||
XCTAssertEqual(mediaItem.contentType, "PDF document")
|
||||
#expect(mediaItem.contentType == "PDF document")
|
||||
|
||||
// When choosing to save the file.
|
||||
let deferred = deferFulfillment(context.viewState.previewControllerDriver) { $0.isExportFile }
|
||||
@@ -256,13 +268,13 @@ class TimelineMediaPreviewViewModelTests: XCTestCase {
|
||||
let exportAction = try await deferred.fulfill()
|
||||
|
||||
guard case let .exportFile(file) = exportAction else {
|
||||
XCTFail("Unexpected action")
|
||||
Issue.record("Unexpected action")
|
||||
return
|
||||
}
|
||||
|
||||
// Then the binding should be set for the user to export the file to their specified location.
|
||||
XCTAssertFalse(photoLibraryManager.addResourceAtCalled)
|
||||
XCTAssertEqual(file.url, mediaItem.fileHandle?.url)
|
||||
#expect(!photoLibraryManager.addResourceAtCalled)
|
||||
#expect(file.url == mediaItem.fileHandle?.url)
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
@@ -272,14 +284,14 @@ class TimelineMediaPreviewViewModelTests: XCTestCase {
|
||||
let initialItem = context.viewState.dataSource.previewController(QLPreviewController(),
|
||||
previewItemAt: context.viewState.dataSource.initialItemIndex)
|
||||
guard let initialPreviewItem = initialItem as? TimelineMediaPreviewItem.Media else {
|
||||
XCTFail("The initial item should be a media preview.")
|
||||
Issue.record("The initial item should be a media preview.")
|
||||
return
|
||||
}
|
||||
context.send(viewAction: .updateCurrentItem(.media(initialPreviewItem)))
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
private func setupViewModel(initialItemIndex: Int = 0, photoLibraryAuthorizationDenied: Bool = false) {
|
||||
private mutating func setupViewModel(initialItemIndex: Int = 0, photoLibraryAuthorizationDenied: Bool = false) {
|
||||
let initialItems = makeItems()
|
||||
timelineController = MockTimelineController(timelineKind: .media(.mediaFilesScreen))
|
||||
timelineController.timelineItems = initialItems
|
||||
|
||||
@@ -8,27 +8,30 @@
|
||||
|
||||
import Combine
|
||||
@testable import ElementX
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@Suite
|
||||
@MainActor
|
||||
class TimelineViewModelTests: XCTestCase {
|
||||
final class TimelineViewModelTests {
|
||||
var userIndicatorControllerMock: UserIndicatorControllerMock!
|
||||
var cancellables = Set<AnyCancellable>()
|
||||
|
||||
override func setUp() async throws {
|
||||
init() async throws {
|
||||
AppSettings.resetAllSettings()
|
||||
cancellables.removeAll()
|
||||
userIndicatorControllerMock = UserIndicatorControllerMock.default
|
||||
}
|
||||
|
||||
override func tearDown() async throws {
|
||||
deinit {
|
||||
userIndicatorControllerMock = nil
|
||||
}
|
||||
|
||||
// MARK: - Message Grouping
|
||||
|
||||
func testMessageGrouping() {
|
||||
@Test
|
||||
func messageGrouping() {
|
||||
// Given 3 messages from Bob.
|
||||
let items = [
|
||||
TextRoomTimelineItem(text: "Message 1",
|
||||
@@ -45,12 +48,13 @@ class TimelineViewModelTests: XCTestCase {
|
||||
let viewModel = makeViewModel(timelineController: timelineController)
|
||||
|
||||
// Then the messages should be grouped together.
|
||||
XCTAssertEqual(viewModel.state.timelineState.itemViewStates[0].groupStyle, .first, "Nothing should prevent the first message from being grouped.")
|
||||
XCTAssertEqual(viewModel.state.timelineState.itemViewStates[1].groupStyle, .middle, "Nothing should prevent the middle message from being grouped.")
|
||||
XCTAssertEqual(viewModel.state.timelineState.itemViewStates[2].groupStyle, .last, "Nothing should prevent the last message from being grouped.")
|
||||
#expect(viewModel.state.timelineState.itemViewStates[0].groupStyle == .first, "Nothing should prevent the first message from being grouped.")
|
||||
#expect(viewModel.state.timelineState.itemViewStates[1].groupStyle == .middle, "Nothing should prevent the middle message from being grouped.")
|
||||
#expect(viewModel.state.timelineState.itemViewStates[2].groupStyle == .last, "Nothing should prevent the last message from being grouped.")
|
||||
}
|
||||
|
||||
func testMessageGroupingMultipleSenders() {
|
||||
@Test
|
||||
func messageGroupingMultipleSenders() {
|
||||
// Given some interleaved messages from Bob and Alice.
|
||||
let items = [
|
||||
TextRoomTimelineItem(text: "Message 1",
|
||||
@@ -73,15 +77,16 @@ class TimelineViewModelTests: XCTestCase {
|
||||
let viewModel = makeViewModel(timelineController: timelineController)
|
||||
|
||||
// Then the messages should be grouped by sender.
|
||||
XCTAssertEqual(viewModel.state.timelineState.itemViewStates[0].groupStyle, .single, "A message should not be grouped when the sender changes.")
|
||||
XCTAssertEqual(viewModel.state.timelineState.itemViewStates[1].groupStyle, .single, "A message should not be grouped when the sender changes.")
|
||||
XCTAssertEqual(viewModel.state.timelineState.itemViewStates[2].groupStyle, .first, "A group should start with a new sender if there are more messages from that sender.")
|
||||
XCTAssertEqual(viewModel.state.timelineState.itemViewStates[3].groupStyle, .last, "A group should be ended when the sender changes in the next message.")
|
||||
XCTAssertEqual(viewModel.state.timelineState.itemViewStates[4].groupStyle, .first, "A group should start with a new sender if there are more messages from that sender.")
|
||||
XCTAssertEqual(viewModel.state.timelineState.itemViewStates[5].groupStyle, .last, "A group should be ended when the sender changes in the next message.")
|
||||
#expect(viewModel.state.timelineState.itemViewStates[0].groupStyle == .single, "A message should not be grouped when the sender changes.")
|
||||
#expect(viewModel.state.timelineState.itemViewStates[1].groupStyle == .single, "A message should not be grouped when the sender changes.")
|
||||
#expect(viewModel.state.timelineState.itemViewStates[2].groupStyle == .first, "A group should start with a new sender if there are more messages from that sender.")
|
||||
#expect(viewModel.state.timelineState.itemViewStates[3].groupStyle == .last, "A group should be ended when the sender changes in the next message.")
|
||||
#expect(viewModel.state.timelineState.itemViewStates[4].groupStyle == .first, "A group should start with a new sender if there are more messages from that sender.")
|
||||
#expect(viewModel.state.timelineState.itemViewStates[5].groupStyle == .last, "A group should be ended when the sender changes in the next message.")
|
||||
}
|
||||
|
||||
func testMessageGroupingWithLeadingReactions() {
|
||||
@Test
|
||||
func messageGroupingWithLeadingReactions() {
|
||||
// Given 3 messages from Bob where the first message has a reaction.
|
||||
let items = [
|
||||
TextRoomTimelineItem(text: "Message 1",
|
||||
@@ -99,12 +104,13 @@ class TimelineViewModelTests: XCTestCase {
|
||||
let viewModel = makeViewModel(timelineController: timelineController)
|
||||
|
||||
// Then the first message should not be grouped but the other two should.
|
||||
XCTAssertEqual(viewModel.state.timelineState.itemViewStates[0].groupStyle, .single, "When the first message has reactions it should not be grouped.")
|
||||
XCTAssertEqual(viewModel.state.timelineState.itemViewStates[1].groupStyle, .first, "A new group should be made when the preceding message has reactions.")
|
||||
XCTAssertEqual(viewModel.state.timelineState.itemViewStates[2].groupStyle, .last, "Nothing should prevent the last message from being grouped.")
|
||||
#expect(viewModel.state.timelineState.itemViewStates[0].groupStyle == .single, "When the first message has reactions it should not be grouped.")
|
||||
#expect(viewModel.state.timelineState.itemViewStates[1].groupStyle == .first, "A new group should be made when the preceding message has reactions.")
|
||||
#expect(viewModel.state.timelineState.itemViewStates[2].groupStyle == .last, "Nothing should prevent the last message from being grouped.")
|
||||
}
|
||||
|
||||
func testMessageGroupingWithInnerReactions() {
|
||||
@Test
|
||||
func messageGroupingWithInnerReactions() {
|
||||
// Given 3 messages from Bob where the middle message has a reaction.
|
||||
let items = [
|
||||
TextRoomTimelineItem(text: "Message 1",
|
||||
@@ -122,12 +128,13 @@ class TimelineViewModelTests: XCTestCase {
|
||||
let viewModel = makeViewModel(timelineController: timelineController)
|
||||
|
||||
// Then the first and second messages should be grouped and the last one should not.
|
||||
XCTAssertEqual(viewModel.state.timelineState.itemViewStates[0].groupStyle, .first, "Nothing should prevent the first message from being grouped.")
|
||||
XCTAssertEqual(viewModel.state.timelineState.itemViewStates[1].groupStyle, .last, "When the message has reactions, the group should end here.")
|
||||
XCTAssertEqual(viewModel.state.timelineState.itemViewStates[2].groupStyle, .single, "The last message should not be grouped when the preceding message has reactions.")
|
||||
#expect(viewModel.state.timelineState.itemViewStates[0].groupStyle == .first, "Nothing should prevent the first message from being grouped.")
|
||||
#expect(viewModel.state.timelineState.itemViewStates[1].groupStyle == .last, "When the message has reactions, the group should end here.")
|
||||
#expect(viewModel.state.timelineState.itemViewStates[2].groupStyle == .single, "The last message should not be grouped when the preceding message has reactions.")
|
||||
}
|
||||
|
||||
func testMessageGroupingWithTrailingReactions() {
|
||||
@Test
|
||||
func messageGroupingWithTrailingReactions() {
|
||||
// Given 3 messages from Bob where the last message has a reaction.
|
||||
let items = [
|
||||
TextRoomTimelineItem(text: "Message 1",
|
||||
@@ -145,14 +152,15 @@ class TimelineViewModelTests: XCTestCase {
|
||||
let viewModel = makeViewModel(timelineController: timelineController)
|
||||
|
||||
// Then the messages should be grouped together.
|
||||
XCTAssertEqual(viewModel.state.timelineState.itemViewStates[0].groupStyle, .first, "Nothing should prevent the first message from being grouped.")
|
||||
XCTAssertEqual(viewModel.state.timelineState.itemViewStates[1].groupStyle, .middle, "Nothing should prevent the second message from being grouped.")
|
||||
XCTAssertEqual(viewModel.state.timelineState.itemViewStates[2].groupStyle, .last, "Reactions on the last message should not prevent it from being grouped.")
|
||||
#expect(viewModel.state.timelineState.itemViewStates[0].groupStyle == .first, "Nothing should prevent the first message from being grouped.")
|
||||
#expect(viewModel.state.timelineState.itemViewStates[1].groupStyle == .middle, "Nothing should prevent the second message from being grouped.")
|
||||
#expect(viewModel.state.timelineState.itemViewStates[2].groupStyle == .last, "Reactions on the last message should not prevent it from being grouped.")
|
||||
}
|
||||
|
||||
// MARK: - Focussing
|
||||
|
||||
func testFocusItem() async throws {
|
||||
@Test
|
||||
func focusItem() async throws {
|
||||
// Given a room with 3 items loaded in a live timeline.
|
||||
let items = [TextRoomTimelineItem(eventID: "t1"),
|
||||
TextRoomTimelineItem(eventID: "t2"),
|
||||
@@ -161,9 +169,9 @@ class TimelineViewModelTests: XCTestCase {
|
||||
timelineController.timelineItems = items
|
||||
|
||||
let viewModel = makeViewModel(timelineController: timelineController)
|
||||
XCTAssertEqual(timelineController.focusOnEventCallCount, 0)
|
||||
XCTAssertTrue(viewModel.context.viewState.timelineState.isLive)
|
||||
XCTAssertNil(viewModel.context.viewState.timelineState.focussedEvent)
|
||||
#expect(timelineController.focusOnEventCallCount == 0)
|
||||
#expect(viewModel.context.viewState.timelineState.isLive)
|
||||
#expect(viewModel.context.viewState.timelineState.focussedEvent == nil)
|
||||
|
||||
// When focussing on an item that isn't loaded.
|
||||
let deferred = deferFulfillment(viewModel.context.$viewState) { !$0.timelineState.isLive }
|
||||
@@ -171,12 +179,13 @@ class TimelineViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then a new timeline should be loaded and the room focussed on that event.
|
||||
XCTAssertEqual(timelineController.focusOnEventCallCount, 1)
|
||||
XCTAssertFalse(viewModel.context.viewState.timelineState.isLive)
|
||||
XCTAssertEqual(viewModel.context.viewState.timelineState.focussedEvent, .init(eventID: "t4", appearance: .immediate))
|
||||
#expect(timelineController.focusOnEventCallCount == 1)
|
||||
#expect(!viewModel.context.viewState.timelineState.isLive)
|
||||
#expect(viewModel.context.viewState.timelineState.focussedEvent == .init(eventID: "t4", appearance: .immediate))
|
||||
}
|
||||
|
||||
func testFocusLoadedItem() async throws {
|
||||
@Test
|
||||
func focusLoadedItem() async throws {
|
||||
// Given a room with 3 items loaded in a live timeline.
|
||||
let items = [TextRoomTimelineItem(eventID: "t1"),
|
||||
TextRoomTimelineItem(eventID: "t2"),
|
||||
@@ -185,22 +194,23 @@ class TimelineViewModelTests: XCTestCase {
|
||||
timelineController.timelineItems = items
|
||||
|
||||
let viewModel = makeViewModel(timelineController: timelineController)
|
||||
XCTAssertEqual(timelineController.focusOnEventCallCount, 0)
|
||||
XCTAssertTrue(viewModel.context.viewState.timelineState.isLive)
|
||||
XCTAssertNil(viewModel.context.viewState.timelineState.focussedEvent)
|
||||
#expect(timelineController.focusOnEventCallCount == 0)
|
||||
#expect(viewModel.context.viewState.timelineState.isLive)
|
||||
#expect(viewModel.context.viewState.timelineState.focussedEvent == nil)
|
||||
|
||||
// When focussing on a loaded item.
|
||||
let deferred = deferFailure(viewModel.context.$viewState, timeout: 1) { !$0.timelineState.isLive }
|
||||
let deferred = deferFailure(viewModel.context.$viewState, timeout: .seconds(1)) { !$0.timelineState.isLive }
|
||||
await viewModel.focusOnEvent(eventID: "t1")
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the timeline should remain live and the item should be focussed.
|
||||
XCTAssertEqual(timelineController.focusOnEventCallCount, 0)
|
||||
XCTAssertTrue(viewModel.context.viewState.timelineState.isLive)
|
||||
XCTAssertEqual(viewModel.context.viewState.timelineState.focussedEvent, .init(eventID: "t1", appearance: .animated))
|
||||
#expect(timelineController.focusOnEventCallCount == 0)
|
||||
#expect(viewModel.context.viewState.timelineState.isLive)
|
||||
#expect(viewModel.context.viewState.timelineState.focussedEvent == .init(eventID: "t1", appearance: .animated))
|
||||
}
|
||||
|
||||
func testFocusLive() async throws {
|
||||
@Test
|
||||
func focusLive() async throws {
|
||||
// Given a room with a non-live timeline focussed on a particular event.
|
||||
let items = [TextRoomTimelineItem(eventID: "t1"),
|
||||
TextRoomTimelineItem(eventID: "t2"),
|
||||
@@ -214,9 +224,9 @@ class TimelineViewModelTests: XCTestCase {
|
||||
await viewModel.focusOnEvent(eventID: "t4")
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertEqual(timelineController.focusLiveCallCount, 0)
|
||||
XCTAssertFalse(viewModel.context.viewState.timelineState.isLive)
|
||||
XCTAssertEqual(viewModel.context.viewState.timelineState.focussedEvent, .init(eventID: "t4", appearance: .immediate))
|
||||
#expect(timelineController.focusLiveCallCount == 0)
|
||||
#expect(!viewModel.context.viewState.timelineState.isLive)
|
||||
#expect(viewModel.context.viewState.timelineState.focussedEvent == .init(eventID: "t4", appearance: .immediate))
|
||||
|
||||
// When switching back to a live timeline.
|
||||
deferred = deferFulfillment(viewModel.context.$viewState) { $0.timelineState.isLive }
|
||||
@@ -224,21 +234,23 @@ class TimelineViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the timeline should switch back to being live and the event focus should be removed.
|
||||
XCTAssertEqual(timelineController.focusLiveCallCount, 1)
|
||||
XCTAssertTrue(viewModel.context.viewState.timelineState.isLive)
|
||||
XCTAssertNil(viewModel.context.viewState.timelineState.focussedEvent)
|
||||
#expect(timelineController.focusLiveCallCount == 1)
|
||||
#expect(viewModel.context.viewState.timelineState.isLive)
|
||||
#expect(viewModel.context.viewState.timelineState.focussedEvent == nil)
|
||||
}
|
||||
|
||||
func testInitialFocusViewState() {
|
||||
@Test
|
||||
func initialFocusViewState() {
|
||||
let timelineController = MockTimelineController()
|
||||
|
||||
let viewModel = makeViewModel(focussedEventID: "t10", timelineController: timelineController)
|
||||
XCTAssertEqual(viewModel.context.viewState.timelineState.focussedEvent, .init(eventID: "t10", appearance: .immediate))
|
||||
#expect(viewModel.context.viewState.timelineState.focussedEvent == .init(eventID: "t10", appearance: .immediate))
|
||||
}
|
||||
|
||||
// MARK: - Read Receipts
|
||||
|
||||
func testSendReadReceipt() async throws {
|
||||
@Test
|
||||
func sendReadReceipt() async throws {
|
||||
// Given a room with only text items in the timeline
|
||||
let items = [TextRoomTimelineItem(eventID: "t1"),
|
||||
TextRoomTimelineItem(eventID: "t2"),
|
||||
@@ -246,17 +258,18 @@ class TimelineViewModelTests: XCTestCase {
|
||||
let (viewModel, _, timelineProxy, _) = readReceiptsConfiguration(with: items)
|
||||
|
||||
// When sending a read receipt for the last item.
|
||||
try viewModel.context.send(viewAction: .sendReadReceiptIfNeeded(XCTUnwrap(items.last?.id)))
|
||||
try viewModel.context.send(viewAction: .sendReadReceiptIfNeeded(#require(items.last?.id)))
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
|
||||
// Then the receipt should be sent.
|
||||
XCTAssertEqual(timelineProxy.sendReadReceiptForTypeCalled, true)
|
||||
#expect(timelineProxy.sendReadReceiptForTypeCalled == true)
|
||||
let arguments = timelineProxy.sendReadReceiptForTypeReceivedArguments
|
||||
XCTAssertEqual(arguments?.eventID, "t3")
|
||||
XCTAssertEqual(arguments?.type, .read)
|
||||
#expect(arguments?.eventID == "t3")
|
||||
#expect(arguments?.type == .read)
|
||||
}
|
||||
|
||||
func testSendReadReceiptWithoutEvents() async throws {
|
||||
@Test
|
||||
func sendReadReceiptWithoutEvents() async throws {
|
||||
// Given a room with only virtual items.
|
||||
let items = [SeparatorRoomTimelineItem(uniqueID: .init("v1")),
|
||||
SeparatorRoomTimelineItem(uniqueID: .init("v2")),
|
||||
@@ -264,14 +277,15 @@ class TimelineViewModelTests: XCTestCase {
|
||||
let (viewModel, _, timelineProxy, _) = readReceiptsConfiguration(with: items)
|
||||
|
||||
// When sending a read receipt for the last item.
|
||||
try viewModel.context.send(viewAction: .sendReadReceiptIfNeeded(XCTUnwrap(items.last?.id)))
|
||||
try viewModel.context.send(viewAction: .sendReadReceiptIfNeeded(#require(items.last?.id)))
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
|
||||
// Then nothing should be sent.
|
||||
XCTAssertEqual(timelineProxy.sendReadReceiptForTypeCalled, false)
|
||||
#expect(timelineProxy.sendReadReceiptForTypeCalled == false)
|
||||
}
|
||||
|
||||
func testSendReadReceiptVirtualLast() async throws {
|
||||
@Test
|
||||
func sendReadReceiptVirtualLast() async throws {
|
||||
// Given a room where the last event is a virtual item.
|
||||
let items: [RoomTimelineItemProtocol] = [TextRoomTimelineItem(eventID: "t1"),
|
||||
TextRoomTimelineItem(eventID: "t2"),
|
||||
@@ -279,7 +293,7 @@ class TimelineViewModelTests: XCTestCase {
|
||||
let (viewModel, _, _, _) = readReceiptsConfiguration(with: items)
|
||||
|
||||
// When sending a read receipt for the last item.
|
||||
try viewModel.context.send(viewAction: .sendReadReceiptIfNeeded(XCTUnwrap(items.last?.id)))
|
||||
try viewModel.context.send(viewAction: .sendReadReceiptIfNeeded(#require(items.last?.id)))
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
}
|
||||
|
||||
@@ -314,7 +328,8 @@ class TimelineViewModelTests: XCTestCase {
|
||||
return (viewModel, roomProxy, timelineProxy, timelineController)
|
||||
}
|
||||
|
||||
func testShowReadReceipts() async throws {
|
||||
@Test
|
||||
func showReadReceipts() async throws {
|
||||
let receipts: [ReadReceipt] = [.init(userID: "@alice:matrix.org", formattedTimestamp: "12:00"),
|
||||
.init(userID: "@charlie:matrix.org", formattedTimestamp: "11:00")]
|
||||
// Given 3 messages from Bob where the middle message has a reaction.
|
||||
@@ -346,7 +361,8 @@ class TimelineViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testShowManageUserAsAdmin() async throws {
|
||||
@Test
|
||||
func showManageUserAsAdmin() async throws {
|
||||
let viewModel = TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "",
|
||||
members: [RoomMemberProxyMock.mockAdmin,
|
||||
RoomMemberProxyMock.mockAlice],
|
||||
@@ -375,14 +391,15 @@ class TimelineViewModelTests: XCTestCase {
|
||||
viewModel.context.send(viewAction: .tappedOnSenderDetails(sender: .init(with: RoomMemberProxyMock.mockAlice)))
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertEqual(viewModel.context.manageMemberViewModel?.id, RoomMemberProxyMock.mockAlice.userID)
|
||||
XCTAssertEqual(viewModel.context.manageMemberViewModel?.state.permissions.canBan, true)
|
||||
XCTAssertEqual(viewModel.context.manageMemberViewModel?.state.permissions.canKick, true)
|
||||
XCTAssertEqual(viewModel.context.manageMemberViewModel?.state.isKickDisabled, false)
|
||||
XCTAssertEqual(viewModel.context.manageMemberViewModel?.state.isBanUnbanDisabled, false)
|
||||
#expect(viewModel.context.manageMemberViewModel?.id == RoomMemberProxyMock.mockAlice.userID)
|
||||
#expect(viewModel.context.manageMemberViewModel?.state.permissions.canBan == true)
|
||||
#expect(viewModel.context.manageMemberViewModel?.state.permissions.canKick == true)
|
||||
#expect(viewModel.context.manageMemberViewModel?.state.isKickDisabled == false)
|
||||
#expect(viewModel.context.manageMemberViewModel?.state.isBanUnbanDisabled == false)
|
||||
}
|
||||
|
||||
func testShowDetailsForAnAdmin() async throws {
|
||||
@Test
|
||||
func showDetailsForAnAdmin() async throws {
|
||||
let viewModel = TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "",
|
||||
members: [RoomMemberProxyMock.mockAdmin,
|
||||
RoomMemberProxyMock.mockAlice],
|
||||
@@ -411,14 +428,15 @@ class TimelineViewModelTests: XCTestCase {
|
||||
viewModel.context.send(viewAction: .tappedOnSenderDetails(sender: .init(with: RoomMemberProxyMock.mockAdmin)))
|
||||
try await deferredState.fulfill()
|
||||
|
||||
XCTAssertEqual(viewModel.context.manageMemberViewModel?.state.permissions.canBan, false)
|
||||
XCTAssertEqual(viewModel.context.manageMemberViewModel?.state.permissions.canKick, false)
|
||||
XCTAssertEqual(viewModel.context.manageMemberViewModel?.state.isKickDisabled, true)
|
||||
XCTAssertEqual(viewModel.context.manageMemberViewModel?.state.isBanUnbanDisabled, true)
|
||||
XCTAssertEqual(viewModel.context.manageMemberViewModel?.id, RoomMemberProxyMock.mockAdmin.userID)
|
||||
#expect(viewModel.context.manageMemberViewModel?.state.permissions.canBan == false)
|
||||
#expect(viewModel.context.manageMemberViewModel?.state.permissions.canKick == false)
|
||||
#expect(viewModel.context.manageMemberViewModel?.state.isKickDisabled == true)
|
||||
#expect(viewModel.context.manageMemberViewModel?.state.isBanUnbanDisabled == true)
|
||||
#expect(viewModel.context.manageMemberViewModel?.id == RoomMemberProxyMock.mockAdmin.userID)
|
||||
}
|
||||
|
||||
func testShowDetailsForABannedUser() async throws {
|
||||
@Test
|
||||
func showDetailsForABannedUser() async throws {
|
||||
let viewModel = TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "",
|
||||
members: [RoomMemberProxyMock.mockAdmin,
|
||||
RoomMemberProxyMock.mockBanned[0]],
|
||||
@@ -447,17 +465,18 @@ class TimelineViewModelTests: XCTestCase {
|
||||
viewModel.context.send(viewAction: .tappedOnSenderDetails(sender: .init(with: RoomMemberProxyMock.mockBanned[0])))
|
||||
try await deferredState.fulfill()
|
||||
|
||||
XCTAssertEqual(viewModel.context.manageMemberViewModel?.state.permissions.canBan, true)
|
||||
XCTAssertEqual(viewModel.context.manageMemberViewModel?.state.permissions.canKick, true)
|
||||
XCTAssertEqual(viewModel.context.manageMemberViewModel?.state.isKickDisabled, true)
|
||||
XCTAssertEqual(viewModel.context.manageMemberViewModel?.state.isBanUnbanDisabled, false)
|
||||
XCTAssertEqual(viewModel.context.manageMemberViewModel?.state.isMemberBanned, true)
|
||||
XCTAssertEqual(viewModel.context.manageMemberViewModel?.id, RoomMemberProxyMock.mockBanned[0].userID)
|
||||
#expect(viewModel.context.manageMemberViewModel?.state.permissions.canBan == true)
|
||||
#expect(viewModel.context.manageMemberViewModel?.state.permissions.canKick == true)
|
||||
#expect(viewModel.context.manageMemberViewModel?.state.isKickDisabled == true)
|
||||
#expect(viewModel.context.manageMemberViewModel?.state.isBanUnbanDisabled == false)
|
||||
#expect(viewModel.context.manageMemberViewModel?.state.isMemberBanned == true)
|
||||
#expect(viewModel.context.manageMemberViewModel?.id == RoomMemberProxyMock.mockBanned[0].userID)
|
||||
}
|
||||
|
||||
// MARK: - Pins
|
||||
|
||||
func testPinnedEvents() async throws {
|
||||
@Test
|
||||
func pinnedEvents() async throws {
|
||||
var configuration = JoinedRoomProxyMockConfiguration(name: "",
|
||||
pinnedEventIDs: .init(["test1"]))
|
||||
let roomProxyMock = JoinedRoomProxyMock(configuration)
|
||||
@@ -475,7 +494,7 @@ class TimelineViewModelTests: XCTestCase {
|
||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
||||
linkMetadataProvider: LinkMetadataProvider(),
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
||||
XCTAssertEqual(configuration.pinnedEventIDs, viewModel.context.viewState.pinnedEventIDs)
|
||||
#expect(configuration.pinnedEventIDs == viewModel.context.viewState.pinnedEventIDs)
|
||||
|
||||
configuration.pinnedEventIDs = ["test1", "test2"]
|
||||
let deferred = deferFulfillment(viewModel.context.$viewState) { value in
|
||||
@@ -485,7 +504,8 @@ class TimelineViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testCanUserPinEvents() async throws {
|
||||
@Test
|
||||
func canUserPinEvents() async throws {
|
||||
let configuration = JoinedRoomProxyMockConfiguration(name: "",
|
||||
powerLevelsConfiguration: .init(canUserPin: true))
|
||||
let roomProxyMock = JoinedRoomProxyMock(configuration)
|
||||
@@ -526,32 +546,34 @@ class TimelineViewModelTests: XCTestCase {
|
||||
|
||||
// MARK: - Tap Actions
|
||||
|
||||
func testTapSendInfoEncryptionAuthentictyDisplaysAlert() {
|
||||
@Test
|
||||
func tapSendInfoEncryptionAuthentictyDisplaysAlert() {
|
||||
// Given a room with an event whose authenticity could not be verified
|
||||
let items = [TextRoomTimelineItem(eventID: "t1", encryptionAuthenticity: .verificationViolation(color: .red))]
|
||||
let timelineController = MockTimelineController()
|
||||
timelineController.timelineItems = items
|
||||
let viewModel = makeViewModel(timelineController: timelineController)
|
||||
|
||||
XCTAssertNil(viewModel.state.bindings.alertInfo)
|
||||
#expect(viewModel.state.bindings.alertInfo == nil)
|
||||
|
||||
viewModel.process(viewAction: .itemSendInfoTapped(itemID: items[0].id))
|
||||
|
||||
XCTAssertEqual(viewModel.state.bindings.alertInfo?.title, "Encrypted by a previously-verified user.")
|
||||
#expect(viewModel.state.bindings.alertInfo?.title == "Encrypted by a previously-verified user.")
|
||||
}
|
||||
|
||||
func testTapSendInfoEncryptionForwarderDisplaysAlert() {
|
||||
@Test
|
||||
func tapSendInfoEncryptionForwarderDisplaysAlert() {
|
||||
// Given a room with an event whose key was forwarded
|
||||
let items = [TextRoomTimelineItem(eventID: "t1", keyForwarder: .test)]
|
||||
let timelineController = MockTimelineController()
|
||||
timelineController.timelineItems = items
|
||||
let viewModel = makeViewModel(timelineController: timelineController)
|
||||
|
||||
XCTAssertNil(viewModel.state.bindings.alertInfo)
|
||||
#expect(viewModel.state.bindings.alertInfo == nil)
|
||||
|
||||
viewModel.process(viewAction: .itemSendInfoTapped(itemID: items[0].id))
|
||||
|
||||
XCTAssertEqual(viewModel.state.bindings.alertInfo?.title, "alice (@alice:matrix.org) shared this message since you were not in the room when it was sent.")
|
||||
#expect(viewModel.state.bindings.alertInfo?.title == "alice (@alice:matrix.org) shared this message since you were not in the room when it was sent.")
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
@@ -9,10 +9,11 @@
|
||||
import Combine
|
||||
@testable import ElementX
|
||||
import Foundation
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@Suite
|
||||
@MainActor
|
||||
class VoiceMessageRecorderTests: XCTestCase {
|
||||
struct VoiceMessageRecorderTests {
|
||||
private var voiceMessageRecorder: VoiceMessageRecorder!
|
||||
|
||||
private var audioRecorder: AudioRecorderMock!
|
||||
@@ -33,7 +34,7 @@ class VoiceMessageRecorderTests: XCTestCase {
|
||||
|
||||
private let recordingURL = URL("/some/url")
|
||||
|
||||
override func setUp() async throws {
|
||||
init() async throws {
|
||||
audioRecorder = AudioRecorderMock()
|
||||
audioRecorder.underlyingCurrentTime = 0
|
||||
audioRecorder.averagePowerReturnValue = 0
|
||||
@@ -61,7 +62,7 @@ class VoiceMessageRecorderTests: XCTestCase {
|
||||
|
||||
let deferred = deferFulfillment(voiceMessageRecorder.actions) { action in
|
||||
switch action {
|
||||
case .didStopRecording(_, let url) where url == self.recordingURL:
|
||||
case .didStopRecording(_, let url) where url == recordingURL:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
@@ -71,141 +72,153 @@ class VoiceMessageRecorderTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testRecordingURL() {
|
||||
@Test
|
||||
func recorderRecordingURL() {
|
||||
audioRecorder.audioFileURL = recordingURL
|
||||
XCTAssertEqual(voiceMessageRecorder.recordingURL, recordingURL)
|
||||
#expect(voiceMessageRecorder.recordingURL == recordingURL)
|
||||
}
|
||||
|
||||
func testRecordingDuration() {
|
||||
@Test
|
||||
func recorderRecordingDuration() {
|
||||
audioRecorder.currentTime = 10.3
|
||||
XCTAssertEqual(voiceMessageRecorder.recordingDuration, 10.3)
|
||||
#expect(voiceMessageRecorder.recordingDuration == 10.3)
|
||||
}
|
||||
|
||||
func testStartRecording() async {
|
||||
@Test
|
||||
func startRecording() async {
|
||||
_ = await voiceMessageRecorder.startRecording()
|
||||
XCTAssert(audioRecorder.recordAudioFileURLCalled)
|
||||
#expect(audioRecorder.recordAudioFileURLCalled)
|
||||
}
|
||||
|
||||
func testStopRecording() async {
|
||||
@Test
|
||||
func stopRecording() async {
|
||||
_ = await voiceMessageRecorder.stopRecording()
|
||||
// Internal audio recorder must have been stopped
|
||||
XCTAssert(audioRecorder.stopRecordingCalled)
|
||||
#expect(audioRecorder.stopRecordingCalled)
|
||||
}
|
||||
|
||||
func testCancelRecording() async {
|
||||
@Test
|
||||
func cancelRecording() async {
|
||||
await voiceMessageRecorder.cancelRecording()
|
||||
// Internal audio recorder must have been stopped
|
||||
XCTAssert(audioRecorder.stopRecordingCalled)
|
||||
#expect(audioRecorder.stopRecordingCalled)
|
||||
// The recording audio file must have been deleted
|
||||
XCTAssert(audioRecorder.deleteRecordingCalled)
|
||||
#expect(audioRecorder.deleteRecordingCalled)
|
||||
}
|
||||
|
||||
func testDeleteRecording() async {
|
||||
@Test
|
||||
func deleteRecording() async {
|
||||
await voiceMessageRecorder.deleteRecording()
|
||||
// The recording audio file must have been deleted
|
||||
XCTAssert(audioRecorder.deleteRecordingCalled)
|
||||
#expect(audioRecorder.deleteRecordingCalled)
|
||||
}
|
||||
|
||||
func testStartPlaybackNoPreview() async {
|
||||
@Test
|
||||
func startPlaybackNoPreview() async {
|
||||
guard case .failure(.previewNotAvailable) = await voiceMessageRecorder.startPlayback() else {
|
||||
XCTFail("An error is expected")
|
||||
Issue.record("An error is expected")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func testStartPlayback() async throws {
|
||||
@Test
|
||||
func startPlayback() async throws {
|
||||
try await setRecordingComplete()
|
||||
|
||||
guard case .success = await voiceMessageRecorder.startPlayback() else {
|
||||
XCTFail("Playback should start")
|
||||
Issue.record("Playback should start")
|
||||
return
|
||||
}
|
||||
XCTAssertEqual(voiceMessageRecorder.previewAudioPlayerState?.isAttached, true)
|
||||
XCTAssert(audioPlayer.loadSourceURLPlaybackURLAutoplayCalled)
|
||||
XCTAssertEqual(audioPlayer.loadSourceURLPlaybackURLAutoplayReceivedArguments?.sourceURL, recordingURL)
|
||||
XCTAssertEqual(audioPlayer.loadSourceURLPlaybackURLAutoplayReceivedArguments?.playbackURL, recordingURL)
|
||||
XCTAssertEqual(audioPlayer.loadSourceURLPlaybackURLAutoplayReceivedArguments?.autoplay, true)
|
||||
XCTAssertFalse(audioPlayer.playCalled)
|
||||
#expect(voiceMessageRecorder.previewAudioPlayerState?.isAttached == true)
|
||||
#expect(audioPlayer.loadSourceURLPlaybackURLAutoplayCalled)
|
||||
#expect(audioPlayer.loadSourceURLPlaybackURLAutoplayReceivedArguments?.sourceURL == recordingURL)
|
||||
#expect(audioPlayer.loadSourceURLPlaybackURLAutoplayReceivedArguments?.playbackURL == recordingURL)
|
||||
#expect(audioPlayer.loadSourceURLPlaybackURLAutoplayReceivedArguments?.autoplay == true)
|
||||
#expect(!audioPlayer.playCalled)
|
||||
}
|
||||
|
||||
func testPausePlayback() async throws {
|
||||
@Test
|
||||
func pausePlayback() async throws {
|
||||
try await setRecordingComplete()
|
||||
|
||||
_ = await voiceMessageRecorder.startPlayback()
|
||||
XCTAssertEqual(voiceMessageRecorder.previewAudioPlayerState?.isAttached, true)
|
||||
#expect(voiceMessageRecorder.previewAudioPlayerState?.isAttached == true)
|
||||
|
||||
voiceMessageRecorder.pausePlayback()
|
||||
XCTAssert(audioPlayer.pauseCalled)
|
||||
#expect(audioPlayer.pauseCalled)
|
||||
}
|
||||
|
||||
func testResumePlayback() async throws {
|
||||
@Test
|
||||
func resumePlayback() async throws {
|
||||
try await setRecordingComplete()
|
||||
audioPlayer.playbackURL = recordingURL
|
||||
|
||||
guard case .success = await voiceMessageRecorder.startPlayback() else {
|
||||
XCTFail("Playback should start")
|
||||
Issue.record("Playback should start")
|
||||
return
|
||||
}
|
||||
XCTAssertEqual(voiceMessageRecorder.previewAudioPlayerState?.isAttached, true)
|
||||
#expect(voiceMessageRecorder.previewAudioPlayerState?.isAttached == true)
|
||||
// The media must not have been reloaded
|
||||
XCTAssertFalse(audioPlayer.loadSourceURLPlaybackURLAutoplayCalled)
|
||||
XCTAssertTrue(audioPlayer.playCalled)
|
||||
#expect(!audioPlayer.loadSourceURLPlaybackURLAutoplayCalled)
|
||||
#expect(audioPlayer.playCalled)
|
||||
}
|
||||
|
||||
func testStopPlayback() async throws {
|
||||
@Test
|
||||
func stopPlayback() async throws {
|
||||
try await setRecordingComplete()
|
||||
|
||||
_ = await voiceMessageRecorder.startPlayback()
|
||||
XCTAssertEqual(voiceMessageRecorder.previewAudioPlayerState?.isAttached, true)
|
||||
#expect(voiceMessageRecorder.previewAudioPlayerState?.isAttached == true)
|
||||
|
||||
await voiceMessageRecorder.stopPlayback()
|
||||
XCTAssertEqual(voiceMessageRecorder.previewAudioPlayerState?.isAttached, false)
|
||||
XCTAssert(audioPlayer.stopCalled)
|
||||
#expect(voiceMessageRecorder.previewAudioPlayerState?.isAttached == false)
|
||||
#expect(audioPlayer.stopCalled)
|
||||
}
|
||||
|
||||
func testSeekPlayback() async throws {
|
||||
@Test
|
||||
func seekPlayback() async throws {
|
||||
try await setRecordingComplete()
|
||||
|
||||
_ = await voiceMessageRecorder.startPlayback()
|
||||
XCTAssertEqual(voiceMessageRecorder.previewAudioPlayerState?.isAttached, true)
|
||||
#expect(voiceMessageRecorder.previewAudioPlayerState?.isAttached == true)
|
||||
|
||||
await voiceMessageRecorder.seekPlayback(to: 0.4)
|
||||
XCTAssertEqual(audioPlayer.seekToReceivedProgress, 0.4)
|
||||
#expect(audioPlayer.seekToReceivedProgress == 0.4)
|
||||
}
|
||||
|
||||
func testBuildRecordedWaveform() async {
|
||||
@Test
|
||||
func buildRecordedWaveform() async throws {
|
||||
// If there is no recording file, an error is expected
|
||||
audioRecorder.audioFileURL = nil
|
||||
guard case .failure(.missingRecordingFile) = await voiceMessageRecorder.buildRecordingWaveform() else {
|
||||
XCTFail("An error is expected")
|
||||
Issue.record("An error is expected")
|
||||
return
|
||||
}
|
||||
|
||||
guard let audioFileURL = Bundle(for: Self.self).url(forResource: "test_audio", withExtension: "mp3") else {
|
||||
XCTFail("Test audio file is missing")
|
||||
return
|
||||
}
|
||||
let audioFileURL = try #require(Bundle(for: UnitTestsAppCoordinator.self).url(forResource: "test_audio", withExtension: "mp3"), "Test audio file is missing")
|
||||
audioRecorder.audioFileURL = audioFileURL
|
||||
guard case .success(let data) = await voiceMessageRecorder.buildRecordingWaveform() else {
|
||||
XCTFail("A waveform is expected")
|
||||
Issue.record("A waveform is expected")
|
||||
return
|
||||
}
|
||||
XCTAssert(!data.isEmpty)
|
||||
#expect(!data.isEmpty)
|
||||
}
|
||||
|
||||
func testSendVoiceMessage_NoRecordingFile() async {
|
||||
@Test
|
||||
func sendVoiceMessage_NoRecordingFile() async {
|
||||
let timelineController = MockTimelineController()
|
||||
|
||||
// If there is no recording file, an error is expected
|
||||
audioRecorder.audioFileURL = nil
|
||||
guard case .failure(.missingRecordingFile) = await voiceMessageRecorder.sendVoiceMessage(timelineController: timelineController,
|
||||
audioConverter: audioConverter) else {
|
||||
XCTFail("An error is expected")
|
||||
Issue.record("An error is expected")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func testSendVoiceMessage_ConversionError() async {
|
||||
@Test
|
||||
func sendVoiceMessage_ConversionError() async {
|
||||
audioRecorder.audioFileURL = recordingURL
|
||||
// If the converter returns an error
|
||||
audioConverter.convertToOpusOggSourceURLDestinationURLThrowableError = AudioConverterError.conversionFailed(nil)
|
||||
@@ -213,16 +226,14 @@ class VoiceMessageRecorderTests: XCTestCase {
|
||||
let timelineController = MockTimelineController()
|
||||
guard case .failure(.failedSendingVoiceMessage) = await voiceMessageRecorder.sendVoiceMessage(timelineController: timelineController,
|
||||
audioConverter: audioConverter) else {
|
||||
XCTFail("An error is expected")
|
||||
Issue.record("An error is expected")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func testSendVoiceMessage_InvalidFile() async {
|
||||
guard let audioFileURL = Bundle(for: Self.self).url(forResource: "test_voice_message", withExtension: "m4a") else {
|
||||
XCTFail("Test audio file is missing")
|
||||
return
|
||||
}
|
||||
@Test
|
||||
func sendVoiceMessage_InvalidFile() async throws {
|
||||
let audioFileURL = try #require(Bundle(for: UnitTestsAppCoordinator.self).url(forResource: "test_voice_message", withExtension: "m4a"), "Test audio file is missing")
|
||||
audioRecorder.audioFileURL = audioFileURL
|
||||
audioConverter.convertToOpusOggSourceURLDestinationURLClosure = { _, destination in
|
||||
try? FileManager.default.removeItem(at: destination)
|
||||
@@ -233,16 +244,14 @@ class VoiceMessageRecorderTests: XCTestCase {
|
||||
timelineProxy.sendVoiceMessageUrlAudioInfoWaveformRequestHandleReturnValue = .failure(.sdkError(SDKError.generic))
|
||||
guard case .failure(.failedSendingVoiceMessage) = await voiceMessageRecorder.sendVoiceMessage(timelineController: timelineController,
|
||||
audioConverter: audioConverter) else {
|
||||
XCTFail("An error is expected")
|
||||
Issue.record("An error is expected")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func testSendVoiceMessage_WaveformAnlyseFailed() async {
|
||||
guard let imageFileURL = Bundle(for: Self.self).url(forResource: "test_image", withExtension: "png") else {
|
||||
XCTFail("Test audio file is missing")
|
||||
return
|
||||
}
|
||||
@Test
|
||||
func sendVoiceMessage_WaveformAnlyseFailed() async throws {
|
||||
let imageFileURL = try #require(Bundle(for: UnitTestsAppCoordinator.self).url(forResource: "test_image", withExtension: "png"), "Test image file is missing")
|
||||
audioRecorder.audioFileURL = imageFileURL
|
||||
audioConverter.convertToOpusOggSourceURLDestinationURLClosure = { _, destination in
|
||||
try? FileManager.default.removeItem(at: destination)
|
||||
@@ -254,16 +263,14 @@ class VoiceMessageRecorderTests: XCTestCase {
|
||||
timelineProxy.sendVoiceMessageUrlAudioInfoWaveformRequestHandleReturnValue = .failure(.sdkError(SDKError.generic))
|
||||
guard case .failure(.failedSendingVoiceMessage) = await voiceMessageRecorder.sendVoiceMessage(timelineController: timelineController,
|
||||
audioConverter: audioConverter) else {
|
||||
XCTFail("An error is expected")
|
||||
Issue.record("An error is expected")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func testSendVoiceMessage_SendError() async {
|
||||
guard let audioFileURL = Bundle(for: Self.self).url(forResource: "test_voice_message", withExtension: "m4a") else {
|
||||
XCTFail("Test audio file is missing")
|
||||
return
|
||||
}
|
||||
@Test
|
||||
func sendVoiceMessage_SendError() async throws {
|
||||
let audioFileURL = try #require(Bundle(for: UnitTestsAppCoordinator.self).url(forResource: "test_voice_message", withExtension: "m4a"), "Test audio file is missing")
|
||||
audioRecorder.audioFileURL = audioFileURL
|
||||
audioConverter.convertToOpusOggSourceURLDestinationURLClosure = { source, destination in
|
||||
try? FileManager.default.removeItem(at: destination)
|
||||
@@ -277,16 +284,14 @@ class VoiceMessageRecorderTests: XCTestCase {
|
||||
timelineProxy.sendVoiceMessageUrlAudioInfoWaveformRequestHandleReturnValue = .failure(.sdkError(SDKError.generic))
|
||||
guard case .failure(.failedSendingVoiceMessage) = await voiceMessageRecorder.sendVoiceMessage(timelineController: timelineController,
|
||||
audioConverter: audioConverter) else {
|
||||
XCTFail("An error is expected")
|
||||
Issue.record("An error is expected")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func testSendVoiceMessage() async {
|
||||
guard let imageFileURL = Bundle(for: Self.self).url(forResource: "test_voice_message", withExtension: "m4a") else {
|
||||
XCTFail("Test audio file is missing")
|
||||
return
|
||||
}
|
||||
@Test
|
||||
func sendVoiceMessage() async throws {
|
||||
let imageFileURL = try #require(Bundle(for: UnitTestsAppCoordinator.self).url(forResource: "test_voice_message", withExtension: "m4a"), "Test audio file is missing")
|
||||
|
||||
let timelineProxy = TimelineProxyMock()
|
||||
let timelineController = MockTimelineController(timelineProxy: timelineProxy)
|
||||
@@ -305,38 +310,39 @@ class VoiceMessageRecorderTests: XCTestCase {
|
||||
try internalConverter.convertToOpusOgg(sourceURL: source, destinationURL: destination)
|
||||
convertedFileSize = try? UInt64(FileManager.default.sizeForItem(at: destination))
|
||||
// the source URL must be the recorded file
|
||||
XCTAssertEqual(source, imageFileURL)
|
||||
#expect(source == imageFileURL)
|
||||
// check the converted file extension
|
||||
XCTAssertEqual(destination.pathExtension, "ogg")
|
||||
#expect(destination.pathExtension == "ogg")
|
||||
}
|
||||
|
||||
timelineProxy.sendVoiceMessageUrlAudioInfoWaveformRequestHandleClosure = { url, audioInfo, waveform, _ in
|
||||
XCTAssertEqual(url, convertedFileURL)
|
||||
XCTAssertEqual(audioInfo.duration, self.audioRecorder.currentTime)
|
||||
XCTAssertEqual(audioInfo.size, convertedFileSize)
|
||||
XCTAssertEqual(audioInfo.mimetype, "audio/ogg")
|
||||
XCTAssertFalse(waveform.isEmpty)
|
||||
#expect(url == convertedFileURL)
|
||||
#expect(audioInfo.duration == audioRecorder.currentTime)
|
||||
#expect(audioInfo.size == convertedFileSize)
|
||||
#expect(audioInfo.mimetype == "audio/ogg")
|
||||
#expect(!waveform.isEmpty)
|
||||
|
||||
return .success(())
|
||||
}
|
||||
|
||||
guard case .success = await voiceMessageRecorder.sendVoiceMessage(timelineController: timelineController, audioConverter: audioConverter) else {
|
||||
XCTFail("A success is expected")
|
||||
Issue.record("A success is expected")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssert(audioConverter.convertToOpusOggSourceURLDestinationURLCalled)
|
||||
XCTAssert(timelineProxy.sendVoiceMessageUrlAudioInfoWaveformRequestHandleCalled)
|
||||
#expect(audioConverter.convertToOpusOggSourceURLDestinationURLCalled)
|
||||
#expect(timelineProxy.sendVoiceMessageUrlAudioInfoWaveformRequestHandleCalled)
|
||||
|
||||
// the converted file must have been deleted
|
||||
if let convertedFileURL {
|
||||
XCTAssertFalse(FileManager.default.fileExists(atPath: convertedFileURL.path()))
|
||||
#expect(!FileManager.default.fileExists(atPath: convertedFileURL.path()))
|
||||
} else {
|
||||
XCTFail("converted file URL is missing")
|
||||
Issue.record("converted file URL is missing")
|
||||
}
|
||||
}
|
||||
|
||||
func testAudioRecorderActionHandling_didStartRecording() async throws {
|
||||
@Test
|
||||
func audioRecorderActionHandling_didStartRecording() async throws {
|
||||
let deferred = deferFulfillment(voiceMessageRecorder.actions) { action in
|
||||
switch action {
|
||||
case .didStartRecording:
|
||||
@@ -349,13 +355,14 @@ class VoiceMessageRecorderTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testAudioRecorderActionHandling_didStopRecording() async throws {
|
||||
@Test
|
||||
func audioRecorderActionHandling_didStopRecording() async throws {
|
||||
audioRecorder.audioFileURL = recordingURL
|
||||
audioRecorder.currentTime = 5
|
||||
|
||||
let deferred = deferFulfillment(voiceMessageRecorder.actions) { action in
|
||||
switch action {
|
||||
case .didStopRecording(_, let url) where url == self.recordingURL:
|
||||
case .didStopRecording(_, let url) where url == recordingURL:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
@@ -365,7 +372,8 @@ class VoiceMessageRecorderTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testAudioRecorderActionHandling_didFailed() async throws {
|
||||
@Test
|
||||
func audioRecorderActionHandling_didFailed() async throws {
|
||||
audioRecorder.audioFileURL = recordingURL
|
||||
|
||||
let deferred = deferFulfillment(voiceMessageRecorder.actions) { action in
|
||||
|
||||
Reference in New Issue
Block a user