@@ -36,7 +36,7 @@ void main() {
3636
3737 // These "late" variables are the common state operated on by each test.
3838 // Each test case calls [prepare] to initialize them.
39- late Subscription subscription;
39+ late Subscription ? subscription;
4040 late PerAccountStore store;
4141 late FakeApiConnection connection;
4242 // [messageList] is here only for the sake of checking when it notifies.
@@ -54,15 +54,18 @@ void main() {
5454 /// Initialize [store] and the rest of the test state.
5555 Future <void > prepare ({
5656 ZulipStream ? stream,
57+ bool isChannelSubscribed = true ,
5758 int ? zulipFeatureLevel,
5859 }) async {
5960 stream ?? = eg.stream (streamId: eg.defaultStreamMessageStreamId);
60- subscription = eg.subscription (stream);
6161 final selfAccount = eg.selfAccount.copyWith (zulipFeatureLevel: zulipFeatureLevel);
6262 store = eg.store (account: selfAccount,
6363 initialSnapshot: eg.initialSnapshot (zulipFeatureLevel: zulipFeatureLevel));
6464 await store.addStream (stream);
65- await store.addSubscription (subscription);
65+ if (isChannelSubscribed) {
66+ subscription = eg.subscription (stream);
67+ await store.addSubscription (subscription! );
68+ }
6669 connection = store.connection as FakeApiConnection ;
6770 notifiedCount = 0 ;
6871 messageList = MessageListView .init (store: store,
@@ -533,18 +536,132 @@ void main() {
533536 });
534537 });
535538
536- test ('on ID collision, new message does not clobber old in store.messages' , () async {
537- await prepare ();
538- final message = eg.streamMessage (id: 1 , content: '<p>foo</p>' );
539- await addMessages ([message]);
540- check (store.messages).deepEquals ({1 : message});
541- final newMessage = eg.streamMessage (id: 1 , content: '<p>bar</p>' );
542- final messages = [newMessage];
543- store.reconcileMessages (messages);
544- check (messages).deepEquals (
545- // (We'll check more messages in an upcoming commit.)
546- [message].map (conditionIdentical));
547- check (store.messages).deepEquals ({1 : message});
539+ group ('fetched message with ID already in store.messages' , () {
540+ late Message messageCopy;
541+
542+ /// Makes a copy of the single message in [MessageStore.messages]
543+ /// by round-tripping through [Message.fromJson] and [Message.toJson] .
544+ ///
545+ /// If that message's [StreamMessage.conversation.displayRecipient]
546+ /// is null, callers must provide a non-null [displayRecipient]
547+ /// to allow [StreamConversation.fromJson] to complete without throwing.
548+ Message copyStoredMessage ({String ? displayRecipient}) {
549+ final message = store.messages.values.single;
550+
551+ Map <String , dynamic > json = message.toJson ();
552+ if (
553+ message is StreamMessage
554+ && message.conversation.displayRecipient == null
555+ ) {
556+ if (displayRecipient == null ) throw ArgumentError ();
557+ json['display_recipient' ] = displayRecipient;
558+ }
559+
560+ return Message .fromJson (json);
561+ }
562+
563+ /// Checks if the single message in [MessageStore.messages]
564+ /// is identical to [message] .
565+ void checkStoredMessageIdenticalTo (Message message) {
566+ check (store.messages)
567+ .deepEquals ({message.id: conditionIdentical (message)});
568+ }
569+
570+ test ('DM' , () async {
571+ await prepare ();
572+ final message = eg.dmMessage (id: 1 , from: eg.otherUser, to: [eg.selfUser]);
573+
574+ store.reconcileMessages ([message]);
575+ checkStoredMessageIdenticalTo (message);
576+ store.reconcileMessages ([copyStoredMessage ()]);
577+ // Not clobbering, because the first call didn't mark stale.
578+ checkStoredMessageIdenticalTo (message);
579+ });
580+
581+ group ('channel message; chooses correctly whether to clobber the stored version' , () {
582+ // Exercise the ways we move the message in and out of the "maybe stale"
583+ // state. These include reconcileMessage itself, so sometimes we test
584+ // repeated calls to that with nothing else happening in between.
585+
586+ test ('various conditions' , () async {
587+ final channel = eg.stream ();
588+ await prepare (stream: channel, isChannelSubscribed: true );
589+ final message = eg.streamMessage (id: 1 , stream: channel);
590+
591+ final otherChannel = eg.stream ();
592+ await store.addStream (otherChannel);
593+
594+ store.reconcileMessages ([message]);
595+ checkStoredMessageIdenticalTo (message);
596+ store.reconcileMessages ([copyStoredMessage ()]);
597+ // Not clobbering, because the first call didn't mark stale,
598+ // because the message was in a subscribed channel.
599+ checkStoredMessageIdenticalTo (message);
600+
601+ await store.removeSubscription (channel.streamId);
602+ messageCopy = copyStoredMessage ();
603+ store.reconcileMessages ([messageCopy]);
604+ // Clobbering because the unsubscribe event marked the message stale.
605+ checkStoredMessageIdenticalTo (messageCopy);
606+ messageCopy = copyStoredMessage ();
607+ store.reconcileMessages ([messageCopy]);
608+ // (Check that reconcileMessage itself didn't unmark as stale.)
609+ checkStoredMessageIdenticalTo (messageCopy);
610+
611+ await store.addSubscription (eg.subscription (channel));
612+ messageCopy = copyStoredMessage ();
613+ store.reconcileMessages ([messageCopy]);
614+ // The channel became subscribed,
615+ // but the message's data hasn't been refreshed, so clobber…
616+ checkStoredMessageIdenticalTo (messageCopy);
617+
618+ store.reconcileMessages ([copyStoredMessage ()]);
619+ // …Now it's been refreshed, by reconcileMessages, so don't clobber.
620+ checkStoredMessageIdenticalTo (messageCopy);
621+
622+ check (store.subscriptions[otherChannel.streamId]).isNull ();
623+ await store.handleEvent (
624+ eg.updateMessageEventMoveFrom (origMessages: [message],
625+ newStreamId: otherChannel.streamId));
626+ messageCopy = copyStoredMessage (displayRecipient: otherChannel.name);
627+ store.reconcileMessages ([messageCopy]);
628+ // Message was moved to an unsubscribed channel, so clobber.
629+ checkStoredMessageIdenticalTo (messageCopy);
630+ messageCopy = copyStoredMessage ();
631+ store.reconcileMessages ([messageCopy]);
632+ // (Check that reconcileMessage itself didn't unmark as stale.)
633+ checkStoredMessageIdenticalTo (messageCopy);
634+ });
635+
636+ test ('in unsubscribed channel on first call' , () async {
637+ await prepare (isChannelSubscribed: false );
638+ final message = eg.streamMessage (id: 1 );
639+
640+ store.reconcileMessages ([message]);
641+ checkStoredMessageIdenticalTo (message);
642+
643+ messageCopy = copyStoredMessage ();
644+ store.reconcileMessages ([messageCopy]);
645+ checkStoredMessageIdenticalTo (messageCopy);
646+ messageCopy = copyStoredMessage ();
647+ store.reconcileMessages ([messageCopy]);
648+ checkStoredMessageIdenticalTo (messageCopy);
649+ });
650+
651+ test ('new-message event when in unsubscribed channel' , () async {
652+ await prepare (isChannelSubscribed: false );
653+ final message = eg.streamMessage (id: 1 );
654+
655+ await store.handleEvent (eg.messageEvent (message));
656+
657+ messageCopy = copyStoredMessage ();
658+ store.reconcileMessages ([messageCopy]);
659+ checkStoredMessageIdenticalTo (messageCopy);
660+ messageCopy = copyStoredMessage ();
661+ store.reconcileMessages ([messageCopy]);
662+ checkStoredMessageIdenticalTo (messageCopy);
663+ });
664+ });
548665 });
549666
550667 test ('matchContent and matchTopic are removed' , () async {
0 commit comments