Skip to content

Commit f1f6cac

Browse files
authored
Fix a bug where the image upload screen was unintentionally dismissed for some failures. (#4414)
1 parent 8000dbe commit f1f6cac

File tree

2 files changed

+108
-23
lines changed

2 files changed

+108
-23
lines changed

ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenViewModel.swift

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,31 +62,27 @@ class MediaUploadPreviewScreenViewModel: MediaUploadPreviewScreenViewModelType,
6262
Task {
6363
defer { stopLoading() }
6464

65-
var shouldDismissOnCompletion = true
6665
switch await processingTask.value {
6766
case .success(let mediaInfos):
6867
for mediaInfo in mediaInfos {
6968
switch await sendAttachment(mediaInfo: mediaInfo, caption: caption) {
7069
case .success:
7170
caption = nil // Set the caption only on the first uploaded file.
7271
case .failure(let error):
73-
MXLog.error("Failed processing media to upload with error: \(error)")
74-
showError(label: L10n.screenMediaUploadPreviewErrorFailedProcessing)
72+
MXLog.error("Failed sending media with error: \(error)")
73+
showError(label: L10n.screenMediaUploadPreviewErrorFailedSending)
7574
}
7675
}
76+
77+
actionsSubject.send(.dismiss)
7778
case .failure(.maxUploadSizeUnknown):
7879
showAlert(.maxUploadSizeUnknown)
79-
shouldDismissOnCompletion = false
8080
case .failure(.maxUploadSizeExceeded(let limit)):
8181
showAlert(.maxUploadSizeExceeded(limit: limit))
8282
case .failure(let error):
8383
MXLog.error("Failed processing media to upload with error: \(error)")
8484
showError(label: L10n.screenMediaUploadPreviewErrorFailedProcessing)
8585
}
86-
87-
if shouldDismissOnCompletion {
88-
actionsSubject.send(.dismiss)
89-
}
9086
}
9187
case .cancel:
9288
requestHandle?.cancel()

UnitTests/Sources/MediaUploadPreviewScreenViewModelTests.swift

Lines changed: 104 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import XCTest
1313
class MediaUploadPreviewScreenViewModelTests: XCTestCase {
1414
var timelineProxy: TimelineProxyMock!
1515
var clientProxy: ClientProxyMock!
16+
var userIndicatorController: UserIndicatorControllerMock!
17+
1618
var viewModel: MediaUploadPreviewScreenViewModel!
1719
var context: MediaUploadPreviewScreenViewModel.Context { viewModel.context }
1820

@@ -33,77 +35,97 @@ class MediaUploadPreviewScreenViewModelTests: XCTestCase {
3335
}
3436

3537
func testImageUploadWithoutCaption() async throws {
36-
setUpViewModel(url: imageURL, expectedCaption: nil)
38+
setUpViewModel(urls: [imageURL], expectedCaption: nil)
3739
context.caption = .init("")
3840
try await send()
3941
}
4042

4143
func testImageUploadWithBlankCaption() async throws {
42-
setUpViewModel(url: imageURL, expectedCaption: nil)
44+
setUpViewModel(urls: [imageURL], expectedCaption: nil)
4345
context.caption = .init(" ")
4446
try await send()
4547
}
4648

4749
func testImageUploadWithCaption() async throws {
4850
let caption = "This is a really great image!"
49-
setUpViewModel(url: imageURL, expectedCaption: caption)
51+
setUpViewModel(urls: [imageURL], expectedCaption: caption)
5052
context.caption = .init(string: caption)
5153
try await send()
5254
}
5355

5456
func testVideoUploadWithoutCaption() async throws {
55-
setUpViewModel(url: videoURL, expectedCaption: nil)
57+
setUpViewModel(urls: [videoURL], expectedCaption: nil)
5658
context.caption = .init("")
5759
try await send()
5860
}
5961

6062
func testVideoUploadWithCaption() async throws {
6163
let caption = "Check out this video!"
62-
setUpViewModel(url: videoURL, expectedCaption: caption)
64+
setUpViewModel(urls: [videoURL], expectedCaption: caption)
6365
context.caption = .init(string: caption)
6466
try await send()
6567
}
6668

6769
func testAudioUploadWithoutCaption() async throws {
68-
setUpViewModel(url: audioURL, expectedCaption: nil)
70+
setUpViewModel(urls: [audioURL], expectedCaption: nil)
6971
context.caption = .init("")
7072
try await send()
7173
}
7274

7375
func testAudioUploadWithCaption() async throws {
7476
let caption = "Listen to this!"
75-
setUpViewModel(url: audioURL, expectedCaption: caption)
77+
setUpViewModel(urls: [audioURL], expectedCaption: caption)
7678
context.caption = .init(string: caption)
7779
try await send()
7880
}
7981

8082
func testFileUploadWithoutCaption() async throws {
81-
setUpViewModel(url: fileURL, expectedCaption: nil)
83+
setUpViewModel(urls: [fileURL], expectedCaption: nil)
8284
context.caption = .init("")
8385
try await send()
8486
}
8587

8688
func testFileUploadWithCaption() async throws {
8789
let caption = "Please will you check my article."
88-
setUpViewModel(url: fileURL, expectedCaption: caption)
90+
setUpViewModel(urls: [fileURL], expectedCaption: caption)
8991
context.caption = .init(string: caption)
9092
try await send()
9193
}
9294

95+
func testProcessingFailure() async throws {
96+
// Given an upload screen for a non-existent file.
97+
setUpViewModel(urls: [badImageURL], expectedCaption: nil)
98+
XCTAssertFalse(context.viewState.shouldDisableInteraction)
99+
XCTAssertEqual(userIndicatorController.submitIndicatorDelayCallsCount, 0)
100+
101+
// When attempting to send the file
102+
let deferredFailure = deferFailure(viewModel.actions, timeout: 1, message: "The screen should remain visible.") { $0 == .dismiss }
103+
context.send(viewAction: .send)
104+
XCTAssertTrue(context.viewState.shouldDisableInteraction, "The interaction should be disabled while sending.")
105+
XCTAssertEqual(userIndicatorController.submitIndicatorDelayCallsCount, 1) // Loading indicator
106+
107+
// Then the failure should occur preventing the screen from being dismissed.
108+
try await deferredFailure.fulfill()
109+
XCTAssertFalse(context.viewState.shouldDisableInteraction)
110+
XCTAssertEqual(userIndicatorController.submitIndicatorDelayCallsCount, 2, "An error indicator should be shown.")
111+
}
112+
93113
func testUploadWithUnknownMaxUploadSize() async throws {
94114
// Given an upload screen that is unable to fetch the max upload size.
95-
setUpViewModel(url: imageURL, expectedCaption: nil, maxUploadSizeResult: .failure(.sdkError(ClientProxyMockError.generic)))
115+
setUpViewModel(urls: [imageURL], expectedCaption: nil, maxUploadSizeResult: .failure(.sdkError(ClientProxyMockError.generic)))
96116
XCTAssertFalse(context.viewState.shouldDisableInteraction)
97117
XCTAssertNil(context.alertInfo)
98118

99119
// When attempting to send the media.
100120
let deferredAlert = deferFulfillment(context.observe(\.viewState.bindings.alertInfo)) { $0 != nil }
121+
let deferredFailure = deferFailure(viewModel.actions, timeout: 1, message: "The screen should remain visible.") { $0 == .dismiss }
101122
context.send(viewAction: .send)
102123

103124
XCTAssertTrue(context.viewState.shouldDisableInteraction, "The interaction should be disabled while sending.")
104125

105126
// Then alert should be shown to tell the user it failed.
106127
try await deferredAlert.fulfill()
128+
try await deferredFailure.fulfill()
107129

108130
XCTAssertFalse(context.viewState.shouldDisableInteraction)
109131
XCTAssertEqual(context.alertInfo?.id, .maxUploadSizeUnknown)
@@ -121,29 +143,90 @@ class MediaUploadPreviewScreenViewModelTests: XCTestCase {
121143

122144
func testUploadExceedingMaxUploadSize() async throws {
123145
// Given an upload screen with a really small max upload size.
124-
setUpViewModel(url: imageURL, expectedCaption: nil, maxUploadSizeResult: .success(100))
146+
setUpViewModel(urls: [imageURL], expectedCaption: nil, maxUploadSizeResult: .success(100))
125147
XCTAssertFalse(context.viewState.shouldDisableInteraction)
126148
XCTAssertNil(context.alertInfo)
127149

128150
// When attempting to send an image that is larger the limit.
129151
let deferredAlert = deferFulfillment(context.observe(\.viewState.bindings.alertInfo)) { $0 != nil }
152+
let deferredFailure = deferFailure(viewModel.actions, timeout: 1, message: "The screen should remain visible.") { $0 == .dismiss }
130153
context.send(viewAction: .send)
131154

132155
XCTAssertTrue(context.viewState.shouldDisableInteraction, "The interaction should be disabled while sending.")
133156

134157
// Then an alert should be shown to inform the user of the max upload size.
135158
try await deferredAlert.fulfill()
159+
try await deferredFailure.fulfill()
136160

137161
XCTAssertFalse(context.viewState.shouldDisableInteraction)
138162
XCTAssertEqual(context.alertInfo?.id, .maxUploadSizeExceeded(limit: 100))
139163
}
140164

165+
func testMultipleFiles() async throws {
166+
// Given an upload screen with multiple media files.
167+
setUpViewModel(urls: [fileURL, imageURL, fileURL], expectedCaption: nil)
168+
XCTAssertFalse(context.viewState.shouldDisableInteraction)
169+
XCTAssertEqual(userIndicatorController.submitIndicatorDelayCallsCount, 0)
170+
171+
// When attempting to send the files.
172+
let deferredDismiss = deferFulfillment(viewModel.actions) { $0 == .dismiss }
173+
context.send(viewAction: .send)
174+
175+
XCTAssertTrue(context.viewState.shouldDisableInteraction, "The interaction should be disabled while sending.")
176+
XCTAssertEqual(userIndicatorController.submitIndicatorDelayCallsCount, 1) // Loading indicator
177+
178+
// Then the screen should be dismissed once all of the files have been sent.
179+
try await deferredDismiss.fulfill()
180+
XCTAssertEqual(timelineProxy.sendImageUrlThumbnailURLImageInfoCaptionRequestHandleCallsCount, 1)
181+
XCTAssertEqual(timelineProxy.sendFileUrlFileInfoCaptionRequestHandleCallsCount, 2)
182+
XCTAssertEqual(userIndicatorController.submitIndicatorDelayCallsCount, 1, "Only a loading indicator should be shown.")
183+
}
184+
185+
func testMultipleFilesWithProcessingFailure() async throws {
186+
// Given an upload screen for a non-existent file.
187+
setUpViewModel(urls: [imageURL, fileURL, badImageURL], expectedCaption: nil)
188+
XCTAssertFalse(context.viewState.shouldDisableInteraction)
189+
XCTAssertEqual(userIndicatorController.submitIndicatorDelayCallsCount, 0)
190+
191+
// When attempting to send the file
192+
let deferredFailure = deferFailure(viewModel.actions, timeout: 1, message: "The screen should remain visible.") { $0 == .dismiss }
193+
context.send(viewAction: .send)
194+
XCTAssertTrue(context.viewState.shouldDisableInteraction, "The interaction should be disabled while sending.")
195+
XCTAssertEqual(userIndicatorController.submitIndicatorDelayCallsCount, 1) // Loading indicator
196+
197+
// Then the failure should occur preventing the screen from being dismissed.
198+
try await deferredFailure.fulfill()
199+
XCTAssertFalse(context.viewState.shouldDisableInteraction)
200+
XCTAssertEqual(userIndicatorController.submitIndicatorDelayCallsCount, 2, "An error indicator should be shown.")
201+
}
202+
203+
func testMultipleFilesWithSendFailure() async throws {
204+
// Given an upload screen with multiple media files where one of the files will fail to send.
205+
setUpViewModel(urls: [fileURL, imageURL, imageURL, fileURL], expectedCaption: nil, simulateImageSendFailures: true)
206+
XCTAssertFalse(context.viewState.shouldDisableInteraction)
207+
XCTAssertEqual(userIndicatorController.submitIndicatorDelayCallsCount, 0)
208+
209+
// When attempting to send the files.
210+
let deferredDismiss = deferFulfillment(viewModel.actions) { $0 == .dismiss }
211+
context.send(viewAction: .send)
212+
213+
XCTAssertTrue(context.viewState.shouldDisableInteraction, "The interaction should be disabled while sending.")
214+
XCTAssertEqual(userIndicatorController.submitIndicatorDelayCallsCount, 1) // Loading indicator
215+
216+
// Then the screen should be dismissed so the user can see which files made it into the timeline.
217+
try await deferredDismiss.fulfill()
218+
XCTAssertEqual(timelineProxy.sendImageUrlThumbnailURLImageInfoCaptionRequestHandleCallsCount, 2)
219+
XCTAssertEqual(timelineProxy.sendFileUrlFileInfoCaptionRequestHandleCallsCount, 2)
220+
XCTAssertEqual(userIndicatorController.submitIndicatorDelayCallsCount, 3, "Error indicators for each failure should be shown.")
221+
}
222+
141223
// MARK: - Helpers
142224

143225
private var audioURL: URL { assertResourceURL(filename: "test_audio.mp3") }
144226
private var fileURL: URL { assertResourceURL(filename: "test_pdf.pdf") }
145227
private var imageURL: URL { assertResourceURL(filename: "test_animated_image.gif") }
146228
private var videoURL: URL { assertResourceURL(filename: "landscape_test_video.mov") }
229+
private var badImageURL = URL(filePath: "/home/user/this_file_doesn't_exist.jpg")
147230

148231
private func assertResourceURL(filename: String) -> URL {
149232
guard let url = Bundle(for: Self.self).url(forResource: filename, withExtension: nil) else {
@@ -153,7 +236,10 @@ class MediaUploadPreviewScreenViewModelTests: XCTestCase {
153236
return url
154237
}
155238

156-
private func setUpViewModel(url: URL, expectedCaption: String?, maxUploadSizeResult: Result<UInt, ClientProxyError>? = nil) {
239+
private func setUpViewModel(urls: [URL],
240+
expectedCaption: String?,
241+
maxUploadSizeResult: Result<UInt, ClientProxyError>? = nil,
242+
simulateImageSendFailures: Bool = false) {
157243
timelineProxy = TimelineProxyMock(.init())
158244
timelineProxy.sendAudioUrlAudioInfoCaptionRequestHandleClosure = { [weak self] _, _, caption, _ in
159245
self?.verifyCaption(caption, expectedCaption: expectedCaption) ?? .failure(.sdkError(TestError.unknown))
@@ -162,7 +248,8 @@ class MediaUploadPreviewScreenViewModelTests: XCTestCase {
162248
self?.verifyCaption(caption, expectedCaption: expectedCaption) ?? .failure(.sdkError(TestError.unknown))
163249
}
164250
timelineProxy.sendImageUrlThumbnailURLImageInfoCaptionRequestHandleClosure = { [weak self] _, _, _, caption, _ in
165-
self?.verifyCaption(caption, expectedCaption: expectedCaption) ?? .failure(.sdkError(TestError.unknown))
251+
guard !simulateImageSendFailures else { return .failure(.sdkError(TestError.unknown)) }
252+
return self?.verifyCaption(caption, expectedCaption: expectedCaption) ?? .failure(.sdkError(TestError.unknown))
166253
}
167254
timelineProxy.sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleClosure = { [weak self] _, _, _, caption, _ in
168255
self?.verifyCaption(caption, expectedCaption: expectedCaption) ?? .failure(.sdkError(TestError.unknown))
@@ -173,14 +260,16 @@ class MediaUploadPreviewScreenViewModelTests: XCTestCase {
173260
clientProxy.underlyingMaxMediaUploadSize = maxUploadSizeResult
174261
}
175262

176-
viewModel = MediaUploadPreviewScreenViewModel(mediaURLs: [url],
263+
userIndicatorController = UserIndicatorControllerMock()
264+
265+
viewModel = MediaUploadPreviewScreenViewModel(mediaURLs: urls,
177266
title: "Some File",
178267
isRoomEncrypted: true,
179268
shouldShowCaptionWarning: true,
180269
mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: ServiceLocator.shared.settings),
181270
timelineController: MockTimelineController(timelineProxy: timelineProxy),
182271
clientProxy: clientProxy,
183-
userIndicatorController: UserIndicatorControllerMock())
272+
userIndicatorController: userIndicatorController)
184273
}
185274

186275
private func verifyCaption(_ caption: String?, expectedCaption: String?) -> Result<Void, TimelineProxyError> {

0 commit comments

Comments
 (0)