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,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