@@ -2,12 +2,15 @@ import 'package:checks/checks.dart';
2
2
import 'package:flutter_test/flutter_test.dart' ;
3
3
import 'package:zulip/api/model/model.dart' ;
4
4
import 'package:zulip/model/recent_senders.dart' ;
5
+ import 'package:zulip/model/store.dart' ;
5
6
import '../example_data.dart' as eg;
7
+ import 'test_store.dart' ;
6
8
7
9
/// [messages] should be sorted by [id] ascending.
8
10
void checkMatchesMessages (RecentSenders model, List <Message > messages) {
9
11
final Map <int , Map <int , Set <int >>> messagesByUserInStream = {};
10
12
final Map <int , Map <TopicName , Map <int , Set <int >>>> messagesByUserInTopic = {};
13
+ messages.sort ((a, b) => a.id - b.id);
11
14
for (final message in messages) {
12
15
if (message is ! StreamMessage ) {
13
16
throw UnsupportedError ('Message of type ${message .runtimeType } is not expected.' );
@@ -142,6 +145,164 @@ void main() {
142
145
});
143
146
});
144
147
148
+ group ('RecentSenders.handleUpdateMessageEvent' , () {
149
+ late PerAccountStore store;
150
+ late RecentSenders model;
151
+
152
+ final origChannel = eg.stream (); final newChannel = eg.stream ();
153
+ final origTopic = 'origTopic' ; final newTopic = 'newTopic' ;
154
+ final userX = eg.user (); final userY = eg.user ();
155
+
156
+ Future <void > prepare (List <Message > messages) async {
157
+ store = eg.store ();
158
+ await store.addMessages (messages);
159
+ await store.addStreams ([origChannel, newChannel]);
160
+ await store.addUsers ([userX, userY]);
161
+ model = store.recentSenders;
162
+ }
163
+
164
+ List <StreamMessage > copyMessagesWith (Iterable <StreamMessage > messages, {
165
+ ZulipStream ? newChannel,
166
+ String ? newTopic,
167
+ }) {
168
+ assert (newChannel != null || newTopic != null );
169
+ return messages.map ((message) => StreamMessage .fromJson (
170
+ message.toJson ()
171
+ ..['stream_id' ] = newChannel? .streamId ?? message.streamId
172
+ // See [StreamMessage.displayRecipient] for why this is needed.
173
+ ..['display_recipient' ] = newChannel? .name ?? message.displayRecipient!
174
+
175
+ ..['subject' ] = newTopic ?? message.topic
176
+ )).toList ();
177
+ }
178
+
179
+ test ('move a conversation entirely, with additional unknown messages' , () async {
180
+ final messages = List .generate (10 , (i) => eg.streamMessage (
181
+ stream: origChannel, topic: origTopic, sender: userX));
182
+ await prepare (messages);
183
+ final unknownMessages = List .generate (10 , (i) => eg.streamMessage (
184
+ stream: origChannel, topic: origTopic, sender: userX));
185
+ checkMatchesMessages (model, messages);
186
+
187
+ final messageIdsByUserInStreamBefore =
188
+ model.streamSenders[origChannel.streamId]! [userX.userId]! .ids;
189
+ final messageIdsByUserInTopicBefore =
190
+ model.topicSenders[origChannel.streamId]! [eg.t (origTopic)]! [userX.userId]! .ids;
191
+
192
+ await store.handleEvent (eg.updateMessageEventMoveFrom (
193
+ origMessages: messages + unknownMessages,
194
+ newStreamId: newChannel.streamId));
195
+ checkMatchesMessages (model, copyMessagesWith (
196
+ messages, newChannel: newChannel));
197
+
198
+ // Check we avoided creating a new list for the moved message IDs.
199
+ check (messageIdsByUserInStreamBefore).identicalTo (
200
+ model.streamSenders[newChannel.streamId]! [userX.userId]! .ids);
201
+ check (messageIdsByUserInTopicBefore).identicalTo (
202
+ model.topicSenders[newChannel.streamId]! [eg.t (origTopic)]! [userX.userId]! .ids);
203
+ });
204
+
205
+ test ('move a conversation exactly' , () async {
206
+ final messages = List .generate (10 , (i) => eg.streamMessage (
207
+ stream: origChannel, topic: origTopic, sender: userX));
208
+ await prepare (messages);
209
+
210
+ final messageIdsByUserInStreamBefore =
211
+ model.streamSenders[origChannel.streamId]! [userX.userId]! .ids;
212
+ final messageIdsByUserInTopicBefore =
213
+ model.topicSenders[origChannel.streamId]! [eg.t (origTopic)]! [userX.userId]! .ids;
214
+
215
+ await store.handleEvent (eg.updateMessageEventMoveFrom (
216
+ origMessages: messages,
217
+ newStreamId: newChannel.streamId,
218
+ newTopicStr: newTopic));
219
+ checkMatchesMessages (model, copyMessagesWith (
220
+ messages, newChannel: newChannel, newTopic: newTopic));
221
+
222
+ // Check we avoided creating a new list for the moved message IDs.
223
+ check (messageIdsByUserInStreamBefore).identicalTo (
224
+ model.streamSenders[newChannel.streamId]! [userX.userId]! .ids);
225
+ check (messageIdsByUserInTopicBefore).identicalTo (
226
+ model.topicSenders[newChannel.streamId]! [eg.t (newTopic)]! [userX.userId]! .ids);
227
+ });
228
+
229
+ test ('move a conversation partially to a different channel' , () async {
230
+ final messages = List .generate (10 , (i) => eg.streamMessage (
231
+ stream: origChannel, topic: origTopic));
232
+ final movedMessages = messages.take (5 ).toList ();
233
+ final otherMessages = messages.skip (5 );
234
+ await prepare (messages);
235
+
236
+ await store.handleEvent (eg.updateMessageEventMoveFrom (
237
+ origMessages: movedMessages,
238
+ newStreamId: newChannel.streamId));
239
+ checkMatchesMessages (model, [
240
+ ...copyMessagesWith (movedMessages, newChannel: newChannel),
241
+ ...otherMessages,
242
+ ]);
243
+ });
244
+
245
+ test ('move a conversation partially to a different topic, within the same channel' , () async {
246
+ final messages = List .generate (10 , (i) => eg.streamMessage (
247
+ stream: origChannel, topic: origTopic, sender: userX));
248
+ final movedMessages = messages.take (5 ).toList ();
249
+ final otherMessages = messages.skip (5 );
250
+ await prepare (messages);
251
+
252
+ final messageIdsByUserInStreamBefore =
253
+ model.streamSenders[origChannel.streamId]! [userX.userId]! .ids;
254
+
255
+ await store.handleEvent (eg.updateMessageEventMoveFrom (
256
+ origMessages: movedMessages,
257
+ newTopicStr: newTopic));
258
+ checkMatchesMessages (model, [
259
+ ...copyMessagesWith (movedMessages, newTopic: newTopic),
260
+ ...otherMessages,
261
+ ]);
262
+
263
+ // Check that we did not touch stream message IDs tracker
264
+ // when there wasn't a stream move.
265
+ check (messageIdsByUserInStreamBefore).identicalTo (
266
+ model.streamSenders[origChannel.streamId]! [userX.userId]! .ids);
267
+ });
268
+
269
+ test ('move a conversation with multiple senders' , () async {
270
+ final messages = [
271
+ eg.streamMessage (stream: origChannel, topic: origTopic, sender: userX),
272
+ eg.streamMessage (stream: origChannel, topic: origTopic, sender: userX),
273
+ eg.streamMessage (stream: origChannel, topic: origTopic, sender: userY),
274
+ ];
275
+ await prepare (messages);
276
+
277
+ await store.handleEvent (eg.updateMessageEventMoveFrom (
278
+ origMessages: messages,
279
+ newStreamId: newChannel.streamId));
280
+ checkMatchesMessages (model, copyMessagesWith (
281
+ messages, newChannel: newChannel));
282
+ });
283
+
284
+ test ('move a converstion, but message IDs from the event are not sorted in ascending order' , () async {
285
+ final messages = List .generate (10 , (i) => eg.streamMessage (
286
+ id: 100 - i, stream: origChannel, topic: origTopic));
287
+ await prepare (messages);
288
+
289
+ await store.handleEvent (eg.updateMessageEventMoveFrom (
290
+ origMessages: messages,
291
+ newStreamId: newChannel.streamId));
292
+ checkMatchesMessages (model,
293
+ copyMessagesWith (messages, newChannel: newChannel));
294
+ });
295
+
296
+ test ('message edit update without move' , () async {
297
+ final messages = List .generate (10 , (i) => eg.streamMessage (
298
+ stream: origChannel, topic: origTopic));
299
+ await prepare (messages);
300
+
301
+ await store.handleEvent (eg.updateMessageEditEvent (messages[0 ]));
302
+ checkMatchesMessages (model, messages);
303
+ });
304
+ });
305
+
145
306
test ('RecentSenders.handleDeleteMessageEvent' , () {
146
307
final model = RecentSenders ();
147
308
final stream = eg.stream ();
0 commit comments