@@ -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,20 @@ 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+ } else {
69+ subscription = null ;
70+ }
6671 connection = store.connection as FakeApiConnection ;
6772 notifiedCount = 0 ;
6873 messageList = MessageListView .init (store: store,
@@ -533,18 +538,130 @@ void main() {
533538 });
534539 });
535540
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});
541+ group ('fetched message with ID already in store.messages' , () {
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+ final 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+ Message checkClobber ({Message ? withMessageCopy}) {
587+ final messageCopy = withMessageCopy ?? copyStoredMessage ();
588+ store.reconcileMessages ([messageCopy]);
589+ checkStoredMessageIdenticalTo (messageCopy);
590+ return messageCopy;
591+ }
592+
593+ void checkNoClobber ({required Message messageBefore}) {
594+ store.reconcileMessages ([copyStoredMessage ()]);
595+ checkStoredMessageIdenticalTo (messageBefore);
596+ }
597+
598+ test ('various conditions' , () async {
599+ final channel = eg.stream ();
600+ await prepare (stream: channel, isChannelSubscribed: true );
601+ final message = eg.streamMessage (id: 1 , stream: channel);
602+
603+ final otherChannel = eg.stream ();
604+ await store.addStream (otherChannel);
605+
606+ store.reconcileMessages ([message]);
607+ checkStoredMessageIdenticalTo (message);
608+ // Not clobbering, because the first call didn't mark stale,
609+ // because the message was in a subscribed channel.
610+ checkNoClobber (messageBefore: message);
611+
612+ await store.removeSubscription (channel.streamId);
613+ // Clobbering because the unsubscribe event marked the message stale.
614+ Message messageCopy = checkClobber ();
615+ // (Check that reconcileMessage itself didn't unmark as stale.)
616+ messageCopy = checkClobber ();
617+
618+ await store.addSubscription (eg.subscription (channel));
619+ // The channel became subscribed,
620+ // but the message's data hasn't been refreshed, so clobber…
621+ messageCopy = checkClobber ();
622+
623+ // …Now it's been refreshed, by reconcileMessages, so don't clobber.
624+ checkNoClobber (messageBefore: messageCopy);
625+
626+ check (store.subscriptions[otherChannel.streamId]).isNull ();
627+ await store.handleEvent (
628+ eg.updateMessageEventMoveFrom (origMessages: [message],
629+ newStreamId: otherChannel.streamId));
630+ // Message was moved to an unsubscribed channel, so clobber.
631+ messageCopy = checkClobber (
632+ withMessageCopy: copyStoredMessage (displayRecipient: otherChannel.name));
633+ // (Check that reconcileMessage itself didn't unmark as stale.)
634+ messageCopy = checkClobber ();
635+
636+ // Subscribe, to mark message as not-stale, setting up another check…
637+ await store.addSubscription (eg.subscription (otherChannel));
638+
639+ await store.handleEvent (ChannelDeleteEvent (id: 1 , streams: [otherChannel]));
640+ // Message was in a channel that became unknown, so clobber.
641+ checkClobber ();
642+ });
643+
644+ test ('in unsubscribed channel on first call' , () async {
645+ await prepare (isChannelSubscribed: false );
646+ final message = eg.streamMessage (id: 1 );
647+
648+ store.reconcileMessages ([message]);
649+ checkStoredMessageIdenticalTo (message);
650+
651+ checkClobber ();
652+ checkClobber ();
653+ });
654+
655+ test ('new-message event when in unsubscribed channel' , () async {
656+ await prepare (isChannelSubscribed: false );
657+ final message = eg.streamMessage (id: 1 );
658+
659+ await store.handleEvent (eg.messageEvent (message));
660+
661+ checkClobber ();
662+ checkClobber ();
663+ });
664+ });
548665 });
549666
550667 test ('matchContent and matchTopic are removed' , () async {
0 commit comments