diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 8d7e0e9c02..b7c637cd62 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -15,6 +15,7 @@ 🐞 Fixed +- Fixed `StreamMessageComposer` throwing when its channel is disposed while it still holds unsent text, for example when the user is disconnected with a channel page open. - Fixed a message that failed to send, update or delete hiding the timestamp and delivery status of the message before it. - Fixed a voice recording sent as a reply without text showing inside a filled container instead of directly on the message bubble. - Fixed a mention rendering as another mentioned user's shorter mention followed by leftover text, such as `@Anna` next to `@Ann`. diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart index bfe65f34d3..72a9acd2e5 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart @@ -1730,6 +1730,9 @@ class DefaultStreamMessageComposerState extends State pumpComposer( + Future pumpComposer( WidgetTester tester, { required StreamMessageComposerController composerController, required Stream draftStream, @@ -72,6 +72,8 @@ void main() { // Let the post-frame callback fire and set up the draft stream listener // before the test starts emitting events. await tester.pumpAndSettle(); + + return channel; } group('draft stream', () { @@ -198,4 +200,41 @@ void main() { expect(composerController.text, isEmpty); }); }); + + group('when the composer is removed with unsent text', () { + testWidgets('saves the text as a draft', (tester) async { + final composerController = StreamMessageComposerController()..text = 'unsent'; + addTearDown(composerController.dispose); + + final channel = await pumpComposer( + tester, + composerController: composerController, + draftStream: const Stream.empty(), + ); + + await tester.pumpWidget(const SizedBox()); + + verify(() => channel.createDraft(any())).called(1); + }); + + testWidgets('does not throw once the channel is disposed', (tester) async { + final composerController = StreamMessageComposerController()..text = 'unsent'; + addTearDown(composerController.dispose); + + final channel = await pumpComposer( + tester, + composerController: composerController, + draftStream: const Stream.empty(), + ); + + // A disposed channel has no state, and throws on any call that needs it. + when(() => channel.state).thenReturn(null); + when(() => channel.createDraft(any())).thenThrow(StateError('disposed')); + + await tester.pumpWidget(const SizedBox()); + + expect(tester.takeException(), isNull); + verifyNever(() => channel.createDraft(any())); + }); + }); }