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,8 @@ 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) : it.isNull ())
139
157
..key.equals (expectedSenderKey)
140
158
..name.equals (messageData.senderFullName));
141
159
});
@@ -221,7 +239,7 @@ void main() {
221
239
async .flushMicrotasks ();
222
240
}
223
241
224
- test ('stream message' , () => awaitFakeAsync ((async ) async {
242
+ test ('stream message' , runWithHttpClient ((async ) async {
225
243
await init ();
226
244
final stream = eg.stream ();
227
245
final message = eg.streamMessage (stream: stream);
@@ -231,7 +249,7 @@ void main() {
231
249
expectedTagComponent: 'stream:${message .streamId }:${message .topic }' );
232
250
}));
233
251
234
- test ('stream message: multiple messages, same topic' , () => awaitFakeAsync ((async ) async {
252
+ test ('stream message: multiple messages, same topic' , runWithHttpClient ((async ) async {
235
253
await init ();
236
254
final stream = eg.stream ();
237
255
const topic = 'topic 1' ;
@@ -267,7 +285,67 @@ void main() {
267
285
expectedTagComponent: expectedTagComponent);
268
286
}));
269
287
270
- test ('stream message: stream name omitted' , () => awaitFakeAsync ((async ) async {
288
+ test ('stream message: multiple messages, different topics' , runWithHttpClient ((async ) async {
289
+ await init ();
290
+ final stream = eg.stream ();
291
+ const topicA = 'topic A' ;
292
+ const topicB = 'topic B' ;
293
+ final message1 = eg.streamMessage (topic: topicA, stream: stream);
294
+ final data1 = messageFcmMessage (message1, streamName: stream.name);
295
+ final message2 = eg.streamMessage (topic: topicB, stream: stream);
296
+ final data2 = messageFcmMessage (message2, streamName: stream.name);
297
+ final message3 = eg.streamMessage (topic: topicA, stream: stream);
298
+ final data3 = messageFcmMessage (message3, streamName: stream.name);
299
+
300
+ await receiveFcmMessage (async , data1);
301
+ checkNotification (data1,
302
+ messageStyleMessages: [data1],
303
+ expectedIsGroupConversation: true ,
304
+ expectedTitle: '#${stream .name } > $topicA ' ,
305
+ expectedTagComponent: 'stream:${stream .streamId }:$topicA ' );
306
+
307
+ await receiveFcmMessage (async , data2);
308
+ checkNotification (data2,
309
+ messageStyleMessages: [data2],
310
+ expectedIsGroupConversation: true ,
311
+ expectedTitle: '#${stream .name } > $topicB ' ,
312
+ expectedTagComponent: 'stream:${stream .streamId }:$topicB ' );
313
+
314
+ await receiveFcmMessage (async , data3);
315
+ checkNotification (data3,
316
+ messageStyleMessages: [data1, data3],
317
+ expectedIsGroupConversation: true ,
318
+ expectedTitle: '#${stream .name } > $topicA ' ,
319
+ expectedTagComponent: 'stream:${stream .streamId }:$topicA ' );
320
+ }));
321
+
322
+ test ('stream message: conversation stays same when stream is renamed' , runWithHttpClient ((async ) async {
323
+ await init ();
324
+ var stream = eg.stream (streamId: 1 , name: 'Before' );
325
+ const topic = 'topic' ;
326
+ final message1 = eg.streamMessage (topic: topic, stream: stream);
327
+ final data1 = messageFcmMessage (message1, streamName: stream.name);
328
+
329
+ await receiveFcmMessage (async , data1);
330
+ checkNotification (data1,
331
+ messageStyleMessages: [data1],
332
+ expectedIsGroupConversation: true ,
333
+ expectedTitle: '#Before > $topic ' ,
334
+ expectedTagComponent: 'stream:${stream .streamId }:$topic ' );
335
+
336
+ stream = eg.stream (streamId: 1 , name: 'After' );
337
+ final message2 = eg.streamMessage (topic: topic, stream: stream);
338
+ final data2 = messageFcmMessage (message2, streamName: stream.name);
339
+
340
+ await receiveFcmMessage (async , data2);
341
+ checkNotification (data2,
342
+ messageStyleMessages: [data1, data2],
343
+ expectedIsGroupConversation: true ,
344
+ expectedTitle: '#After > $topic ' ,
345
+ expectedTagComponent: 'stream:${stream .streamId }:$topic ' );
346
+ }));
347
+
348
+ test ('stream message: stream name omitted' , runWithHttpClient ((async ) async {
271
349
await init ();
272
350
final stream = eg.stream ();
273
351
final message = eg.streamMessage (stream: stream);
@@ -277,7 +355,7 @@ void main() {
277
355
expectedTagComponent: 'stream:${message .streamId }:${message .topic }' );
278
356
}));
279
357
280
- test ('group DM: 3 users' , () => awaitFakeAsync ((async ) async {
358
+ test ('group DM: 3 users' , runWithHttpClient ((async ) async {
281
359
await init ();
282
360
final message = eg.dmMessage (from: eg.thirdUser, to: [eg.otherUser, eg.selfUser]);
283
361
await checkNotifications (async , messageFcmMessage (message),
@@ -286,7 +364,7 @@ void main() {
286
364
expectedTagComponent: 'dm:${message .allRecipientIds .join ("," )}' );
287
365
}));
288
366
289
- test ('group DM: more than 3 users' , () => awaitFakeAsync ((async ) async {
367
+ test ('group DM: more than 3 users' , runWithHttpClient ((async ) async {
290
368
await init ();
291
369
final message = eg.dmMessage (from: eg.thirdUser,
292
370
to: [eg.otherUser, eg.selfUser, eg.fourthUser]);
@@ -296,7 +374,31 @@ void main() {
296
374
expectedTagComponent: 'dm:${message .allRecipientIds .join ("," )}' );
297
375
}));
298
376
299
- test ('1:1 DM' , () => awaitFakeAsync ((async ) async {
377
+ test ('group DM: title updates with latest sender' , runWithHttpClient ((async ) async {
378
+ await init ();
379
+ final message1 = eg.dmMessage (from: eg.otherUser, to: [eg.selfUser, eg.thirdUser]);
380
+ final data1 = messageFcmMessage (message1);
381
+ final message2 = eg.dmMessage (from: eg.thirdUser, to: [eg.selfUser, eg.otherUser]);
382
+ final data2 = messageFcmMessage (message2);
383
+
384
+ final expectedTagComponent = 'dm:${message1 .allRecipientIds .join ("," )}' ;
385
+
386
+ await receiveFcmMessage (async , data1);
387
+ checkNotification (data1,
388
+ messageStyleMessages: [data1],
389
+ expectedIsGroupConversation: true ,
390
+ expectedTitle: "${eg .otherUser .fullName } to you and 1 other" ,
391
+ expectedTagComponent: expectedTagComponent);
392
+
393
+ await receiveFcmMessage (async , data2);
394
+ checkNotification (data2,
395
+ messageStyleMessages: [data1, data2],
396
+ expectedIsGroupConversation: true ,
397
+ expectedTitle: "${eg .thirdUser .fullName } to you and 1 other" ,
398
+ expectedTagComponent: expectedTagComponent);
399
+ }));
400
+
401
+ test ('1:1 DM' , runWithHttpClient ((async ) async {
300
402
await init ();
301
403
final message = eg.dmMessage (from: eg.otherUser, to: [eg.selfUser]);
302
404
await checkNotifications (async , messageFcmMessage (message),
@@ -305,7 +407,89 @@ void main() {
305
407
expectedTagComponent: 'dm:${message .allRecipientIds .join ("," )}' );
306
408
}));
307
409
308
- test ('self-DM' , () => awaitFakeAsync ((async ) async {
410
+ test ('1:1 DM: title updates when sender name changes' , runWithHttpClient ((async ) async {
411
+ await init ();
412
+ final otherUser = eg.user (fullName: 'Before' );
413
+ final message1 = eg.dmMessage (from: otherUser, to: [eg.selfUser]);
414
+ final data1 = messageFcmMessage (message1);
415
+
416
+ final expectedTagComponent = 'dm:${message1 .allRecipientIds .join ("," )}' ;
417
+
418
+ await receiveFcmMessage (async , data1);
419
+ checkNotification (data1,
420
+ messageStyleMessages: [data1],
421
+ expectedIsGroupConversation: false ,
422
+ expectedTitle: 'Before' ,
423
+ expectedTagComponent: expectedTagComponent);
424
+
425
+ otherUser.fullName = 'After' ;
426
+ final message2 = eg.dmMessage (from: otherUser, to: [eg.selfUser]);
427
+ final data2 = messageFcmMessage (message2);
428
+
429
+ await receiveFcmMessage (async , data2);
430
+ checkNotification (data2,
431
+ messageStyleMessages: [data1, data2],
432
+ expectedIsGroupConversation: false ,
433
+ expectedTitle: 'After' ,
434
+ expectedTagComponent: expectedTagComponent);
435
+ }));
436
+
437
+ test ('1:1 DM: conversation stays same when sender email changes' , runWithHttpClient ((async ) async {
438
+ await init ();
439
+ final otherUser
= eg.
user (email
: '[email protected] ' );
440
+ final message1 = eg.dmMessage (from: otherUser, to: [eg.selfUser]);
441
+ final data1 = messageFcmMessage (message1);
442
+
443
+ final expectedTagComponent = 'dm:${message1 .allRecipientIds .join ("," )}' ;
444
+
445
+ await receiveFcmMessage (async , data1);
446
+ checkNotification (data1,
447
+ messageStyleMessages: [data1],
448
+ expectedIsGroupConversation: false ,
449
+ expectedTitle: otherUser.fullName,
450
+ expectedTagComponent: expectedTagComponent);
451
+
452
+ otherUser.email
= '[email protected] ' ;
453
+ final message2 = eg.dmMessage (from: otherUser, to: [eg.selfUser]);
454
+ final data2 = messageFcmMessage (message2);
455
+
456
+ await receiveFcmMessage (async , data2);
457
+ checkNotification (data2,
458
+ messageStyleMessages: [data1, data2],
459
+ expectedIsGroupConversation: false ,
460
+ expectedTitle: otherUser.fullName,
461
+ expectedTagComponent: expectedTagComponent);
462
+ }));
463
+
464
+ test ('1:1 DM: sender avatar loading fails, remote error' , runWithHttpClient ((async ) async {
465
+ await init ();
466
+ final message = eg.dmMessage (from: eg.otherUser, to: [eg.selfUser]);
467
+ final data = messageFcmMessage (message);
468
+ await receiveFcmMessage (async , data);
469
+ checkNotification (data,
470
+ messageStyleMessages: [data],
471
+ expectedIsGroupConversation: false ,
472
+ expectedTitle: eg.otherUser.fullName,
473
+ expectedTagComponent: 'dm:${message .allRecipientIds .join ("," )}' ,
474
+ expectedIconBitmap: null );
475
+ }, httpClientFactory: () => http_testing.MockClient ((request) async =>
476
+ http.Response .bytes ([], HttpStatus .internalServerError))));
477
+
478
+ test ('1:1 DM: sender avatar loading fails, local error' , runWithHttpClient ((async ) async {
479
+ await init ();
480
+ final message = eg.dmMessage (from: eg.otherUser, to: [eg.selfUser]);
481
+ final data = messageFcmMessage (message);
482
+ await receiveFcmMessage (async , data);
483
+ checkNotification (data,
484
+ messageStyleMessages: [data],
485
+ expectedIsGroupConversation: false ,
486
+ expectedTitle: eg.otherUser.fullName,
487
+ expectedTagComponent: 'dm:${message .allRecipientIds .join ("," )}' ,
488
+ expectedIconBitmap: null );
489
+ }, httpClientFactory: () => http_testing.MockClient ((request) async =>
490
+ throw http.ClientException ('Network failure' ))));
491
+
492
+ test ('self-DM' , runWithHttpClient ((async ) async {
309
493
await init ();
310
494
final message = eg.dmMessage (from: eg.selfUser, to: []);
311
495
await checkNotifications (async , messageFcmMessage (message),
0 commit comments