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
@@ -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()
}