@@ -2,10 +2,11 @@ import 'dart:convert';
22import 'dart:typed_data' ;
33
44import 'package:checks/checks.dart' ;
5+ import 'package:collection/collection.dart' ;
56import 'package:fake_async/fake_async.dart' ;
67import 'package:firebase_messaging/firebase_messaging.dart' ;
78import '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 ;
910import 'package:flutter_test/flutter_test.dart' ;
1011import 'package:zulip/api/model/model.dart' ;
1112import 'package:zulip/api/notifications.dart' ;
@@ -107,25 +108,53 @@ void main() {
107108
108109 group ('NotificationDisplayManager show' , () {
109110 void checkNotification (MessageFcmMessage data, {
111+ required List <MessageFcmMessage > messageStyleMessages,
110112 required String expectedTitle,
111113 required String expectedTagComponent,
114+ required bool expectedIsGroupConversation,
112115 }) {
113116 final expectedTag = '${data .realmUri }|${data .userId }|$expectedTagComponent ' ;
114117 final expectedGroupKey = '${data .realmUri }|${data .userId }' ;
115118 final expectedId =
116119 NotificationDisplayManager .notificationIdAsHashOf (expectedTag);
117120 const expectedIntentFlags =
118121 PendingIntentFlag .immutable | PendingIntentFlag .updateCurrent;
122+ final expectedSelfUserKey = '${data .realmUri }|${data .userId }' ;
123+
124+ final messageStyleMessagesChecks =
125+ messageStyleMessages.mapIndexed ((i, messageData) {
126+ assert (messageData.realmUri == data.realmUri);
127+ assert (messageData.userId == data.userId);
128+
129+ final expectedSenderKey =
130+ '${messageData .realmUri }|${messageData .senderId }' ;
131+ final isLast = i == (messageStyleMessages.length - 1 );
132+ return (Subject <Object ?> it) => it.isA <MessagingStyleMessage >()
133+ ..text.equals (messageData.content)
134+ ..timestampMs.equals (messageData.time * 1000 )
135+ ..person.which ((it) => it.isNotNull ()
136+ ..iconBitmap.which ((it) => isLast ? it.isNotNull () : it.isNull ())
137+ ..key.equals (expectedSenderKey)
138+ ..name.equals (messageData.senderFullName));
139+ });
119140
120141 check (testBinding.androidNotificationHost.takeNotifyCalls ())
121- ..length.equals (2 )
122- ..containsInOrder (< Condition <AndroidNotificationHostApiNotifyCall >> [
123- (it) => it
142+ .deepEquals (< Condition <Object ?>> [
143+ (it) => it.isA <AndroidNotificationHostApiNotifyCall >()
124144 ..id.equals (expectedId)
125145 ..tag.equals (expectedTag)
126146 ..channelId.equals (NotificationChannelManager .kChannelId)
127- ..contentTitle.equals (expectedTitle)
128- ..contentText.equals (data.content)
147+ ..contentTitle.isNull ()
148+ ..contentText.isNull ()
149+ ..messagingStyle.which ((it) => it.isNotNull ()
150+ ..user.which ((it) => it
151+ ..iconBitmap.isNull ()
152+ ..key.equals (expectedSelfUserKey)
153+ ..name.equals ('You' )) // TODO(i18n)
154+ ..isGroupConversation.equals (expectedIsGroupConversation)
155+ ..conversationTitle.equals (expectedTitle)
156+ ..messages.deepEquals (messageStyleMessagesChecks))
157+ ..number.equals (messageStyleMessages.length)
129158 ..color.equals (kZulipBrandColor.value)
130159 ..smallIconResourceName.equals ('zulip_notification' )
131160 ..extras.isNull ()
@@ -137,7 +166,7 @@ void main() {
137166 ..requestCode.equals (expectedId)
138167 ..flags.equals (expectedIntentFlags)
139168 ..intentPayload.equals (jsonEncode (data.toJson ()))),
140- (it) => it
169+ (it) => it. isA < AndroidNotificationHostApiNotifyCall >()
141170 ..id.equals (NotificationDisplayManager .notificationIdAsHashOf (expectedGroupKey))
142171 ..tag.equals (expectedGroupKey)
143172 ..channelId.equals (NotificationChannelManager .kChannelId)
@@ -151,13 +180,14 @@ void main() {
151180 ..inboxStyle.which ((it) => it.isNotNull ()
152181 ..summaryText.equals (data.realmUri.toString ()))
153182 ..autoCancel.equals (true )
154- ..contentIntent.isNull ()
183+ ..contentIntent.isNull (),
155184 ]);
156185 }
157186
158187 Future <void > checkNotifications (FakeAsync async , MessageFcmMessage data, {
159188 required String expectedTitle,
160189 required String expectedTagComponent,
190+ required bool expectedIsGroupConversation,
161191 }) async {
162192 // We could just call `NotificationDisplayManager.onFcmMessage`.
163193 // But this way is cheap, and it provides our test coverage of
@@ -166,30 +196,81 @@ void main() {
166196 testBinding.firebaseMessaging.onMessage.add (
167197 RemoteMessage (data: data.toJson ()));
168198 async .flushMicrotasks ();
169- checkNotification (data, expectedTitle: expectedTitle,
199+ checkNotification (data,
200+ messageStyleMessages: [data],
201+ expectedIsGroupConversation: expectedIsGroupConversation,
202+ expectedTitle: expectedTitle,
170203 expectedTagComponent: expectedTagComponent);
204+ testBinding.androidNotificationHost.clearActiveNotifications ();
171205
172206 testBinding.firebaseMessaging.onBackgroundMessage.add (
173207 RemoteMessage (data: data.toJson ()));
174208 async .flushMicrotasks ();
175- checkNotification (data, expectedTitle: expectedTitle,
209+ checkNotification (data,
210+ messageStyleMessages: [data],
211+ expectedIsGroupConversation: expectedIsGroupConversation,
212+ expectedTitle: expectedTitle,
176213 expectedTagComponent: expectedTagComponent);
177214 }
178215
216+ Future <void > receiveFcmMessage (FakeAsync async , MessageFcmMessage data) async {
217+ testBinding.firebaseMessaging.onMessage.add (
218+ RemoteMessage (data: data.toJson ()));
219+ async .flushMicrotasks ();
220+ }
221+
179222 test ('stream message' , () => awaitFakeAsync ((async ) async {
180223 await init ();
181224 final stream = eg.stream ();
182225 final message = eg.streamMessage (stream: stream);
183226 await checkNotifications (async , messageFcmMessage (message, streamName: stream.name),
227+ expectedIsGroupConversation: true ,
184228 expectedTitle: '#${stream .name } > ${message .topic }' ,
185229 expectedTagComponent: 'stream:${message .streamId }:${message .topic }' );
186230 }));
187231
188- test ('stream message, stream name omitted' , () => awaitFakeAsync ((async ) async {
232+ test ('stream message: multiple messages, same topic' , () => awaitFakeAsync ((async ) async {
233+ await init ();
234+ final stream = eg.stream ();
235+ const topic = 'topic 1' ;
236+ final message1 = eg.streamMessage (topic: topic, stream: stream);
237+ final data1 = messageFcmMessage (message1, streamName: stream.name);
238+ final message2 = eg.streamMessage (topic: topic, stream: stream);
239+ final data2 = messageFcmMessage (message2, streamName: stream.name);
240+ final message3 = eg.streamMessage (topic: topic, stream: stream);
241+ final data3 = messageFcmMessage (message3, streamName: stream.name);
242+
243+ final expectedTitle = '#${stream .name } > $topic ' ;
244+ final expectedTagComponent = 'stream:${stream .streamId }:$topic ' ;
245+
246+ await receiveFcmMessage (async , data1);
247+ checkNotification (data1,
248+ messageStyleMessages: [data1],
249+ expectedIsGroupConversation: true ,
250+ expectedTitle: expectedTitle,
251+ expectedTagComponent: expectedTagComponent);
252+
253+ await receiveFcmMessage (async , data2);
254+ checkNotification (data2,
255+ messageStyleMessages: [data1, data2],
256+ expectedIsGroupConversation: true ,
257+ expectedTitle: expectedTitle,
258+ expectedTagComponent: expectedTagComponent);
259+
260+ await receiveFcmMessage (async , data3);
261+ checkNotification (data3,
262+ messageStyleMessages: [data1, data2, data3],
263+ expectedIsGroupConversation: true ,
264+ expectedTitle: expectedTitle,
265+ expectedTagComponent: expectedTagComponent);
266+ }));
267+
268+ test ('stream message: stream name omitted' , () => awaitFakeAsync ((async ) async {
189269 await init ();
190270 final stream = eg.stream ();
191271 final message = eg.streamMessage (stream: stream);
192272 await checkNotifications (async , messageFcmMessage (message, streamName: null ),
273+ expectedIsGroupConversation: true ,
193274 expectedTitle: '#(unknown channel) > ${message .topic }' ,
194275 expectedTagComponent: 'stream:${message .streamId }:${message .topic }' );
195276 }));
@@ -198,6 +279,7 @@ void main() {
198279 await init ();
199280 final message = eg.dmMessage (from: eg.thirdUser, to: [eg.otherUser, eg.selfUser]);
200281 await checkNotifications (async , messageFcmMessage (message),
282+ expectedIsGroupConversation: true ,
201283 expectedTitle: "${eg .thirdUser .fullName } to you and 1 other" ,
202284 expectedTagComponent: 'dm:${message .allRecipientIds .join ("," )}' );
203285 }));
@@ -207,6 +289,7 @@ void main() {
207289 final message = eg.dmMessage (from: eg.thirdUser,
208290 to: [eg.otherUser, eg.selfUser, eg.fourthUser]);
209291 await checkNotifications (async , messageFcmMessage (message),
292+ expectedIsGroupConversation: true ,
210293 expectedTitle: "${eg .thirdUser .fullName } to you and 2 others" ,
211294 expectedTagComponent: 'dm:${message .allRecipientIds .join ("," )}' );
212295 }));
@@ -215,6 +298,7 @@ void main() {
215298 await init ();
216299 final message = eg.dmMessage (from: eg.otherUser, to: [eg.selfUser]);
217300 await checkNotifications (async , messageFcmMessage (message),
301+ expectedIsGroupConversation: false ,
218302 expectedTitle: eg.otherUser.fullName,
219303 expectedTagComponent: 'dm:${message .allRecipientIds .join ("," )}' );
220304 }));
@@ -223,6 +307,7 @@ void main() {
223307 await init ();
224308 final message = eg.dmMessage (from: eg.selfUser, to: []);
225309 await checkNotifications (async , messageFcmMessage (message),
310+ expectedIsGroupConversation: false ,
226311 expectedTitle: eg.selfUser.fullName,
227312 expectedTagComponent: 'dm:${message .allRecipientIds .join ("," )}' );
228313 }));
@@ -403,6 +488,8 @@ extension on Subject<AndroidNotificationHostApiNotifyCall> {
403488 Subject <String ?> get groupKey => has ((x) => x.groupKey, 'groupKey' );
404489 Subject <InboxStyle ?> get inboxStyle => has ((x) => x.inboxStyle, 'inboxStyle' );
405490 Subject <bool ?> get isGroupSummary => has ((x) => x.isGroupSummary, 'isGroupSummary' );
491+ Subject <MessagingStyle ?> get messagingStyle => has ((x) => x.messagingStyle, 'messagingStyle' );
492+ Subject <int ?> get number => has ((x) => x.number, 'number' );
406493 Subject <String ?> get smallIconResourceName => has ((x) => x.smallIconResourceName, 'smallIconResourceName' );
407494}
408495
@@ -415,3 +502,22 @@ extension on Subject<PendingIntent> {
415502extension on Subject <InboxStyle > {
416503 Subject <String > get summaryText => has ((x) => x.summaryText, 'summaryText' );
417504}
505+
506+ extension on Subject <MessagingStyle > {
507+ Subject <Person > get user => has ((x) => x.user, 'user' );
508+ Subject <String ?> get conversationTitle => has ((x) => x.conversationTitle, 'conversationTitle' );
509+ Subject <List <MessagingStyleMessage ?>> get messages => has ((x) => x.messages, 'messages' );
510+ Subject <bool > get isGroupConversation => has ((x) => x.isGroupConversation, 'isGroupConversation' );
511+ }
512+
513+ extension on Subject <Person > {
514+ Subject <Uint8List ?> get iconBitmap => has ((x) => x.iconBitmap, 'iconBitmap' );
515+ Subject <String > get key => has ((x) => x.key, 'key' );
516+ Subject <String > get name => has ((x) => x.name, 'name' );
517+ }
518+
519+ extension on Subject <MessagingStyleMessage > {
520+ Subject <String > get text => has ((x) => x.text, 'text' );
521+ Subject <int > get timestampMs => has ((x) => x.timestampMs, 'timestampMs' );
522+ Subject <Person > get person => has ((x) => x.person, 'person' );
523+ }
0 commit comments