@@ -2,14 +2,16 @@ import 'dart:convert';
2
2
import 'dart:typed_data' ;
3
3
4
4
import 'package:checks/checks.dart' ;
5
+ import 'package:collection/collection.dart' ;
5
6
import 'package:fake_async/fake_async.dart' ;
6
7
import 'package:firebase_messaging/firebase_messaging.dart' ;
7
8
import 'package:flutter/material.dart' ;
8
- import 'package:flutter_local_notifications/flutter_local_notifications.dart' hide Message;
9
+ import 'package:flutter_local_notifications/flutter_local_notifications.dart' hide Message, Person ;
9
10
import 'package:flutter_test/flutter_test.dart' ;
10
11
import 'package:zulip/api/model/model.dart' ;
11
12
import 'package:zulip/api/notifications.dart' ;
12
13
import 'package:zulip/host/android_notifications.dart' ;
14
+ import 'package:zulip/model/localizations.dart' ;
13
15
import 'package:zulip/model/narrow.dart' ;
14
16
import 'package:zulip/model/store.dart' ;
15
17
import 'package:zulip/notifications/display.dart' ;
@@ -75,6 +77,7 @@ MessageFcmMessage messageFcmMessage(
75
77
76
78
void main () {
77
79
TestZulipBinding .ensureInitialized ();
80
+ final zulipLocalizations = GlobalLocalizations .zulipLocalizations;
78
81
79
82
Future <void > init () async {
80
83
addTearDown (testBinding.reset);
@@ -107,25 +110,53 @@ void main() {
107
110
108
111
group ('NotificationDisplayManager show' , () {
109
112
void checkNotification (MessageFcmMessage data, {
113
+ required List <MessageFcmMessage > messageStyleMessages,
110
114
required String expectedTitle,
111
115
required String expectedTagComponent,
116
+ required bool expectedIsGroupConversation,
112
117
}) {
113
118
final expectedTag = '${data .realmUri }|${data .userId }|$expectedTagComponent ' ;
114
119
final expectedGroupKey = '${data .realmUri }|${data .userId }' ;
115
120
final expectedId =
116
121
NotificationDisplayManager .notificationIdAsHashOf (expectedTag);
117
122
const expectedIntentFlags =
118
123
PendingIntentFlag .immutable | PendingIntentFlag .updateCurrent;
124
+ final expectedSelfUserKey = '${data .realmUri }|${data .userId }' ;
125
+
126
+ final messageStyleMessagesChecks = messageStyleMessages
127
+ .mapIndexed <Condition <Object ?>>((i, messageData) {
128
+ assert (messageData.realmUri == data.realmUri);
129
+ assert (messageData.userId == data.userId);
130
+
131
+ final expectedSenderKey =
132
+ '${messageData .realmUri }|${messageData .senderId }' ;
133
+ final isLast = i == (messageStyleMessages.length - 1 );
134
+ return (it) => it.isA <MessagingStyleMessage >()
135
+ ..text.equals (messageData.content)
136
+ ..timestampMs.equals (messageData.time * 1000 )
137
+ ..person.which ((it) => it.isNotNull ()
138
+ ..iconBitmap.which ((it) => isLast ? it.isNotNull () : it.isNull ())
139
+ ..key.equals (expectedSenderKey)
140
+ ..name.equals (messageData.senderFullName));
141
+ });
119
142
120
143
check (testBinding.androidNotificationHost.takeNotifyCalls ())
121
- ..length.equals (2 )
122
- ..containsInOrder (< Condition <AndroidNotificationHostApiNotifyCall >> [
123
- (it) => it
144
+ .deepEquals (< Condition <Object ?>> [
145
+ (it) => it.isA <AndroidNotificationHostApiNotifyCall >()
124
146
..id.equals (expectedId)
125
147
..tag.equals (expectedTag)
126
148
..channelId.equals (NotificationChannelManager .kChannelId)
127
- ..contentTitle.equals (expectedTitle)
128
- ..contentText.equals (data.content)
149
+ ..contentTitle.isNull ()
150
+ ..contentText.isNull ()
151
+ ..messagingStyle.which ((it) => it.isNotNull ()
152
+ ..user.which ((it) => it
153
+ ..iconBitmap.isNull ()
154
+ ..key.equals (expectedSelfUserKey)
155
+ ..name.equals (zulipLocalizations.notifSelfUser))
156
+ ..isGroupConversation.equals (expectedIsGroupConversation)
157
+ ..conversationTitle.equals (expectedTitle)
158
+ ..messages.deepEquals (messageStyleMessagesChecks))
159
+ ..number.equals (messageStyleMessages.length)
129
160
..color.equals (kZulipBrandColor.value)
130
161
..smallIconResourceName.equals ('zulip_notification' )
131
162
..extras.isNull ()
@@ -137,7 +168,7 @@ void main() {
137
168
..requestCode.equals (expectedId)
138
169
..flags.equals (expectedIntentFlags)
139
170
..intentPayload.equals (jsonEncode (data.toJson ()))),
140
- (it) => it
171
+ (it) => it. isA < AndroidNotificationHostApiNotifyCall >()
141
172
..id.equals (NotificationDisplayManager .notificationIdAsHashOf (expectedGroupKey))
142
173
..tag.equals (expectedGroupKey)
143
174
..channelId.equals (NotificationChannelManager .kChannelId)
@@ -151,13 +182,14 @@ void main() {
151
182
..inboxStyle.which ((it) => it.isNotNull ()
152
183
..summaryText.equals (data.realmUri.toString ()))
153
184
..autoCancel.equals (true )
154
- ..contentIntent.isNull ()
185
+ ..contentIntent.isNull (),
155
186
]);
156
187
}
157
188
158
189
Future <void > checkNotifications (FakeAsync async , MessageFcmMessage data, {
159
190
required String expectedTitle,
160
191
required String expectedTagComponent,
192
+ required bool expectedIsGroupConversation,
161
193
}) async {
162
194
// We could just call `NotificationDisplayManager.onFcmMessage`.
163
195
// But this way is cheap, and it provides our test coverage of
@@ -166,30 +198,81 @@ void main() {
166
198
testBinding.firebaseMessaging.onMessage.add (
167
199
RemoteMessage (data: data.toJson ()));
168
200
async .flushMicrotasks ();
169
- checkNotification (data, expectedTitle: expectedTitle,
201
+ checkNotification (data,
202
+ messageStyleMessages: [data],
203
+ expectedIsGroupConversation: expectedIsGroupConversation,
204
+ expectedTitle: expectedTitle,
170
205
expectedTagComponent: expectedTagComponent);
206
+ testBinding.androidNotificationHost.clearActiveNotifications ();
171
207
172
208
testBinding.firebaseMessaging.onBackgroundMessage.add (
173
209
RemoteMessage (data: data.toJson ()));
174
210
async .flushMicrotasks ();
175
- checkNotification (data, expectedTitle: expectedTitle,
211
+ checkNotification (data,
212
+ messageStyleMessages: [data],
213
+ expectedIsGroupConversation: expectedIsGroupConversation,
214
+ expectedTitle: expectedTitle,
176
215
expectedTagComponent: expectedTagComponent);
177
216
}
178
217
218
+ Future <void > receiveFcmMessage (FakeAsync async , MessageFcmMessage data) async {
219
+ testBinding.firebaseMessaging.onMessage.add (
220
+ RemoteMessage (data: data.toJson ()));
221
+ async .flushMicrotasks ();
222
+ }
223
+
179
224
test ('stream message' , () => awaitFakeAsync ((async ) async {
180
225
await init ();
181
226
final stream = eg.stream ();
182
227
final message = eg.streamMessage (stream: stream);
183
228
await checkNotifications (async , messageFcmMessage (message, streamName: stream.name),
229
+ expectedIsGroupConversation: true ,
184
230
expectedTitle: '#${stream .name } > ${message .topic }' ,
185
231
expectedTagComponent: 'stream:${message .streamId }:${message .topic }' );
186
232
}));
187
233
234
+ test ('multiple stream messages, same topic' , () => awaitFakeAsync ((async ) async {
235
+ await init ();
236
+ final stream = eg.stream ();
237
+ const topic = 'topic 1' ;
238
+ final message1 = eg.streamMessage (topic: topic, stream: stream);
239
+ final data1 = messageFcmMessage (message1, streamName: stream.name);
240
+ final message2 = eg.streamMessage (topic: topic, stream: stream);
241
+ final data2 = messageFcmMessage (message2, streamName: stream.name);
242
+ final message3 = eg.streamMessage (topic: topic, stream: stream);
243
+ final data3 = messageFcmMessage (message3, streamName: stream.name);
244
+
245
+ final expectedTitle = '#${stream .name } > $topic ' ;
246
+ final expectedTagComponent = 'stream:${stream .streamId }:$topic ' ;
247
+
248
+ await receiveFcmMessage (async , data1);
249
+ checkNotification (data1,
250
+ messageStyleMessages: [data1],
251
+ expectedIsGroupConversation: true ,
252
+ expectedTitle: expectedTitle,
253
+ expectedTagComponent: expectedTagComponent);
254
+
255
+ await receiveFcmMessage (async , data2);
256
+ checkNotification (data2,
257
+ messageStyleMessages: [data1, data2],
258
+ expectedIsGroupConversation: true ,
259
+ expectedTitle: expectedTitle,
260
+ expectedTagComponent: expectedTagComponent);
261
+
262
+ await receiveFcmMessage (async , data3);
263
+ checkNotification (data3,
264
+ messageStyleMessages: [data1, data2, data3],
265
+ expectedIsGroupConversation: true ,
266
+ expectedTitle: expectedTitle,
267
+ expectedTagComponent: expectedTagComponent);
268
+ }));
269
+
188
270
test ('stream message, stream name omitted' , () => awaitFakeAsync ((async ) async {
189
271
await init ();
190
272
final stream = eg.stream ();
191
273
final message = eg.streamMessage (stream: stream);
192
274
await checkNotifications (async , messageFcmMessage (message, streamName: null ),
275
+ expectedIsGroupConversation: true ,
193
276
expectedTitle: '#(unknown channel) > ${message .topic }' ,
194
277
expectedTagComponent: 'stream:${message .streamId }:${message .topic }' );
195
278
}));
@@ -198,6 +281,7 @@ void main() {
198
281
await init ();
199
282
final message = eg.dmMessage (from: eg.thirdUser, to: [eg.otherUser, eg.selfUser]);
200
283
await checkNotifications (async , messageFcmMessage (message),
284
+ expectedIsGroupConversation: true ,
201
285
expectedTitle: "${eg .thirdUser .fullName } to you and 1 other" ,
202
286
expectedTagComponent: 'dm:${message .allRecipientIds .join ("," )}' );
203
287
}));
@@ -207,6 +291,7 @@ void main() {
207
291
final message = eg.dmMessage (from: eg.thirdUser,
208
292
to: [eg.otherUser, eg.selfUser, eg.fourthUser]);
209
293
await checkNotifications (async , messageFcmMessage (message),
294
+ expectedIsGroupConversation: true ,
210
295
expectedTitle: "${eg .thirdUser .fullName } to you and 2 others" ,
211
296
expectedTagComponent: 'dm:${message .allRecipientIds .join ("," )}' );
212
297
}));
@@ -215,6 +300,7 @@ void main() {
215
300
await init ();
216
301
final message = eg.dmMessage (from: eg.otherUser, to: [eg.selfUser]);
217
302
await checkNotifications (async , messageFcmMessage (message),
303
+ expectedIsGroupConversation: false ,
218
304
expectedTitle: eg.otherUser.fullName,
219
305
expectedTagComponent: 'dm:${message .allRecipientIds .join ("," )}' );
220
306
}));
@@ -223,6 +309,7 @@ void main() {
223
309
await init ();
224
310
final message = eg.dmMessage (from: eg.selfUser, to: []);
225
311
await checkNotifications (async , messageFcmMessage (message),
312
+ expectedIsGroupConversation: false ,
226
313
expectedTitle: eg.selfUser.fullName,
227
314
expectedTagComponent: 'dm:${message .allRecipientIds .join ("," )}' );
228
315
}));
@@ -403,6 +490,8 @@ extension on Subject<AndroidNotificationHostApiNotifyCall> {
403
490
Subject <String ?> get groupKey => has ((x) => x.groupKey, 'groupKey' );
404
491
Subject <InboxStyle ?> get inboxStyle => has ((x) => x.inboxStyle, 'inboxStyle' );
405
492
Subject <bool ?> get isGroupSummary => has ((x) => x.isGroupSummary, 'isGroupSummary' );
493
+ Subject <MessagingStyle ?> get messagingStyle => has ((x) => x.messagingStyle, 'messagingStyle' );
494
+ Subject <int ?> get number => has ((x) => x.number, 'number' );
406
495
Subject <String ?> get smallIconResourceName => has ((x) => x.smallIconResourceName, 'smallIconResourceName' );
407
496
}
408
497
@@ -415,3 +504,22 @@ extension on Subject<PendingIntent> {
415
504
extension on Subject <InboxStyle > {
416
505
Subject <String > get summaryText => has ((x) => x.summaryText, 'summaryText' );
417
506
}
507
+
508
+ extension on Subject <MessagingStyle > {
509
+ Subject <Person > get user => has ((x) => x.user, 'user' );
510
+ Subject <String ?> get conversationTitle => has ((x) => x.conversationTitle, 'conversationTitle' );
511
+ Subject <List <MessagingStyleMessage ?>> get messages => has ((x) => x.messages, 'messages' );
512
+ Subject <bool > get isGroupConversation => has ((x) => x.isGroupConversation, 'isGroupConversation' );
513
+ }
514
+
515
+ extension on Subject <Person > {
516
+ Subject <Uint8List ?> get iconBitmap => has ((x) => x.iconBitmap, 'iconBitmap' );
517
+ Subject <String > get key => has ((x) => x.key, 'key' );
518
+ Subject <String > get name => has ((x) => x.name, 'name' );
519
+ }
520
+
521
+ extension on Subject <MessagingStyleMessage > {
522
+ Subject <String > get text => has ((x) => x.text, 'text' );
523
+ Subject <int > get timestampMs => has ((x) => x.timestampMs, 'timestampMs' );
524
+ Subject <Person > get person => has ((x) => x.person, 'person' );
525
+ }
0 commit comments