1
1
import 'dart:convert' ;
2
+ import 'dart:io' ;
2
3
import 'dart:typed_data' ;
3
4
4
5
import 'package:checks/checks.dart' ;
5
6
import 'package:collection/collection.dart' ;
6
7
import 'package:fake_async/fake_async.dart' ;
7
8
import 'package:firebase_messaging/firebase_messaging.dart' ;
9
+ import 'package:flutter/foundation.dart' ;
8
10
import 'package:flutter/material.dart' ;
9
11
import 'package:flutter_local_notifications/flutter_local_notifications.dart' hide Message, Person;
10
12
import 'package:flutter_test/flutter_test.dart' ;
13
+ import 'package:http/http.dart' as http;
14
+ import 'package:http/testing.dart' as http_testing;
11
15
import 'package:zulip/api/model/model.dart' ;
12
16
import 'package:zulip/api/notifications.dart' ;
13
17
import 'package:zulip/host/android_notifications.dart' ;
@@ -25,6 +29,7 @@ import 'package:zulip/widgets/theme.dart';
25
29
import '../fake_async.dart' ;
26
30
import '../model/binding.dart' ;
27
31
import '../example_data.dart' as eg;
32
+ import '../test_images.dart' ;
28
33
import '../test_navigation.dart' ;
29
34
import '../widgets/message_list_checks.dart' ;
30
35
import '../widgets/page_checks.dart' ;
@@ -79,6 +84,17 @@ void main() {
79
84
TestZulipBinding .ensureInitialized ();
80
85
final zulipLocalizations = GlobalLocalizations .zulipLocalizations;
81
86
87
+ final mockHttpClient = http_testing.MockClient (
88
+ (request) async => http.Response .bytes (kSolidBlueAvatar, HttpStatus .ok));
89
+
90
+ T Function () runWithHttpClient <T >(
91
+ Future <T > Function (FakeAsync async ) callback, {
92
+ http.Client Function ()? httpClientFactory,
93
+ }) =>
94
+ () => http.runWithClient (
95
+ () => awaitFakeAsync (callback),
96
+ httpClientFactory ?? () => mockHttpClient);
97
+
82
98
Future <void > init () async {
83
99
addTearDown (testBinding.reset);
84
100
testBinding.firebaseMessagingInitialToken = '012abc' ;
@@ -114,6 +130,7 @@ void main() {
114
130
required String expectedTitle,
115
131
required String expectedTagComponent,
116
132
required bool expectedIsGroupConversation,
133
+ List <int >? expectedIconBitmap = kSolidBlueAvatar,
117
134
}) {
118
135
final expectedTag = '${data .realmUri }|${data .userId }|$expectedTagComponent ' ;
119
136
final expectedGroupKey = '${data .realmUri }|${data .userId }' ;
@@ -135,7 +152,9 @@ void main() {
135
152
..text.equals (messageData.content)
136
153
..timestampMs.equals (messageData.time * 1000 )
137
154
..person.which ((it) => it.isNotNull ()
138
- ..iconBitmap.which ((it) => isLast ? it.isNotNull () : it.isNull ())
155
+ ..iconBitmap.which ((it) => (isLast && expectedIconBitmap != null )
156
+ ? it.isNotNull ().deepEquals (expectedIconBitmap)
157
+ : it.isNull ())
139
158
..key.equals (expectedSenderKey)
140
159
..name.equals (messageData.senderFullName));
141
160
});
@@ -221,7 +240,7 @@ void main() {
221
240
async .flushMicrotasks ();
222
241
}
223
242
224
- test ('stream message' , () => awaitFakeAsync ((async ) async {
243
+ test ('stream message' , runWithHttpClient ((async ) async {
225
244
await init ();
226
245
final stream = eg.stream ();
227
246
final message = eg.streamMessage (stream: stream);
@@ -231,7 +250,7 @@ void main() {
231
250
expectedTagComponent: 'stream:${message .streamId }:${message .topic }' );
232
251
}));
233
252
234
- test ('stream message: multiple messages, same topic' , () => awaitFakeAsync ((async ) async {
253
+ test ('stream message: multiple messages, same topic' , runWithHttpClient ((async ) async {
235
254
await init ();
236
255
final stream = eg.stream ();
237
256
const topic = 'topic 1' ;
@@ -267,7 +286,67 @@ void main() {
267
286
expectedTagComponent: expectedTagComponent);
268
287
}));
269
288
270
- test ('stream message: stream name omitted' , () => awaitFakeAsync ((async ) async {
289
+ test ('stream message: multiple messages, different topics' , runWithHttpClient ((async ) async {
290
+ await init ();
291
+ final stream = eg.stream ();
292
+ const topicA = 'topic A' ;
293
+ const topicB = 'topic B' ;
294
+ final message1 = eg.streamMessage (topic: topicA, stream: stream);
295
+ final data1 = messageFcmMessage (message1, streamName: stream.name);
296
+ final message2 = eg.streamMessage (topic: topicB, stream: stream);
297
+ final data2 = messageFcmMessage (message2, streamName: stream.name);
298
+ final message3 = eg.streamMessage (topic: topicA, stream: stream);
299
+ final data3 = messageFcmMessage (message3, streamName: stream.name);
300
+
301
+ await receiveFcmMessage (async , data1);
302
+ checkNotification (data1,
303
+ messageStyleMessages: [data1],
304
+ expectedIsGroupConversation: true ,
305
+ expectedTitle: '#${stream .name } > $topicA ' ,
306
+ expectedTagComponent: 'stream:${stream .streamId }:$topicA ' );
307
+
308
+ await receiveFcmMessage (async , data2);
309
+ checkNotification (data2,
310
+ messageStyleMessages: [data2],
311
+ expectedIsGroupConversation: true ,
312
+ expectedTitle: '#${stream .name } > $topicB ' ,
313
+ expectedTagComponent: 'stream:${stream .streamId }:$topicB ' );
314
+
315
+ await receiveFcmMessage (async , data3);
316
+ checkNotification (data3,
317
+ messageStyleMessages: [data1, data3],
318
+ expectedIsGroupConversation: true ,
319
+ expectedTitle: '#${stream .name } > $topicA ' ,
320
+ expectedTagComponent: 'stream:${stream .streamId }:$topicA ' );
321
+ }));
322
+
323
+ test ('stream message: conversation stays same when stream is renamed' , runWithHttpClient ((async ) async {
324
+ await init ();
325
+ var stream = eg.stream (streamId: 1 , name: 'Before' );
326
+ const topic = 'topic' ;
327
+ final message1 = eg.streamMessage (topic: topic, stream: stream);
328
+ final data1 = messageFcmMessage (message1, streamName: stream.name);
329
+
330
+ await receiveFcmMessage (async , data1);
331
+ checkNotification (data1,
332
+ messageStyleMessages: [data1],
333
+ expectedIsGroupConversation: true ,
334
+ expectedTitle: '#Before > $topic ' ,
335
+ expectedTagComponent: 'stream:${stream .streamId }:$topic ' );
336
+
337
+ stream = eg.stream (streamId: 1 , name: 'After' );
338
+ final message2 = eg.streamMessage (topic: topic, stream: stream);
339
+ final data2 = messageFcmMessage (message2, streamName: stream.name);
340
+
341
+ await receiveFcmMessage (async , data2);
342
+ checkNotification (data2,
343
+ messageStyleMessages: [data1, data2],
344
+ expectedIsGroupConversation: true ,
345
+ expectedTitle: '#After > $topic ' ,
346
+ expectedTagComponent: 'stream:${stream .streamId }:$topic ' );
347
+ }));
348
+
349
+ test ('stream message: stream name omitted' , runWithHttpClient ((async ) async {
271
350
await init ();
272
351
final stream = eg.stream ();
273
352
final message = eg.streamMessage (stream: stream);
@@ -277,7 +356,7 @@ void main() {
277
356
expectedTagComponent: 'stream:${message .streamId }:${message .topic }' );
278
357
}));
279
358
280
- test ('group DM: 3 users' , () => awaitFakeAsync ((async ) async {
359
+ test ('group DM: 3 users' , runWithHttpClient ((async ) async {
281
360
await init ();
282
361
final message = eg.dmMessage (from: eg.thirdUser, to: [eg.otherUser, eg.selfUser]);
283
362
await checkNotifications (async , messageFcmMessage (message),
@@ -286,7 +365,7 @@ void main() {
286
365
expectedTagComponent: 'dm:${message .allRecipientIds .join ("," )}' );
287
366
}));
288
367
289
- test ('group DM: more than 3 users' , () => awaitFakeAsync ((async ) async {
368
+ test ('group DM: more than 3 users' , runWithHttpClient ((async ) async {
290
369
await init ();
291
370
final message = eg.dmMessage (from: eg.thirdUser,
292
371
to: [eg.otherUser, eg.selfUser, eg.fourthUser]);
@@ -296,7 +375,31 @@ void main() {
296
375
expectedTagComponent: 'dm:${message .allRecipientIds .join ("," )}' );
297
376
}));
298
377
299
- test ('1:1 DM' , () => awaitFakeAsync ((async ) async {
378
+ test ('group DM: title updates with latest sender' , runWithHttpClient ((async ) async {
379
+ await init ();
380
+ final message1 = eg.dmMessage (from: eg.otherUser, to: [eg.selfUser, eg.thirdUser]);
381
+ final data1 = messageFcmMessage (message1);
382
+ final message2 = eg.dmMessage (from: eg.thirdUser, to: [eg.selfUser, eg.otherUser]);
383
+ final data2 = messageFcmMessage (message2);
384
+
385
+ final expectedTagComponent = 'dm:${message1 .allRecipientIds .join ("," )}' ;
386
+
387
+ await receiveFcmMessage (async , data1);
388
+ checkNotification (data1,
389
+ messageStyleMessages: [data1],
390
+ expectedIsGroupConversation: true ,
391
+ expectedTitle: "${eg .otherUser .fullName } to you and 1 other" ,
392
+ expectedTagComponent: expectedTagComponent);
393
+
394
+ await receiveFcmMessage (async , data2);
395
+ checkNotification (data2,
396
+ messageStyleMessages: [data1, data2],
397
+ expectedIsGroupConversation: true ,
398
+ expectedTitle: "${eg .thirdUser .fullName } to you and 1 other" ,
399
+ expectedTagComponent: expectedTagComponent);
400
+ }));
401
+
402
+ test ('1:1 DM' , runWithHttpClient ((async ) async {
300
403
await init ();
301
404
final message = eg.dmMessage (from: eg.otherUser, to: [eg.selfUser]);
302
405
await checkNotifications (async , messageFcmMessage (message),
@@ -305,7 +408,89 @@ void main() {
305
408
expectedTagComponent: 'dm:${message .allRecipientIds .join ("," )}' );
306
409
}));
307
410
308
- test ('self-DM' , () => awaitFakeAsync ((async ) async {
411
+ test ('1:1 DM: title updates when sender name changes' , runWithHttpClient ((async ) async {
412
+ await init ();
413
+ final otherUser = eg.user (fullName: 'Before' );
414
+ final message1 = eg.dmMessage (from: otherUser, to: [eg.selfUser]);
415
+ final data1 = messageFcmMessage (message1);
416
+
417
+ final expectedTagComponent = 'dm:${message1 .allRecipientIds .join ("," )}' ;
418
+
419
+ await receiveFcmMessage (async , data1);
420
+ checkNotification (data1,
421
+ messageStyleMessages: [data1],
422
+ expectedIsGroupConversation: false ,
423
+ expectedTitle: 'Before' ,
424
+ expectedTagComponent: expectedTagComponent);
425
+
426
+ otherUser.fullName = 'After' ;
427
+ final message2 = eg.dmMessage (from: otherUser, to: [eg.selfUser]);
428
+ final data2 = messageFcmMessage (message2);
429
+
430
+ await receiveFcmMessage (async , data2);
431
+ checkNotification (data2,
432
+ messageStyleMessages: [data1, data2],
433
+ expectedIsGroupConversation: false ,
434
+ expectedTitle: 'After' ,
435
+ expectedTagComponent: expectedTagComponent);
436
+ }));
437
+
438
+ test ('1:1 DM: conversation stays same when sender email changes' , runWithHttpClient ((async ) async {
439
+ await init ();
440
+ final otherUser
= eg.
user (email
: '[email protected] ' );
441
+ final message1 = eg.dmMessage (from: otherUser, to: [eg.selfUser]);
442
+ final data1 = messageFcmMessage (message1);
443
+
444
+ final expectedTagComponent = 'dm:${message1 .allRecipientIds .join ("," )}' ;
445
+
446
+ await receiveFcmMessage (async , data1);
447
+ checkNotification (data1,
448
+ messageStyleMessages: [data1],
449
+ expectedIsGroupConversation: false ,
450
+ expectedTitle: otherUser.fullName,
451
+ expectedTagComponent: expectedTagComponent);
452
+
453
+ otherUser.email
= '[email protected] ' ;
454
+ final message2 = eg.dmMessage (from: otherUser, to: [eg.selfUser]);
455
+ final data2 = messageFcmMessage (message2);
456
+
457
+ await receiveFcmMessage (async , data2);
458
+ checkNotification (data2,
459
+ messageStyleMessages: [data1, data2],
460
+ expectedIsGroupConversation: false ,
461
+ expectedTitle: otherUser.fullName,
462
+ expectedTagComponent: expectedTagComponent);
463
+ }));
464
+
465
+ test ('1:1 DM: sender avatar loading fails, remote error' , runWithHttpClient ((async ) async {
466
+ await init ();
467
+ final message = eg.dmMessage (from: eg.otherUser, to: [eg.selfUser]);
468
+ final data = messageFcmMessage (message);
469
+ await receiveFcmMessage (async , data);
470
+ checkNotification (data,
471
+ messageStyleMessages: [data],
472
+ expectedIsGroupConversation: false ,
473
+ expectedTitle: eg.otherUser.fullName,
474
+ expectedTagComponent: 'dm:${message .allRecipientIds .join ("," )}' ,
475
+ expectedIconBitmap: null );
476
+ }, httpClientFactory: () => http_testing.MockClient ((request) async =>
477
+ http.Response .bytes ([], HttpStatus .internalServerError))));
478
+
479
+ test ('1:1 DM: sender avatar loading fails, local error' , runWithHttpClient ((async ) async {
480
+ await init ();
481
+ final message = eg.dmMessage (from: eg.otherUser, to: [eg.selfUser]);
482
+ final data = messageFcmMessage (message);
483
+ await receiveFcmMessage (async , data);
484
+ checkNotification (data,
485
+ messageStyleMessages: [data],
486
+ expectedIsGroupConversation: false ,
487
+ expectedTitle: eg.otherUser.fullName,
488
+ expectedTagComponent: 'dm:${message .allRecipientIds .join ("," )}' ,
489
+ expectedIconBitmap: null );
490
+ }, httpClientFactory: () => http_testing.MockClient ((request) async =>
491
+ throw http.ClientException ('Network failure' ))));
492
+
493
+ test ('self-DM' , runWithHttpClient ((async ) async {
309
494
await init ();
310
495
final message = eg.dmMessage (from: eg.selfUser, to: []);
311
496
await checkNotifications (async , messageFcmMessage (message),
0 commit comments