@@ -13,6 +13,8 @@ import XCTest
13
13
class MediaUploadPreviewScreenViewModelTests : XCTestCase {
14
14
var timelineProxy : TimelineProxyMock !
15
15
var clientProxy : ClientProxyMock !
16
+ var userIndicatorController : UserIndicatorControllerMock !
17
+
16
18
var viewModel : MediaUploadPreviewScreenViewModel !
17
19
var context : MediaUploadPreviewScreenViewModel . Context { viewModel. context }
18
20
@@ -33,77 +35,97 @@ class MediaUploadPreviewScreenViewModelTests: XCTestCase {
33
35
}
34
36
35
37
func testImageUploadWithoutCaption( ) async throws {
36
- setUpViewModel ( url : imageURL, expectedCaption: nil )
38
+ setUpViewModel ( urls : [ imageURL] , expectedCaption: nil )
37
39
context. caption = . init( " " )
38
40
try await send ( )
39
41
}
40
42
41
43
func testImageUploadWithBlankCaption( ) async throws {
42
- setUpViewModel ( url : imageURL, expectedCaption: nil )
44
+ setUpViewModel ( urls : [ imageURL] , expectedCaption: nil )
43
45
context. caption = . init( " " )
44
46
try await send ( )
45
47
}
46
48
47
49
func testImageUploadWithCaption( ) async throws {
48
50
let caption = " This is a really great image! "
49
- setUpViewModel ( url : imageURL, expectedCaption: caption)
51
+ setUpViewModel ( urls : [ imageURL] , expectedCaption: caption)
50
52
context. caption = . init( string: caption)
51
53
try await send ( )
52
54
}
53
55
54
56
func testVideoUploadWithoutCaption( ) async throws {
55
- setUpViewModel ( url : videoURL, expectedCaption: nil )
57
+ setUpViewModel ( urls : [ videoURL] , expectedCaption: nil )
56
58
context. caption = . init( " " )
57
59
try await send ( )
58
60
}
59
61
60
62
func testVideoUploadWithCaption( ) async throws {
61
63
let caption = " Check out this video! "
62
- setUpViewModel ( url : videoURL, expectedCaption: caption)
64
+ setUpViewModel ( urls : [ videoURL] , expectedCaption: caption)
63
65
context. caption = . init( string: caption)
64
66
try await send ( )
65
67
}
66
68
67
69
func testAudioUploadWithoutCaption( ) async throws {
68
- setUpViewModel ( url : audioURL, expectedCaption: nil )
70
+ setUpViewModel ( urls : [ audioURL] , expectedCaption: nil )
69
71
context. caption = . init( " " )
70
72
try await send ( )
71
73
}
72
74
73
75
func testAudioUploadWithCaption( ) async throws {
74
76
let caption = " Listen to this! "
75
- setUpViewModel ( url : audioURL, expectedCaption: caption)
77
+ setUpViewModel ( urls : [ audioURL] , expectedCaption: caption)
76
78
context. caption = . init( string: caption)
77
79
try await send ( )
78
80
}
79
81
80
82
func testFileUploadWithoutCaption( ) async throws {
81
- setUpViewModel ( url : fileURL, expectedCaption: nil )
83
+ setUpViewModel ( urls : [ fileURL] , expectedCaption: nil )
82
84
context. caption = . init( " " )
83
85
try await send ( )
84
86
}
85
87
86
88
func testFileUploadWithCaption( ) async throws {
87
89
let caption = " Please will you check my article. "
88
- setUpViewModel ( url : fileURL, expectedCaption: caption)
90
+ setUpViewModel ( urls : [ fileURL] , expectedCaption: caption)
89
91
context. caption = . init( string: caption)
90
92
try await send ( )
91
93
}
92
94
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
+
93
113
func testUploadWithUnknownMaxUploadSize( ) async throws {
94
114
// 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) ) )
96
116
XCTAssertFalse ( context. viewState. shouldDisableInteraction)
97
117
XCTAssertNil ( context. alertInfo)
98
118
99
119
// When attempting to send the media.
100
120
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 }
101
122
context. send ( viewAction: . send)
102
123
103
124
XCTAssertTrue ( context. viewState. shouldDisableInteraction, " The interaction should be disabled while sending. " )
104
125
105
126
// Then alert should be shown to tell the user it failed.
106
127
try await deferredAlert. fulfill ( )
128
+ try await deferredFailure. fulfill ( )
107
129
108
130
XCTAssertFalse ( context. viewState. shouldDisableInteraction)
109
131
XCTAssertEqual ( context. alertInfo? . id, . maxUploadSizeUnknown)
@@ -121,29 +143,90 @@ class MediaUploadPreviewScreenViewModelTests: XCTestCase {
121
143
122
144
func testUploadExceedingMaxUploadSize( ) async throws {
123
145
// 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 ) )
125
147
XCTAssertFalse ( context. viewState. shouldDisableInteraction)
126
148
XCTAssertNil ( context. alertInfo)
127
149
128
150
// When attempting to send an image that is larger the limit.
129
151
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 }
130
153
context. send ( viewAction: . send)
131
154
132
155
XCTAssertTrue ( context. viewState. shouldDisableInteraction, " The interaction should be disabled while sending. " )
133
156
134
157
// Then an alert should be shown to inform the user of the max upload size.
135
158
try await deferredAlert. fulfill ( )
159
+ try await deferredFailure. fulfill ( )
136
160
137
161
XCTAssertFalse ( context. viewState. shouldDisableInteraction)
138
162
XCTAssertEqual ( context. alertInfo? . id, . maxUploadSizeExceeded( limit: 100 ) )
139
163
}
140
164
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
+
141
223
// MARK: - Helpers
142
224
143
225
private var audioURL : URL { assertResourceURL ( filename: " test_audio.mp3 " ) }
144
226
private var fileURL : URL { assertResourceURL ( filename: " test_pdf.pdf " ) }
145
227
private var imageURL : URL { assertResourceURL ( filename: " test_animated_image.gif " ) }
146
228
private var videoURL : URL { assertResourceURL ( filename: " landscape_test_video.mov " ) }
229
+ private var badImageURL = URL ( filePath: " /home/user/this_file_doesn't_exist.jpg " )
147
230
148
231
private func assertResourceURL( filename: String ) -> URL {
149
232
guard let url = Bundle ( for: Self . self) . url ( forResource: filename, withExtension: nil ) else {
@@ -153,7 +236,10 @@ class MediaUploadPreviewScreenViewModelTests: XCTestCase {
153
236
return url
154
237
}
155
238
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 ) {
157
243
timelineProxy = TimelineProxyMock ( . init( ) )
158
244
timelineProxy. sendAudioUrlAudioInfoCaptionRequestHandleClosure = { [ weak self] _, _, caption, _ in
159
245
self ? . verifyCaption ( caption, expectedCaption: expectedCaption) ?? . failure( . sdkError( TestError . unknown) )
@@ -162,7 +248,8 @@ class MediaUploadPreviewScreenViewModelTests: XCTestCase {
162
248
self ? . verifyCaption ( caption, expectedCaption: expectedCaption) ?? . failure( . sdkError( TestError . unknown) )
163
249
}
164
250
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) )
166
253
}
167
254
timelineProxy. sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleClosure = { [ weak self] _, _, _, caption, _ in
168
255
self ? . verifyCaption ( caption, expectedCaption: expectedCaption) ?? . failure( . sdkError( TestError . unknown) )
@@ -173,14 +260,16 @@ class MediaUploadPreviewScreenViewModelTests: XCTestCase {
173
260
clientProxy. underlyingMaxMediaUploadSize = maxUploadSizeResult
174
261
}
175
262
176
- viewModel = MediaUploadPreviewScreenViewModel ( mediaURLs: [ url] ,
263
+ userIndicatorController = UserIndicatorControllerMock ( )
264
+
265
+ viewModel = MediaUploadPreviewScreenViewModel ( mediaURLs: urls,
177
266
title: " Some File " ,
178
267
isRoomEncrypted: true ,
179
268
shouldShowCaptionWarning: true ,
180
269
mediaUploadingPreprocessor: MediaUploadingPreprocessor ( appSettings: ServiceLocator . shared. settings) ,
181
270
timelineController: MockTimelineController ( timelineProxy: timelineProxy) ,
182
271
clientProxy: clientProxy,
183
- userIndicatorController: UserIndicatorControllerMock ( ) )
272
+ userIndicatorController: userIndicatorController )
184
273
}
185
274
186
275
private func verifyCaption( _ caption: String ? , expectedCaption: String ? ) -> Result < Void , TimelineProxyError > {
0 commit comments