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:
Copilot
2026-02-24 12:20:01 +00:00
committed by GitHub
parent 62f193eda1
commit e58c5801fc
38 changed files with 3460 additions and 3304 deletions
@@ -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
}