From a42b3de6f401645a6a9b40428b60d276e4d5ac44 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Thu, 8 Apr 2021 21:13:08 +0530 Subject: [PATCH] test: add tests for stream chat flutter (#335) * feat: Added tests * feat: Added header test * feat: Added new tests * fix * feat: Added test, fmt * feat: Added delete test * added system and unread ind tests * fix: analysis * added new tests * fix: analysis * added tests * add attachment actions modal tests * fix analysis * fix analysis * add backbutton tests * fix analysis * increase timeout * add channelheader tests * add infotile tests * add reactions modal and channel unread indicator tests * add golden test for reaction bubble * add dark tests * add system message golden * add deletd message golden * add message action modal tests * update goldens * update workflow runner * update goldens * remove channel unread indicator in favor of unread indicator * move file and media screen to sample app * add channel image tesst * add channel_list_header test * add thread_header test * bump up test threashold * fix review Co-authored-by: Salvatore Giordano --- .github/workflows/stream_flutter_workflow.yml | 4 +- .../lib/src/models/attachment.dart | 85 +-- .../stream_chat/lib/src/models/message.dart | 36 +- packages/stream_chat/lib/stream_chat.dart | 1 + packages/stream_chat/pubspec.yaml | 1 + packages/stream_chat_flutter/.gitignore | 5 +- .../lib/src/attachment_actions_modal.dart | 24 +- .../lib/src/channel_file_display_screen.dart | 182 ----- .../lib/src/channel_image.dart | 259 ++++--- .../lib/src/channel_media_display_screen.dart | 251 ------- .../lib/src/channel_preview.dart | 28 +- .../lib/src/channel_unread_indicator.dart | 49 -- .../lib/src/info_tile.dart | 17 +- .../lib/src/message_actions_modal.dart | 14 +- .../lib/src/message_input.dart | 12 +- .../lib/src/message_reactions_modal.dart | 4 - .../lib/src/message_widget.dart | 5 +- .../lib/src/reaction_bubble.dart | 114 +-- .../lib/src/unread_indicator.dart | 2 +- .../lib/stream_chat_flutter.dart | 27 +- packages/stream_chat_flutter/pubspec.yaml | 5 +- .../test/flutter_test_config.dart | 9 + .../src/attachment_actions_modal_test.dart | 510 +++++++++++++ .../test/src/attachment_widgets_test.dart | 50 ++ .../test/src/back_button_test.dart | 144 ++++ .../test/src/channel_header_test.dart | 388 ++++++++++ .../test/src/channel_image_test.dart | 263 +++++++ .../test/src/channel_list_header_test.dart | 207 ++++++ .../test/src/channel_name_test.dart | 73 ++ .../test/src/channel_preview_test.dart | 4 + .../test/src/date_divider_test.dart | 32 + .../test/src/deleted_message_test.dart | 201 +++++ .../test/src/full_screen_media_test.dart | 91 +++ .../src/goldens/deleted_message_custom.png | Bin 0 -> 2936 bytes .../test/src/goldens/deleted_message_dark.png | Bin 0 -> 2785 bytes .../src/goldens/deleted_message_light.png | Bin 0 -> 2750 bytes .../test/src/goldens/reaction_bubble_0.png | Bin 0 -> 410 bytes .../test/src/goldens/reaction_bubble_2.png | Bin 0 -> 3339 bytes .../src/goldens/reaction_bubble_3_dark.png | Bin 0 -> 1976 bytes .../src/goldens/reaction_bubble_3_light.png | Bin 0 -> 2067 bytes .../src/goldens/reaction_bubble_like_dark.png | Bin 0 -> 1494 bytes .../goldens/reaction_bubble_like_light.png | Bin 0 -> 1291 bytes .../test/src/goldens/system_message_dark.png | Bin 0 -> 1812 bytes .../test/src/goldens/system_message_light.png | Bin 0 -> 1866 bytes .../test/src/image_footer_test.dart | 50 ++ .../test/src/info_tile_test.dart | 69 ++ .../test/src/message_action_modal_test.dart | 701 +++++++++++++++++- .../test/src/message_input_test.dart | 81 ++ .../test/src/message_reaction_modal_test.dart | 102 --- .../src/message_reactions_modal_test.dart | 148 ++++ .../test/src/message_text_test.dart | 54 ++ .../stream_chat_flutter/test/src/mocks.dart | 7 + .../test/src/reaction_bubble_test.dart | 270 +++++-- .../test/src/simple_frame.dart | 19 + .../test/src/system_message_test.dart | 164 ++++ .../test/src/thread_header_test.dart | 140 ++++ .../test/src/typing_indicator_test.dart | 79 ++ .../test/src/unread_indicator_test.dart | 127 ++++ 58 files changed, 4140 insertions(+), 968 deletions(-) delete mode 100644 packages/stream_chat_flutter/lib/src/channel_file_display_screen.dart delete mode 100644 packages/stream_chat_flutter/lib/src/channel_media_display_screen.dart delete mode 100644 packages/stream_chat_flutter/lib/src/channel_unread_indicator.dart create mode 100644 packages/stream_chat_flutter/test/flutter_test_config.dart create mode 100644 packages/stream_chat_flutter/test/src/attachment_actions_modal_test.dart create mode 100644 packages/stream_chat_flutter/test/src/attachment_widgets_test.dart create mode 100644 packages/stream_chat_flutter/test/src/back_button_test.dart create mode 100644 packages/stream_chat_flutter/test/src/channel_header_test.dart create mode 100644 packages/stream_chat_flutter/test/src/channel_image_test.dart create mode 100644 packages/stream_chat_flutter/test/src/channel_list_header_test.dart create mode 100644 packages/stream_chat_flutter/test/src/channel_name_test.dart create mode 100644 packages/stream_chat_flutter/test/src/date_divider_test.dart create mode 100644 packages/stream_chat_flutter/test/src/deleted_message_test.dart create mode 100644 packages/stream_chat_flutter/test/src/full_screen_media_test.dart create mode 100644 packages/stream_chat_flutter/test/src/goldens/deleted_message_custom.png create mode 100644 packages/stream_chat_flutter/test/src/goldens/deleted_message_dark.png create mode 100644 packages/stream_chat_flutter/test/src/goldens/deleted_message_light.png create mode 100644 packages/stream_chat_flutter/test/src/goldens/reaction_bubble_0.png create mode 100644 packages/stream_chat_flutter/test/src/goldens/reaction_bubble_2.png create mode 100644 packages/stream_chat_flutter/test/src/goldens/reaction_bubble_3_dark.png create mode 100644 packages/stream_chat_flutter/test/src/goldens/reaction_bubble_3_light.png create mode 100644 packages/stream_chat_flutter/test/src/goldens/reaction_bubble_like_dark.png create mode 100644 packages/stream_chat_flutter/test/src/goldens/reaction_bubble_like_light.png create mode 100644 packages/stream_chat_flutter/test/src/goldens/system_message_dark.png create mode 100644 packages/stream_chat_flutter/test/src/goldens/system_message_light.png create mode 100644 packages/stream_chat_flutter/test/src/image_footer_test.dart create mode 100644 packages/stream_chat_flutter/test/src/info_tile_test.dart create mode 100644 packages/stream_chat_flutter/test/src/message_input_test.dart delete mode 100644 packages/stream_chat_flutter/test/src/message_reaction_modal_test.dart create mode 100644 packages/stream_chat_flutter/test/src/message_reactions_modal_test.dart create mode 100644 packages/stream_chat_flutter/test/src/message_text_test.dart create mode 100644 packages/stream_chat_flutter/test/src/simple_frame.dart create mode 100644 packages/stream_chat_flutter/test/src/system_message_test.dart create mode 100644 packages/stream_chat_flutter/test/src/thread_header_test.dart create mode 100644 packages/stream_chat_flutter/test/src/typing_indicator_test.dart create mode 100644 packages/stream_chat_flutter/test/src/unread_indicator_test.dart diff --git a/.github/workflows/stream_flutter_workflow.yml b/.github/workflows/stream_flutter_workflow.yml index aa621854..7b5df2ba 100644 --- a/.github/workflows/stream_flutter_workflow.yml +++ b/.github/workflows/stream_flutter_workflow.yml @@ -58,7 +58,7 @@ jobs: ./.github/workflows/scripts/validate-formatting.sh test: - runs-on: ubuntu-latest + runs-on: macos-latest timeout-minutes: 15 steps: - uses: actions/checkout@v2 @@ -102,4 +102,4 @@ jobs: - uses: VeryGoodOpenSource/very_good_coverage@v1.1.1 with: path: packages/stream_chat_flutter/coverage/lcov.info - min_coverage: 16 + min_coverage: 35 diff --git a/packages/stream_chat/lib/src/models/attachment.dart b/packages/stream_chat/lib/src/models/attachment.dart index de5149db..548a3e6d 100644 --- a/packages/stream_chat/lib/src/models/attachment.dart +++ b/packages/stream_chat/lib/src/models/attachment.dart @@ -1,5 +1,6 @@ // ignore_for_file: public_member_api_docs +import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:stream_chat/src/models/action.dart'; import 'package:stream_chat/src/models/attachment_file.dart'; @@ -10,7 +11,7 @@ part 'attachment.g.dart'; /// The class that contains the information about an attachment @JsonSerializable(includeIfNull: false) -class Attachment { +class Attachment extends Equatable { /// Constructor used for json serialization Attachment({ String id, @@ -37,12 +38,11 @@ class Attachment { UploadState uploadState, }) : id = id ?? Uuid().v4(), title = title ?? file?.name, - localUri = file?.path != null ? Uri.parse(file.path) : null { - this.uploadState = uploadState ?? - ((assetUrl != null || imageUrl != null) - ? const UploadState.success() - : const UploadState.preparing()); - } + localUri = file?.path != null ? Uri.parse(file.path) : null, + uploadState = uploadState ?? + ((assetUrl != null || imageUrl != null) + ? const UploadState.success() + : const UploadState.preparing()); /// Create a new instance from a json factory Attachment.fromJson(Map json) => @@ -104,7 +104,7 @@ class Attachment { final AttachmentFile file; /// The current upload state of the attachment - UploadState uploadState; + final UploadState uploadState; /// Map of custom channel extraData @JsonKey(includeIfNull: false) @@ -205,49 +205,28 @@ class Attachment { ); @override - bool operator ==(Object other) => - identical(this, other) || - other is Attachment && - runtimeType == other.runtimeType && - type == other.type && - titleLink == other.titleLink && - title == other.title && - thumbUrl == other.thumbUrl && - text == other.text && - pretext == other.pretext && - ogScrapeUrl == other.ogScrapeUrl && - imageUrl == other.imageUrl && - footerIcon == other.footerIcon && - footer == other.footer && - fields == other.fields && - fallback == other.fallback && - color == other.color && - authorName == other.authorName && - authorLink == other.authorLink && - authorIcon == other.authorIcon && - assetUrl == other.assetUrl && - actions == other.actions && - extraData == other.extraData; - - @override - int get hashCode => - type.hashCode ^ - titleLink.hashCode ^ - title.hashCode ^ - thumbUrl.hashCode ^ - text.hashCode ^ - pretext.hashCode ^ - ogScrapeUrl.hashCode ^ - imageUrl.hashCode ^ - footerIcon.hashCode ^ - footer.hashCode ^ - fields.hashCode ^ - fallback.hashCode ^ - color.hashCode ^ - authorName.hashCode ^ - authorLink.hashCode ^ - authorIcon.hashCode ^ - assetUrl.hashCode ^ - actions.hashCode ^ - extraData.hashCode; + List get props => [ + id, + type, + titleLink, + title, + thumbUrl, + text, + pretext, + ogScrapeUrl, + imageUrl, + footerIcon, + footer, + fields, + fallback, + color, + authorName, + authorLink, + authorIcon, + assetUrl, + actions, + file, + uploadState, + extraData, + ]; } diff --git a/packages/stream_chat/lib/src/models/message.dart b/packages/stream_chat/lib/src/models/message.dart index a89324a7..28790b15 100644 --- a/packages/stream_chat/lib/src/models/message.dart +++ b/packages/stream_chat/lib/src/models/message.dart @@ -1,3 +1,4 @@ +import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:stream_chat/src/models/attachment.dart'; import 'package:stream_chat/src/models/reaction.dart'; @@ -41,7 +42,7 @@ enum MessageSendingStatus { /// The class that contains the information about a message @JsonSerializable() -class Message { +class Message extends Equatable { /// Constructor used for json serialization Message({ String id, @@ -345,6 +346,39 @@ class Message { pinnedBy: other.pinnedBy, ); } + + @override + List get props => [ + id, + text, + type, + attachments, + mentionedUsers, + reactionCounts, + reactionScores, + latestReactions, + ownReactions, + parentId, + quotedMessage, + quotedMessageId, + replyCount, + threadParticipants, + showInChannel, + shadowed, + silent, + command, + createdAt, + updatedAt, + deletedAt, + user, + pinned, + pinnedAt, + pinExpires, + pinnedBy, + extraData, + status, + skipPush, + ]; } /// A translated message diff --git a/packages/stream_chat/lib/stream_chat.dart b/packages/stream_chat/lib/stream_chat.dart index 0e1c9ed2..f491c39a 100644 --- a/packages/stream_chat/lib/stream_chat.dart +++ b/packages/stream_chat/lib/stream_chat.dart @@ -15,6 +15,7 @@ export './src/attachment_file_uploader.dart' show AttachmentFileUploader; export './src/client.dart'; export './src/db/chat_persistence_client.dart'; export './src/event_type.dart'; +export './src/exceptions.dart'; export './src/extensions/rate_limit.dart'; export './src/extensions/string_extension.dart'; export './src/models/action.dart'; diff --git a/packages/stream_chat/pubspec.yaml b/packages/stream_chat/pubspec.yaml index 24298ae1..7343b721 100644 --- a/packages/stream_chat/pubspec.yaml +++ b/packages/stream_chat/pubspec.yaml @@ -12,6 +12,7 @@ dependencies: async: ^2.4.2 collection: ^1.14.13 dio: ^3.0.10 + equatable: ^2.0.0 freezed_annotation: ^0.12.0 http_parser: ^3.1.4 json_annotation: ^3.0.1 diff --git a/packages/stream_chat_flutter/.gitignore b/packages/stream_chat_flutter/.gitignore index 1d3bae26..fe82edc9 100644 --- a/packages/stream_chat_flutter/.gitignore +++ b/packages/stream_chat_flutter/.gitignore @@ -62,4 +62,7 @@ doc/api/ fvm google-services.json example/ios/dist -.vscode/ \ No newline at end of file +.vscode/ + +# don't check in golden failure output +**/failures/*.png \ No newline at end of file diff --git a/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart b/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart index d9b9c016..0c8eac7d 100644 --- a/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart @@ -34,12 +34,12 @@ class AttachmentActionsModal extends StatelessWidget { /// Returns a new [AttachmentActionsModal] const AttachmentActionsModal({ + @required this.currentIndex, this.message, - this.currentIndex, this.onShowMessage, this.imageDownloader, this.fileDownloader, - }); + }) : assert(currentIndex != null, 'currentIndex cannot be null'); @override Widget build(BuildContext context) { @@ -145,19 +145,20 @@ class AttachmentActionsModal extends StatelessWidget { () { final channel = StreamChannel.of(context).channel; if (message.attachments.length > 1 || - message.text.isNotEmpty) { + message.text?.isNotEmpty == true) { final remainingAttachments = [...message.attachments] ..removeAt(currentIndex); channel.updateMessage(message.copyWith( attachments: remainingAttachments, )); - Navigator.pop(context); - Navigator.pop(context); + Navigator.of(context) + ..pop() + ..maybePop(); } else { - channel.deleteMessage(message).then((value) { - Navigator.pop(context); - Navigator.pop(context); - }); + channel.deleteMessage(message); + Navigator.of(context) + ..pop() + ..maybePop(); } }, color: theme.colorTheme.accentRed, @@ -185,8 +186,10 @@ class AttachmentActionsModal extends StatelessWidget { StreamSvgIcon icon, VoidCallback onTap, { Color color, + Key key, }) { return Material( + key: key, color: StreamChatTheme.of(context).colorTheme.white, child: InkWell( onTap: onTap, @@ -224,7 +227,7 @@ class AttachmentActionsModal extends StatelessWidget { if (progress == null || progress?.toProgressIndicatorValue == 1.0) { Future.delayed( const Duration(milliseconds: 500), - Navigator.of(context).pop, + Navigator.of(context).maybePop, ); } return Material( @@ -248,6 +251,7 @@ class AttachmentActionsModal extends StatelessWidget { ) : progress.toProgressIndicatorValue == 1.0 ? Container( + key: Key('completedIcon'), height: 160, width: 160, child: StreamSvgIcon.check( diff --git a/packages/stream_chat_flutter/lib/src/channel_file_display_screen.dart b/packages/stream_chat_flutter/lib/src/channel_file_display_screen.dart deleted file mode 100644 index 083b238b..00000000 --- a/packages/stream_chat_flutter/lib/src/channel_file_display_screen.dart +++ /dev/null @@ -1,182 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -import 'attachment/attachment.dart'; - -class ChannelFileDisplayScreen extends StatefulWidget { - /// The sorting used for the channels matching the filters. - /// Sorting is based on field and direction, multiple sorting options can be provided. - /// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count. - /// Direction can be ascending or descending. - final List sortOptions; - - /// Pagination parameters - /// limit: the number of users to return (max is 30) - /// offset: the offset (max is 1000) - /// message_limit: how many messages should be included to each channel - final PaginationParams paginationParams; - - /// The builder used when the file list is empty. - final WidgetBuilder emptyBuilder; - - const ChannelFileDisplayScreen({ - this.sortOptions, - this.paginationParams, - this.emptyBuilder, - }); - - @override - _ChannelFileDisplayScreenState createState() => - _ChannelFileDisplayScreenState(); -} - -class _ChannelFileDisplayScreenState extends State { - @override - void initState() { - super.initState(); - final messageSearchBloc = MessageSearchBloc.of(context); - messageSearchBloc.search( - filter: { - 'cid': { - r'$in': [StreamChannel.of(context).channel.cid] - } - }, - messageFilter: { - 'attachments.type': { - r'$in': ['file'], - }, - }, - sort: widget.sortOptions, - pagination: widget.paginationParams, - ); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: StreamChatTheme.of(context).colorTheme.white, - appBar: AppBar( - brightness: Theme.of(context).brightness, - elevation: 1, - centerTitle: true, - title: Text( - 'Files', - style: TextStyle( - color: StreamChatTheme.of(context).colorTheme.black, - fontSize: 16.0), - ), - leading: Center( - child: InkWell( - onTap: () { - Navigator.of(context).pop(); - }, - child: Container( - width: 24.0, - height: 24.0, - child: StreamSvgIcon.left( - color: StreamChatTheme.of(context).colorTheme.black, - size: 24.0, - ), - ), - ), - ), - backgroundColor: StreamChatTheme.of(context).colorTheme.white, - ), - body: _buildMediaGrid(), - ); - } - - Widget _buildMediaGrid() { - final messageSearchBloc = MessageSearchBloc.of(context); - - return StreamBuilder>( - builder: (context, snapshot) { - if (snapshot.data == null) { - return Center( - child: const CircularProgressIndicator(), - ); - } - - if (snapshot.data.isEmpty) { - if (widget.emptyBuilder != null) { - return widget.emptyBuilder(context); - } - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - StreamSvgIcon.files( - size: 136.0, - color: StreamChatTheme.of(context).colorTheme.greyGainsboro, - ), - SizedBox(height: 16.0), - Text( - 'No Files', - style: TextStyle( - fontSize: 14.0, - color: StreamChatTheme.of(context).colorTheme.black, - ), - ), - SizedBox(height: 8.0), - Text( - 'Files sent in this chat will appear here', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14.0, - color: StreamChatTheme.of(context) - .colorTheme - .black - .withOpacity(0.5), - ), - ), - ], - ), - ); - } - - final media = {}; - - for (var item in snapshot.data) { - item.message.attachments.where((e) => e.type == 'file').forEach((e) { - media[e] = item.message; - }); - } - - return LazyLoadScrollView( - onEndOfPage: () => messageSearchBloc.search( - filter: { - 'cid': { - r'$in': [StreamChannel.of(context).channel.cid] - } - }, - messageFilter: { - 'attachments.type': { - r'$in': ['file'] - }, - }, - sort: widget.sortOptions, - pagination: widget.paginationParams.copyWith( - offset: messageSearchBloc.messageResponses?.length ?? 0, - ), - ), - child: ListView.builder( - itemBuilder: (context, position) { - return Padding( - padding: const EdgeInsets.all(1.0), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: FileAttachment( - message: media.values.toList()[position], - attachment: media.keys.toList()[position], - ), - ), - ); - }, - itemCount: media.length, - ), - ); - }, - stream: messageSearchBloc.messagesStream, - ); - } -} diff --git a/packages/stream_chat_flutter/lib/src/channel_image.dart b/packages/stream_chat_flutter/lib/src/channel_image.dart index e7e39d38..4b3edce3 100644 --- a/packages/stream_chat_flutter/lib/src/channel_image.dart +++ b/packages/stream_chat_flutter/lib/src/channel_image.dart @@ -50,7 +50,6 @@ class ChannelImage extends StatelessWidget { this.channel, this.constraints, this.onTap, - this.showOnlineStatus = true, this.borderRadius, this.selected = false, this.selectionColor, @@ -68,8 +67,6 @@ class ChannelImage extends StatelessWidget { /// The function called when the image is tapped final VoidCallback onTap; - final bool showOnlineStatus; - final bool selected; final Color selectionColor; @@ -81,142 +78,144 @@ class ChannelImage extends StatelessWidget { final streamChat = StreamChat.of(context); final channel = this.channel ?? StreamChannel.of(context).channel; return StreamBuilder>( - stream: channel.extraDataStream, - initialData: channel.extraData, - builder: (context, snapshot) { - String image; - if (snapshot.data?.containsKey('image') == true) { - image = snapshot.data['image']; - } else if (channel.state.members?.length == 2) { - final otherMember = channel.state.members - .firstWhere((member) => member.user.id != streamChat.user.id); - return StreamBuilder( - stream: streamChat.client.state.usersStream - .map((users) => users[otherMember.userId]), - initialData: otherMember.user, - builder: (context, snapshot) { - return UserAvatar( - borderRadius: borderRadius ?? - StreamChatTheme.of(context) - .channelPreviewTheme - .avatarTheme - .borderRadius, - user: snapshot.data ?? otherMember.user, - constraints: constraints ?? - StreamChatTheme.of(context) - .channelPreviewTheme - .avatarTheme - .constraints, - onTap: onTap != null ? (_) => onTap() : null, - selected: selected, - selectionColor: selectionColor ?? - StreamChatTheme.of(context).colorTheme.accentBlue, - selectionThickness: selectionThickness, - ); - }); - } else { - final images = channel.state.members - .where((member) => - member.user.id != streamChat.user.id && - member.user.extraData['image'] != null) - .take(4) - .map((e) => e.user.extraData['image'] as String) - .toList(); - return GroupImage( - images: images, - borderRadius: borderRadius ?? - StreamChatTheme.of(context) - .channelPreviewTheme - .avatarTheme - .borderRadius, - constraints: constraints ?? - StreamChatTheme.of(context) - .channelPreviewTheme - .avatarTheme - .constraints, - onTap: onTap, - selected: selected, - selectionColor: selectionColor ?? - StreamChatTheme.of(context).colorTheme.accentBlue, - selectionThickness: selectionThickness, - ); - } - - Widget child = ClipRRect( + stream: channel.extraDataStream, + initialData: channel.extraData, + builder: (context, snapshot) { + String image; + if (snapshot.data?.containsKey('image') == true) { + image = snapshot.data['image']; + } else if (channel.state.members?.length == 2) { + final otherMember = channel.state.members + .firstWhere((member) => member.user.id != streamChat.user.id); + return StreamBuilder( + stream: streamChat.client.state.usersStream + .map((users) => users[otherMember.userId]), + initialData: otherMember.user, + builder: (context, snapshot) { + return UserAvatar( + borderRadius: borderRadius ?? + StreamChatTheme.of(context) + .channelPreviewTheme + .avatarTheme + .borderRadius, + user: snapshot.data ?? otherMember.user, + constraints: constraints ?? + StreamChatTheme.of(context) + .channelPreviewTheme + .avatarTheme + .constraints, + onTap: onTap != null ? (_) => onTap() : null, + selected: selected, + selectionColor: selectionColor ?? + StreamChatTheme.of(context).colorTheme.accentBlue, + selectionThickness: selectionThickness, + ); + }); + } else { + final images = channel.state.members + .where((member) => + member.user.id != streamChat.user.id && + member.user.extraData['image'] != null) + .take(4) + .map((e) => e.user.extraData['image'] as String) + .toList(); + return GroupImage( + images: images, borderRadius: borderRadius ?? StreamChatTheme.of(context) .channelPreviewTheme .avatarTheme .borderRadius, - child: Container( - constraints: constraints ?? - StreamChatTheme.of(context) - .channelPreviewTheme - .avatarTheme - .constraints, - decoration: BoxDecoration( - color: StreamChatTheme.of(context).colorTheme.accentBlue, - ), - child: Stack( - alignment: Alignment.center, - fit: StackFit.expand, - children: [ - image != null - ? CachedNetworkImage( - imageUrl: image, - errorWidget: (_, __, ___) { - return Center( - child: Text( - snapshot.data?.containsKey('name') ?? false - ? snapshot.data['name'][0] - : '', - style: TextStyle( - color: StreamChatTheme.of(context) - .colorTheme - .white, - fontWeight: FontWeight.bold, - ), - ), - ); - }, - fit: BoxFit.cover, - ) - : StreamChatTheme.of(context) - .defaultChannelImage(context, channel), - Material( - color: Colors.transparent, - child: InkWell( - onTap: onTap, - ), - ), - ], - ), - ), + constraints: constraints ?? + StreamChatTheme.of(context) + .channelPreviewTheme + .avatarTheme + .constraints, + onTap: onTap, + selected: selected, + selectionColor: selectionColor ?? + StreamChatTheme.of(context).colorTheme.accentBlue, + selectionThickness: selectionThickness, ); - if (selected) { - child = ClipRRect( - borderRadius: (borderRadius ?? - StreamChatTheme.of(context) - .ownMessageTheme - .avatarTheme - .borderRadius) + - BorderRadius.circular(selectionThickness), - child: Container( - constraints: constraints ?? + } + + Widget child = ClipRRect( + borderRadius: borderRadius ?? + StreamChatTheme.of(context) + .channelPreviewTheme + .avatarTheme + .borderRadius, + child: Container( + constraints: constraints ?? + StreamChatTheme.of(context) + .channelPreviewTheme + .avatarTheme + .constraints, + decoration: BoxDecoration( + color: StreamChatTheme.of(context).colorTheme.accentBlue, + ), + child: Stack( + alignment: Alignment.center, + fit: StackFit.expand, + children: [ + image != null + ? CachedNetworkImage( + imageUrl: image, + errorWidget: (_, __, ___) { + return Center( + child: Text( + snapshot.data?.containsKey('name') ?? false + ? snapshot.data['name'][0] + : '', + style: TextStyle( + color: StreamChatTheme.of(context) + .colorTheme + .white, + fontWeight: FontWeight.bold, + ), + ), + ); + }, + fit: BoxFit.cover, + ) + : StreamChatTheme.of(context) + .defaultChannelImage(context, channel), + Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + ), + ), + ], + ), + ), + ); + if (selected) { + child = ClipRRect( + key: Key('selectedImage'), + borderRadius: (borderRadius ?? StreamChatTheme.of(context) .ownMessageTheme .avatarTheme - .constraints, - color: selectionColor ?? - StreamChatTheme.of(context).colorTheme.accentBlue, - child: Padding( - padding: EdgeInsets.all(selectionThickness), - child: child, - ), + .borderRadius) + + BorderRadius.circular(selectionThickness), + child: Container( + constraints: constraints ?? + StreamChatTheme.of(context) + .ownMessageTheme + .avatarTheme + .constraints, + color: selectionColor ?? + StreamChatTheme.of(context).colorTheme.accentBlue, + child: Padding( + padding: EdgeInsets.all(selectionThickness), + child: child, ), - ); - } - return child; - }); + ), + ); + } + return child; + }, + ); } } diff --git a/packages/stream_chat_flutter/lib/src/channel_media_display_screen.dart b/packages/stream_chat_flutter/lib/src/channel_media_display_screen.dart deleted file mode 100644 index 7b34e449..00000000 --- a/packages/stream_chat_flutter/lib/src/channel_media_display_screen.dart +++ /dev/null @@ -1,251 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'package:video_player/video_player.dart'; - -import 'attachment/attachment.dart'; - -class ChannelMediaDisplayScreen extends StatefulWidget { - /// The sorting used for the channels matching the filters. - /// Sorting is based on field and direction, multiple sorting options can be provided. - /// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count. - /// Direction can be ascending or descending. - final List sortOptions; - - /// Pagination parameters - /// limit: the number of users to return (max is 30) - /// offset: the offset (max is 1000) - /// message_limit: how many messages should be included to each channel - final PaginationParams paginationParams; - - /// The builder used when the file list is empty. - final WidgetBuilder emptyBuilder; - - final ShowMessageCallback onShowMessage; - - const ChannelMediaDisplayScreen({ - this.sortOptions, - this.paginationParams, - this.emptyBuilder, - this.onShowMessage, - }); - - @override - _ChannelMediaDisplayScreenState createState() => - _ChannelMediaDisplayScreenState(); -} - -class _ChannelMediaDisplayScreenState extends State { - Map controllerCache = {}; - - @override - void initState() { - super.initState(); - final messageSearchBloc = MessageSearchBloc.of(context); - messageSearchBloc.search( - filter: { - 'cid': { - r'$in': [StreamChannel.of(context).channel.cid], - } - }, - messageFilter: { - 'attachments.type': { - r'$in': ['image', 'video'] - }, - }, - sort: widget.sortOptions, - pagination: widget.paginationParams, - ); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: StreamChatTheme.of(context).colorTheme.white, - appBar: AppBar( - brightness: Theme.of(context).brightness, - elevation: 1, - centerTitle: true, - title: Text( - 'Photos & Videos', - style: TextStyle( - color: StreamChatTheme.of(context).colorTheme.black, - fontSize: 16.0, - ), - ), - leading: Center( - child: InkWell( - onTap: () { - Navigator.of(context).pop(); - }, - child: Container( - width: 24.0, - height: 24.0, - child: StreamSvgIcon.left( - color: StreamChatTheme.of(context).colorTheme.black, - size: 24.0, - ), - ), - ), - ), - backgroundColor: StreamChatTheme.of(context).colorTheme.white, - ), - body: _buildMediaGrid(), - ); - } - - Widget _buildMediaGrid() { - final messageSearchBloc = MessageSearchBloc.of(context); - - return StreamBuilder>( - builder: (context, snapshot) { - if (snapshot.data == null) { - return Center( - child: const CircularProgressIndicator(), - ); - } - - if (snapshot.data.isEmpty) { - if (widget.emptyBuilder != null) { - return widget.emptyBuilder(context); - } - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - StreamSvgIcon.pictures( - size: 136.0, - color: StreamChatTheme.of(context).colorTheme.greyGainsboro, - ), - SizedBox(height: 16.0), - Text( - 'No Media', - style: TextStyle( - fontSize: 14.0, - color: StreamChatTheme.of(context).colorTheme.black, - ), - ), - SizedBox(height: 8.0), - Text( - 'Photos or video sent in this chat will \nappear here', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14.0, - color: StreamChatTheme.of(context) - .colorTheme - .black - .withOpacity(0.5), - ), - ), - ], - ), - ); - } - - final media = <_AssetPackage>[]; - - for (var item in snapshot.data) { - item.message.attachments - .where((e) => - (e.type == 'image' || e.type == 'video') && - e.ogScrapeUrl == null) - .forEach((e) { - VideoPlayerController controller; - if (e.type == 'video') { - var cachedController = controllerCache[e.assetUrl]; - - if (cachedController == null) { - controller = VideoPlayerController.network(e.assetUrl); - controller.initialize(); - controllerCache[e.assetUrl] = controller; - } else { - controller = cachedController; - } - } - media.add(_AssetPackage(e, item.message, controller)); - }); - } - - return LazyLoadScrollView( - onEndOfPage: () => messageSearchBloc.search( - filter: { - 'cid': { - r'$in': [StreamChannel.of(context).channel.cid] - } - }, - messageFilter: { - 'attachments.type': { - r'$in': ['image', 'video'] - }, - }, - sort: widget.sortOptions, - pagination: widget.paginationParams.copyWith( - offset: messageSearchBloc.messageResponses?.length ?? 0, - ), - ), - child: GridView.builder( - gridDelegate: - SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3), - itemBuilder: (context, position) { - var channel = StreamChannel.of(context).channel; - return Padding( - padding: const EdgeInsets.all(1.0), - child: InkWell( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => StreamChannel( - channel: channel, - child: FullScreenMedia( - mediaAttachments: - media.map((e) => e.attachment).toList(), - startIndex: position, - message: media[position].message, - sentAt: media[position].message.createdAt, - userName: media[position].message.user.name, - onShowMessage: widget.onShowMessage, - ), - ), - ), - ); - }, - child: media[position].attachment.type == 'image' - ? IgnorePointer( - child: ImageAttachment( - attachment: media[position].attachment, - message: media[position].message, - showTitle: false, - size: Size( - MediaQuery.of(context).size.width * 0.8, - MediaQuery.of(context).size.height * 0.3, - ), - ), - ) - : VideoPlayer(media[position].videoPlayer), - ), - ); - }, - itemCount: media.length, - ), - ); - }, - stream: messageSearchBloc.messagesStream, - ); - } - - @override - void dispose() { - super.dispose(); - for (var c in controllerCache.values) { - c.dispose(); - } - } -} - -class _AssetPackage { - Attachment attachment; - Message message; - VideoPlayerController videoPlayer; - - _AssetPackage(this.attachment, this.message, this.videoPlayer); -} diff --git a/packages/stream_chat_flutter/lib/src/channel_preview.dart b/packages/stream_chat_flutter/lib/src/channel_preview.dart index 3243988b..959342a6 100644 --- a/packages/stream_chat_flutter/lib/src/channel_preview.dart +++ b/packages/stream_chat_flutter/lib/src/channel_preview.dart @@ -6,7 +6,6 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import '../stream_chat_flutter.dart'; import 'channel_name.dart'; -import 'channel_unread_indicator.dart'; /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_preview.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_preview_paint.png) @@ -98,19 +97,20 @@ class ChannelPreview extends StatelessWidget { ), ), StreamBuilder>( - stream: channel.state.membersStream, - initialData: channel.state.members, - builder: (context, snapshot) { - if (!snapshot.hasData || - snapshot.data.isEmpty || - !snapshot.data.any((Member e) => - e.user.id == channel.client.state.user.id)) { - return SizedBox(); - } - return ChannelUnreadIndicator( - channel: channel, - ); - }), + stream: channel.state.membersStream, + initialData: channel.state.members, + builder: (context, snapshot) { + if (!snapshot.hasData || + snapshot.data.isEmpty || + !snapshot.data.any((Member e) => + e.user.id == channel.client.state.user.id)) { + return SizedBox(); + } + return UnreadIndicator( + cid: channel.cid, + ); + }, + ), ], ), subtitle: Row( diff --git a/packages/stream_chat_flutter/lib/src/channel_unread_indicator.dart b/packages/stream_chat_flutter/lib/src/channel_unread_indicator.dart deleted file mode 100644 index cb9dc362..00000000 --- a/packages/stream_chat_flutter/lib/src/channel_unread_indicator.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; -import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; - -class ChannelUnreadIndicator extends StatelessWidget { - const ChannelUnreadIndicator({ - Key key, - @required this.channel, - }) : super(key: key); - - final Channel channel; - - @override - Widget build(BuildContext context) { - return StreamBuilder( - stream: channel.state.unreadCountStream, - initialData: channel.state.unreadCount, - builder: (context, snapshot) { - if (!snapshot.hasData || snapshot.data == 0) { - return SizedBox(); - } - - return Material( - borderRadius: BorderRadius.circular(8), - color: StreamChatTheme.of(context) - .channelPreviewTheme - .unreadCounterColor, - child: Padding( - padding: const EdgeInsets.only( - left: 5.0, - right: 5.0, - top: 2, - bottom: 1, - ), - child: Center( - child: Text( - '${snapshot.data > 99 ? '99+' : snapshot.data}', - style: TextStyle( - fontSize: 11, - color: Colors.white, - ), - ), - ), - ), - ); - }, - ); - } -} diff --git a/packages/stream_chat_flutter/lib/src/info_tile.dart b/packages/stream_chat_flutter/lib/src/info_tile.dart index 2e12d6b3..72a077cc 100644 --- a/packages/stream_chat_flutter/lib/src/info_tile.dart +++ b/packages/stream_chat_flutter/lib/src/info_tile.dart @@ -11,14 +11,15 @@ class InfoTile extends StatelessWidget { final TextStyle textStyle; final Color backgroundColor; - InfoTile( - {this.message, - this.child, - this.showMessage, - this.tileAnchor, - this.childAnchor, - this.textStyle, - this.backgroundColor}); + InfoTile({ + this.message, + this.child, + this.showMessage, + this.tileAnchor, + this.childAnchor, + this.textStyle, + this.backgroundColor, + }); @override Widget build(BuildContext context) { diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart index c997d09c..0fdabf7d 100644 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart @@ -3,7 +3,6 @@ import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:stream_chat_flutter/src/message_action.dart'; import 'package:stream_chat_flutter/src/reaction_picker.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; @@ -18,11 +17,12 @@ import 'stream_chat_theme.dart'; class MessageActionsModal extends StatefulWidget { final Widget Function(BuildContext, Message) editMessageInputBuilder; - final void Function(Message) onThreadReplyTap; - final void Function(Message) onReplyTap; + final OnMessageTap onThreadReplyTap; + final OnMessageTap onReplyTap; final Message message; final MessageTheme messageTheme; final bool showReactions; + final OnMessageTap onCopyTap; final bool showDeleteMessage; final bool showCopyMessage; final bool showEditMessage; @@ -60,6 +60,7 @@ class MessageActionsModal extends StatefulWidget { this.reverse = false, this.customActions = const [], this.attachmentBorderRadiusGeometry, + this.onCopyTap, }) : super(key: key); @override @@ -211,10 +212,9 @@ class _MessageActionsModalState extends State { CrossAxisAlignment.stretch, children: [ if (widget.showReplyMessage && - (widget.message.status == + widget.message.status == MessageSendingStatus.sent || - widget.message.status == null) && - widget.message.parentId == null) + widget.message.status == null) _buildReplyButton(context), if (widget.showThreadReplyMessage && (widget.message.status == @@ -452,7 +452,7 @@ class _MessageActionsModalState extends State { Widget _buildCopyButton(BuildContext context) { return InkWell( onTap: () async { - await Clipboard.setData(ClipboardData(text: widget.message.text)); + widget.onCopyTap?.call(widget.message); Navigator.pop(context); }, child: Padding( diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 385dc41f..b4b2176c 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -693,7 +693,7 @@ class MessageInputState extends State { if (!mounted) { return; } - StreamChannel.of(context).channel.keyStroke().catchError((e) {}); + StreamChannel.of(context).channel.keyStroke()?.catchError((e) {}); setState(() { _messageIsPresent = s.trim().isNotEmpty; @@ -2181,10 +2181,12 @@ class MessageInputState extends State { void _parseExistingMessage(Message message) { textEditingController.text = message.text; _messageIsPresent = true; - for (final attachment in message?.attachments) { - _attachments[attachment.id] = attachment.copyWith( - uploadState: attachment.uploadState ?? UploadState.success(), - ); + if (message.attachments != null) { + for (final attachment in message.attachments) { + _attachments[attachment.id] = attachment.copyWith( + uploadState: attachment.uploadState ?? UploadState.success(), + ); + } } } diff --git a/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart b/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart index c2a13ae7..e2171078 100644 --- a/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart @@ -13,8 +13,6 @@ import 'message_widget.dart'; import 'stream_chat_theme.dart'; class MessageReactionsModal extends StatelessWidget { - final Widget Function(BuildContext, Message) editMessageInputBuilder; - final void Function(Message) onThreadTap; final Message message; final MessageTheme messageTheme; final bool reverse; @@ -30,8 +28,6 @@ class MessageReactionsModal extends StatelessWidget { @required this.message, @required this.messageTheme, this.showReactions = true, - this.onThreadTap, - this.editMessageInputBuilder, this.messageShape, this.attachmentShape, this.reverse = false, diff --git a/packages/stream_chat_flutter/lib/src/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget.dart index af6ea1ec..09feeb74 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget.dart @@ -5,6 +5,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_portal/flutter_portal.dart'; import 'package:jiffy/jiffy.dart'; import 'package:stream_chat_flutter/src/message_action.dart'; @@ -841,6 +842,8 @@ class _MessageWidgetState extends State return StreamChannel( channel: channel, child: MessageActionsModal( + onCopyTap: (message) => + Clipboard.setData(ClipboardData(text: message.text)), attachmentBorderRadiusGeometry: widget.attachmentBorderRadiusGeometry, showUserAvatar: @@ -903,8 +906,6 @@ class _MessageWidgetState extends State widget.attachmentShape ?? _getDefaultAttachmentShape(context), reverse: widget.reverse, message: widget.message, - editMessageInputBuilder: widget.editMessageInputBuilder, - onThreadTap: widget.onThreadTap, showReactions: widget.showReactions, ), ); diff --git a/packages/stream_chat_flutter/lib/src/reaction_bubble.dart b/packages/stream_chat_flutter/lib/src/reaction_bubble.dart index 4ddcca93..160c0579 100644 --- a/packages/stream_chat_flutter/lib/src/reaction_bubble.dart +++ b/packages/stream_chat_flutter/lib/src/reaction_bubble.dart @@ -2,6 +2,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; import 'package:stream_chat_flutter/src/reaction_icon.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; @@ -36,67 +37,70 @@ class ReactionBubble extends StatelessWidget { return Transform( transform: Matrix4.rotationY(reverse ? pi : 0), alignment: Alignment.center, - child: Stack( - alignment: Alignment.center, - children: [ - Transform.translate( - offset: Offset(reverse ? offset : -offset, 0), - child: Container( - padding: const EdgeInsets.all(2), - decoration: BoxDecoration( - color: maskColor, - borderRadius: BorderRadius.all(Radius.circular(16)), - ), + child: Center( + child: Stack( + alignment: Alignment.center, + fit: StackFit.loose, + children: [ + Transform.translate( + offset: Offset(reverse ? offset : -offset, 0), child: Container( - padding: EdgeInsets.symmetric( - vertical: 4, - horizontal: totalReactions > 1 ? 4 : 0, - ), + padding: const EdgeInsets.all(2), decoration: BoxDecoration( - border: Border.all( - color: borderColor, - ), - color: backgroundColor, - borderRadius: BorderRadius.all(Radius.circular(14)), + color: maskColor, + borderRadius: BorderRadius.all(Radius.circular(16)), ), - child: LayoutBuilder( - builder: (context, constraints) { - return Flex( - direction: Axis.horizontal, - mainAxisSize: MainAxisSize.min, - children: [ - if (constraints.maxWidth < double.infinity) - ...reactions - .take((constraints.maxWidth) ~/ 24) - .map((reaction) { - return _buildReaction( - reactionIcons, - reaction, - context, - ); - }).toList(), - if (constraints.maxWidth == double.infinity) - ...reactions.map((reaction) { - return _buildReaction( - reactionIcons, - reaction, - context, - ); - }).toList(), - ], - ); - }, + child: Container( + padding: EdgeInsets.symmetric( + vertical: 4, + horizontal: totalReactions > 1 ? 4 : 0, + ), + decoration: BoxDecoration( + border: Border.all( + color: borderColor, + ), + color: backgroundColor, + borderRadius: BorderRadius.all(Radius.circular(14)), + ), + child: LayoutBuilder( + builder: (context, constraints) { + return Flex( + direction: Axis.horizontal, + mainAxisSize: MainAxisSize.min, + children: [ + if (constraints.maxWidth < double.infinity) + ...reactions + .take((constraints.maxWidth) ~/ 24) + .map((reaction) { + return _buildReaction( + reactionIcons, + reaction, + context, + ); + }).toList(), + if (constraints.maxWidth == double.infinity) + ...reactions.map((reaction) { + return _buildReaction( + reactionIcons, + reaction, + context, + ); + }).toList(), + ], + ); + }, + ), ), ), ), - ), - Positioned( - bottom: 2, - left: reverse ? null : 13, - right: !reverse ? null : 13, - child: _buildReactionsTail(context), - ), - ], + Positioned( + bottom: 2, + left: reverse ? null : 13, + right: !reverse ? null : 13, + child: _buildReactionsTail(context), + ), + ], + ), ), ); } diff --git a/packages/stream_chat_flutter/lib/src/unread_indicator.dart b/packages/stream_chat_flutter/lib/src/unread_indicator.dart index 61a92987..0c30e9a1 100644 --- a/packages/stream_chat_flutter/lib/src/unread_indicator.dart +++ b/packages/stream_chat_flutter/lib/src/unread_indicator.dart @@ -40,7 +40,7 @@ class UnreadIndicator extends StatelessWidget { ), child: Center( child: Text( - '${snapshot.data}', + '${snapshot.data > 99 ? '99+' : snapshot.data}', style: TextStyle( fontSize: 11, color: Colors.white, diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index b8062209..eb35c69b 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -1,3 +1,6 @@ +export 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; + +export 'src/attachment/attachment.dart'; export 'src/back_button.dart'; export 'src/channel_header.dart'; export 'src/channel_image.dart'; @@ -5,39 +8,35 @@ export 'src/channel_list_header.dart'; export 'src/channel_list_view.dart'; export 'src/channel_name.dart'; export 'src/channel_preview.dart'; +export 'src/connection_status_builder.dart'; export 'src/date_divider.dart'; export 'src/deleted_message.dart'; -export 'src/message_action.dart'; -export 'src/attachment/attachment.dart'; export 'src/full_screen_media.dart'; -export 'src/image_header.dart'; export 'src/image_footer.dart'; +export 'src/image_header.dart'; +export 'src/info_tile.dart'; +export 'src/mention_tile.dart'; +export 'src/message_action.dart'; export 'src/message_input.dart'; export 'src/message_list_view.dart'; +export 'src/message_search_item.dart'; +export 'src/message_search_list_view.dart'; export 'src/message_text.dart'; export 'src/message_widget.dart'; +export 'src/option_list_tile.dart'; export 'src/reaction_picker.dart'; export 'src/sending_indicator.dart'; +export 'src/stream_chat.dart'; export 'src/stream_chat_theme.dart'; export 'src/stream_neumorphic_button.dart'; export 'src/stream_svg_icon.dart'; export 'src/system_message.dart'; export 'src/thread_header.dart'; export 'src/typing_indicator.dart'; +export 'src/unread_indicator.dart'; export 'src/user_avatar.dart'; export 'src/user_item.dart'; export 'src/user_item.dart'; export 'src/user_list_view.dart'; export 'src/user_list_view.dart'; export 'src/utils.dart'; -export 'src/message_search_item.dart'; -export 'src/message_search_list_view.dart'; -export 'src/unread_indicator.dart'; -export 'src/option_list_tile.dart'; -export 'src/channel_file_display_screen.dart'; -export 'src/channel_media_display_screen.dart'; -export 'src/info_tile.dart'; -export 'src/stream_chat.dart'; -export 'src/connection_status_builder.dart'; -export 'src/mention_tile.dart'; -export 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; diff --git a/packages/stream_chat_flutter/pubspec.yaml b/packages/stream_chat_flutter/pubspec.yaml index 93401a9c..65c688d3 100644 --- a/packages/stream_chat_flutter/pubspec.yaml +++ b/packages/stream_chat_flutter/pubspec.yaml @@ -55,9 +55,12 @@ flutter: - svgs/ - lib/svgs/ - animations/ + uses-material-design: true dev_dependencies: flutter_test: sdk: flutter mockito: ^4.1.3 - pedantic: ^1.9.2 \ No newline at end of file + pedantic: ^1.9.2 + golden_toolkit: ^0.6.0 + \ No newline at end of file diff --git a/packages/stream_chat_flutter/test/flutter_test_config.dart b/packages/stream_chat_flutter/test/flutter_test_config.dart new file mode 100644 index 00000000..c09db700 --- /dev/null +++ b/packages/stream_chat_flutter/test/flutter_test_config.dart @@ -0,0 +1,9 @@ +import 'dart:async'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:golden_toolkit/golden_toolkit.dart'; + +Future testExecutable(FutureOr Function() testMain) async { + await loadAppFonts(); + return testMain(); +} diff --git a/packages/stream_chat_flutter/test/src/attachment_actions_modal_test.dart b/packages/stream_chat_flutter/test/src/attachment_actions_modal_test.dart new file mode 100644 index 00000000..1ef02cd9 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/attachment_actions_modal_test.dart @@ -0,0 +1,510 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:stream_chat_flutter/src/attachment_actions_modal.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import 'mocks.dart'; + +class MockAttachmentDownloader extends Mock { + ProgressCallback progressCallback; + Completer completer = Completer(); + + Future call( + Attachment attachment, { + ProgressCallback progressCallback, + }) { + this.progressCallback = progressCallback; + return completer.future; + } +} + +void main() { + testWidgets( + 'it should show all the actions', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + + final themeData = ThemeData(); + final streamTheme = StreamChatThemeData.getDefaultTheme(themeData); + await tester.pumpWidget( + MaterialApp( + theme: themeData, + home: StreamChat( + streamChatThemeData: streamTheme, + client: client, + child: AttachmentActionsModal( + message: Message( + text: 'test', + user: User( + id: 'user-id', + ), + attachments: [ + Attachment( + type: 'image', + title: 'text.jpg', + ), + ], + ), + currentIndex: 0, + ), + ), + ), + ); + expect(find.text('Reply'), findsOneWidget); + expect(find.text('Show in Chat'), findsOneWidget); + expect(find.text('Save Image'), findsOneWidget); + expect(find.text('Delete'), findsOneWidget); + }, + ); + + testWidgets( + 'it should hide delete if it\'s not my message', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id2')); + + final themeData = ThemeData(); + final streamTheme = StreamChatThemeData.getDefaultTheme(themeData); + await tester.pumpWidget( + MaterialApp( + theme: themeData, + home: StreamChat( + streamChatThemeData: streamTheme, + client: client, + child: AttachmentActionsModal( + message: Message( + text: 'test', + user: User( + id: 'user-id', + ), + attachments: [ + Attachment( + type: 'image', + title: 'text.jpg', + ), + ], + ), + currentIndex: 0, + ), + ), + ), + ); + expect(find.text('Reply'), findsOneWidget); + expect(find.text('Show in Chat'), findsOneWidget); + expect(find.text('Save Image'), findsOneWidget); + expect(find.text('Delete'), findsNothing); + }, + ); + + testWidgets( + 'it should show save video for videos', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + + final themeData = ThemeData(); + final streamTheme = StreamChatThemeData.getDefaultTheme(themeData); + await tester.pumpWidget( + MaterialApp( + theme: themeData, + home: StreamChat( + streamChatThemeData: streamTheme, + client: client, + child: Container( + child: AttachmentActionsModal( + message: Message( + text: 'test', + user: User( + id: 'user-id', + ), + attachments: [ + Attachment( + type: 'video', + title: 'video.mp4', + ), + ], + ), + currentIndex: 0, + ), + ), + ), + ), + ); + expect(find.text('Save Video'), findsOneWidget); + }, + ); + + testWidgets( + 'tapping on reply should pop', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + + final themeData = ThemeData(); + final streamTheme = StreamChatThemeData.getDefaultTheme(themeData); + + final mockObserver = MockNavigatorObserver(); + + final message = Message( + text: 'test', + user: User( + id: 'user-id', + ), + attachments: [ + Attachment( + type: 'image', + title: 'image.jpg', + ), + ], + ); + await tester.pumpWidget( + MaterialApp( + theme: themeData, + navigatorObservers: [mockObserver], + home: StreamChat( + streamChatThemeData: streamTheme, + client: client, + child: Container( + child: AttachmentActionsModal( + message: message, + currentIndex: 0, + ), + ), + ), + ), + ); + await tester.tap(find.text('Reply')); + verify(mockObserver.didPop(any, any)); + }, + ); + + testWidgets( + 'tapping on show in chat should call onShowMessage', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + + final themeData = ThemeData(); + final streamTheme = StreamChatThemeData.getDefaultTheme(themeData); + final onShowMessage = MockVoidCallback(); + + await tester.pumpWidget( + MaterialApp( + theme: themeData, + home: StreamChat( + streamChatThemeData: streamTheme, + client: client, + child: Container( + child: AttachmentActionsModal( + onShowMessage: onShowMessage, + message: Message( + text: 'test', + user: User( + id: 'user-id', + ), + attachments: [ + Attachment( + type: 'image', + title: 'image.jpg', + ), + ]), + currentIndex: 0, + ), + ), + ), + ), + ); + await tester.tap(find.text('Show in Chat')); + verify(onShowMessage.call()).called(1); + }, + ); + + testWidgets( + 'tapping on delete in chat should remove the attachment', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + final mockChannel = MockChannel(); + + when(mockChannel.updateMessage(any)).thenAnswer((_) { + return; + }); + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + + final message = Message( + text: 'test', + user: User( + id: 'user-id', + ), + attachments: [ + Attachment( + type: 'image', + title: 'image.jpg', + ), + Attachment( + type: 'image', + title: 'image.jpg', + ), + ], + ); + + await tester.pumpWidget( + MaterialApp( + builder: (context, child) { + return StreamChat( + client: client, + child: child, + ); + }, + home: StreamChannel( + showLoading: false, + channel: mockChannel, + child: AttachmentActionsModal( + message: message, + currentIndex: 0, + ), + ), + ), + ); + await tester.tap(find.text('Delete')); + verify(mockChannel.updateMessage(message.copyWith( + attachments: [ + message.attachments[1], + ], + ))).called(1); + }, + ); + + testWidgets( + 'tapping on delete in chat should remove the attachment if there is text', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + final mockChannel = MockChannel(); + + when(mockChannel.updateMessage(any)).thenAnswer((_) { + return; + }); + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + + final message = Message( + text: 'test', + user: User( + id: 'user-id', + ), + attachments: [ + Attachment( + type: 'image', + title: 'image.jpg', + ), + ], + ); + + await tester.pumpWidget( + MaterialApp( + builder: (context, child) { + return StreamChat( + client: client, + child: child, + ); + }, + home: StreamChannel( + showLoading: false, + channel: mockChannel, + child: AttachmentActionsModal( + message: message, + currentIndex: 0, + ), + ), + ), + ); + await tester.tap(find.text('Delete')); + verify(mockChannel.updateMessage(message.copyWith( + attachments: [], + ))).called(1); + }, + ); + + testWidgets( + 'tapping on delete in chat should remove the message if that\'s the only attachment and there is no text', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + final mockChannel = MockChannel(); + + when(mockChannel.deleteMessage(any)).thenAnswer((_) { + return; + }); + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + + final message = Message( + user: User( + id: 'user-id', + ), + attachments: [ + Attachment( + type: 'image', + title: 'image.jpg', + ), + ], + ); + + await tester.pumpWidget( + MaterialApp( + builder: (context, child) { + return StreamChat( + client: client, + child: child, + ); + }, + home: StreamChannel( + showLoading: false, + channel: mockChannel, + child: AttachmentActionsModal( + message: message, + currentIndex: 0, + ), + ), + ), + ); + await tester.tap(find.text('Delete')); + verify(mockChannel.deleteMessage(message)).called(1); + }, + ); + + testWidgets( + 'tapping on save in chat should call image downloader', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + + final imageDownloader = MockAttachmentDownloader(); + + await tester.pumpWidget( + MaterialApp( + builder: (context, child) { + return StreamChat( + client: client, + child: child, + ); + }, + home: Container( + child: AttachmentActionsModal( + imageDownloader: imageDownloader, + message: Message( + text: 'test', + user: User( + id: 'user-id', + ), + attachments: [ + Attachment( + type: 'image', + title: 'image.jpg', + ), + ]), + currentIndex: 0, + ), + ), + ), + ); + + await tester.tap(find.text('Save Image')); + + imageDownloader.progressCallback(0, 100); + await tester.pump(); + expect(find.text('0%'), findsOneWidget); + + imageDownloader.progressCallback(50, 100); + await tester.pump(); + expect(find.text('50%'), findsOneWidget); + + imageDownloader.progressCallback(100, 100); + imageDownloader.completer.complete('path'); + await tester.pump(); + expect(find.byKey(Key('completedIcon')), findsOneWidget); + await tester.pumpAndSettle(Duration(milliseconds: 500)); + }, + ); + + testWidgets( + 'tapping on save in chat should call file downloader', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + + final fileDownloader = MockAttachmentDownloader(); + + await tester.pumpWidget( + MaterialApp( + builder: (context, child) { + return StreamChat( + client: client, + child: child, + ); + }, + home: Container( + child: AttachmentActionsModal( + fileDownloader: fileDownloader, + message: Message( + text: 'test', + user: User( + id: 'user-id', + ), + attachments: [ + Attachment( + type: 'video', + title: 'video.mp4', + ), + ]), + currentIndex: 0, + ), + ), + ), + ); + + await tester.tap(find.text('Save Video')); + + fileDownloader.progressCallback(0, 100); + await tester.pump(); + expect(find.text('0%'), findsOneWidget); + + fileDownloader.progressCallback(50, 100); + await tester.pump(); + expect(find.text('50%'), findsOneWidget); + + fileDownloader.progressCallback(100, 100); + fileDownloader.completer.complete('path'); + await tester.pump(); + expect(find.byKey(Key('completedIcon')), findsOneWidget); + await tester.pumpAndSettle(Duration(milliseconds: 500)); + }, + ); +} diff --git a/packages/stream_chat_flutter/test/src/attachment_widgets_test.dart b/packages/stream_chat_flutter/test/src/attachment_widgets_test.dart new file mode 100644 index 00000000..b547e80f --- /dev/null +++ b/packages/stream_chat_flutter/test/src/attachment_widgets_test.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import 'mocks.dart'; + +void main() { + testWidgets( + 'it should show file details', + (WidgetTester tester) async { + final channel = MockChannel(); + final channelState = MockChannelState(); + + when(channel.state).thenReturn(channelState); + + final themeData = ThemeData(); + final streamTheme = StreamChatThemeData.getDefaultTheme(themeData); + + await tester.pumpWidget( + MaterialApp( + home: StreamChatTheme( + data: streamTheme, + child: StreamChannel( + channel: channel, + child: Container( + child: FileAttachment( + size: Size( + 300.0, + 300.0, + ), + message: Message(), + attachment: Attachment( + type: 'file', + title: 'example.pdf', + extraData: { + 'mime_type': 'pdf', + }, + ), + ), + ), + ), + ), + ), + ); + + expect(find.text('example.pdf'), findsOneWidget); + }, + ); +} diff --git a/packages/stream_chat_flutter/test/src/back_button_test.dart b/packages/stream_chat_flutter/test/src/back_button_test.dart new file mode 100644 index 00000000..3ee88ed1 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/back_button_test.dart @@ -0,0 +1,144 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:stream_chat_flutter/src/back_button.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import 'mocks.dart'; + +void main() { + testWidgets( + 'BackButton control test', + (WidgetTester tester) async { + final theme = ThemeData(); + await tester.pumpWidget( + MaterialApp( + home: const Material(child: Text('Home')), + routes: { + '/next': (BuildContext context) { + return Material( + child: Center( + child: StreamChatTheme( + data: StreamChatThemeData.getDefaultTheme(theme), + child: StreamBackButton(), + ), + ), + ); + }, + }, + ), + ); + + // ignore: unawaited_futures + tester.state(find.byType(Navigator)).pushNamed('/next'); + + await tester.pumpAndSettle(); + + await tester.tap(find.byType(StreamBackButton)); + + await tester.pumpAndSettle(); + + expect(find.text('Home'), findsOneWidget); + }, + ); + + testWidgets( + 'it should not throw errors if cannot pop', + (WidgetTester tester) async { + final theme = ThemeData(); + await tester.pumpWidget( + MaterialApp( + home: Material( + child: Center( + child: StreamChatTheme( + data: StreamChatThemeData.getDefaultTheme(theme), + child: StreamBackButton(), + ), + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + await tester.tap(find.byType(StreamBackButton)); + + await tester.pumpAndSettle(); + + expect(find.byType(StreamBackButton), findsOneWidget); + }, + ); + + testWidgets( + 'BackButton onPressed overrides default pop behavior', + (WidgetTester tester) async { + final theme = ThemeData(); + var customCallbackWasCalled = false; + await tester.pumpWidget( + MaterialApp( + home: const Material(child: Text('Home')), + routes: { + '/next': (BuildContext context) { + return Material( + child: Center( + child: StreamChatTheme( + data: StreamChatThemeData.getDefaultTheme(theme), + child: StreamBackButton( + onPressed: () => customCallbackWasCalled = true, + ), + ), + ), + ); + }, + }, + ), + ); + + // ignore: unawaited_futures + tester.state(find.byType(Navigator)).pushNamed('/next'); + + await tester.pumpAndSettle(); + + expect(find.text('Home'), findsNothing); // Start off on the second page. + expect( + customCallbackWasCalled, + false, + ); // customCallbackWasCalled should still be false. + await tester.tap(find.byType(StreamBackButton)); + + await tester.pumpAndSettle(); + + // We're still on the second page. + expect(find.text('Home'), findsNothing); + // But the custom callback is called. + expect(customCallbackWasCalled, true); + }, + ); + + testWidgets( + 'it should show unread count', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + + when(client.state).thenReturn(clientState); + + await tester.pumpWidget( + MaterialApp( + home: Material( + child: Center( + child: StreamChat( + client: client, + child: StreamBackButton( + showUnreads: true, + ), + ), + ), + ), + ), + ); + + expect(find.byType(UnreadIndicator), findsOneWidget); + }, + ); +} diff --git a/packages/stream_chat_flutter/test/src/channel_header_test.dart b/packages/stream_chat_flutter/test/src/channel_header_test.dart new file mode 100644 index 00000000..1a5cb79d --- /dev/null +++ b/packages/stream_chat_flutter/test/src/channel_header_test.dart @@ -0,0 +1,388 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:stream_chat_flutter/src/channel_info.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import 'mocks.dart'; + +void main() { + testWidgets( + 'it should show basic channel information', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + final channel = MockChannel(); + final channelState = MockChannelState(); + final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(channel.lastMessageAt).thenReturn(lastMessageAt); + when(channel.state).thenReturn(channelState); + when(channel.client).thenReturn(client); + when(channel.isMuted).thenReturn(false); + when(channel.isMutedStream).thenAnswer((i) => Stream.value(false)); + when(channel.extraDataStream).thenAnswer((i) => Stream.value({ + 'name': 'test', + })); + when(channel.extraData).thenReturn({ + 'name': 'test', + }); + when(channelState.unreadCount).thenReturn(1); + when(channelState.unreadCountStream).thenAnswer((i) => Stream.value(1)); + when(channelState.membersStream).thenAnswer((i) => Stream.value([ + Member( + userId: 'user-id', + user: User(id: 'user-id'), + ) + ])); + when(channelState.members).thenReturn([ + Member( + userId: 'user-id', + user: User(id: 'user-id'), + ), + ]); + + await tester.pumpWidget(MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: Scaffold( + body: ChannelHeader(), + ), + ), + ), + )); + + expect(find.text('test'), findsOneWidget); + expect(find.byType(ChannelImage), findsOneWidget); + expect(find.byType(StreamBackButton), findsOneWidget); + expect(find.byType(ChannelInfo), findsOneWidget); + }, + ); + + testWidgets( + 'it should show the InfoTile message if disconnected', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + final channel = MockChannel(); + final channelState = MockChannelState(); + final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(channel.lastMessageAt).thenReturn(lastMessageAt); + when(channel.state).thenReturn(channelState); + when(channel.client).thenReturn(client); + when(channel.isMuted).thenReturn(false); + when(channel.isMutedStream).thenAnswer((i) => Stream.value(false)); + when(channel.extraDataStream).thenAnswer((i) => Stream.value({ + 'name': 'test', + })); + when(channel.extraData).thenReturn({ + 'name': 'test', + }); + when(channelState.unreadCount).thenReturn(1); + when(channelState.unreadCountStream).thenAnswer((i) => Stream.value(1)); + when(channelState.membersStream).thenAnswer((i) => Stream.value([ + Member( + userId: 'user-id', + user: User(id: 'user-id'), + ) + ])); + when(channelState.members).thenReturn([ + Member( + userId: 'user-id', + user: User(id: 'user-id'), + ), + ]); + when(client.wsConnectionStatusStream) + .thenAnswer((_) => Stream.value(ConnectionStatus.disconnected)); + + await tester.pumpWidget(MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: Scaffold( + body: ChannelHeader( + showConnectionStateTile: true, + ), + ), + ), + ), + )); + + expect(tester.widget(find.byType(InfoTile)).showMessage, true); + expect(tester.widget(find.byType(InfoTile)).message, + 'Disconnected'); + }, + ); + + testWidgets( + 'it should show the InfoTile message if connecting', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + final channel = MockChannel(); + final channelState = MockChannelState(); + final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(channel.lastMessageAt).thenReturn(lastMessageAt); + when(channel.state).thenReturn(channelState); + when(channel.client).thenReturn(client); + when(channel.isMuted).thenReturn(false); + when(channel.isMutedStream).thenAnswer((i) => Stream.value(false)); + when(channel.extraDataStream).thenAnswer((i) => Stream.value({ + 'name': 'test', + })); + when(channel.extraData).thenReturn({ + 'name': 'test', + }); + when(channelState.unreadCount).thenReturn(1); + when(channelState.unreadCountStream).thenAnswer((i) => Stream.value(1)); + when(channelState.membersStream).thenAnswer((i) => Stream.value([ + Member( + userId: 'user-id', + user: User(id: 'user-id'), + ) + ])); + when(channelState.members).thenReturn([ + Member( + userId: 'user-id', + user: User(id: 'user-id'), + ), + ]); + when(client.wsConnectionStatusStream) + .thenAnswer((_) => Stream.value(ConnectionStatus.connecting)); + when(channel.initialized).thenAnswer((_) => Future.value(true)); + + await tester.pumpWidget(MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + showLoading: false, + child: Scaffold( + body: ChannelHeader( + showConnectionStateTile: true, + ), + ), + ), + ), + )); + + await tester.pump(); + + expect(tester.widget(find.byType(InfoTile)).showMessage, true); + expect(tester.widget(find.byType(InfoTile)).message, + 'Reconnecting...'); + }, + ); + + testWidgets( + 'it should apply passed properties', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + final channel = MockChannel(); + final channelState = MockChannelState(); + final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(channel.lastMessageAt).thenReturn(lastMessageAt); + when(channel.state).thenReturn(channelState); + when(channel.client).thenReturn(client); + when(channel.isMuted).thenReturn(false); + when(channel.isMutedStream).thenAnswer((i) => Stream.value(false)); + when(channel.extraDataStream).thenAnswer((i) => Stream.value({ + 'name': 'test', + })); + when(channel.extraData).thenReturn({ + 'name': 'test', + }); + when(channelState.unreadCount).thenReturn(1); + when(channelState.unreadCountStream).thenAnswer((i) => Stream.value(1)); + when(channelState.membersStream).thenAnswer((i) => Stream.value([ + Member( + userId: 'user-id', + user: User(id: 'user-id'), + ) + ])); + when(channelState.members).thenReturn([ + Member( + userId: 'user-id', + user: User(id: 'user-id'), + ), + ]); + + await tester.pumpWidget(MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: Scaffold( + body: ChannelHeader( + leading: Text('leading'), + subtitle: Text('subtitle'), + actions: [ + Text('action'), + ], + title: Text('title'), + ), + ), + ), + ), + )); + + expect(find.text('test'), findsNothing); + expect(find.byType(StreamBackButton), findsNothing); + expect(find.byType(ChannelImage), findsNothing); + expect(find.byType(ChannelInfo), findsNothing); + expect(find.text('leading'), findsOneWidget); + expect(find.text('title'), findsOneWidget); + expect(find.text('subtitle'), findsOneWidget); + expect(find.text('action'), findsOneWidget); + }, + ); + + testWidgets( + 'showBackButton: false should hide the StreamBackButton and ' + 'showTypingIndicator: false should hide the typing indicator and ' + 'showConnectionStateTile: false should be passed to the infotile', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + final channel = MockChannel(); + final channelState = MockChannelState(); + final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(channel.lastMessageAt).thenReturn(lastMessageAt); + when(channel.state).thenReturn(channelState); + when(channel.client).thenReturn(client); + when(channel.isMuted).thenReturn(false); + when(channel.isMutedStream).thenAnswer((i) => Stream.value(false)); + when(channel.extraDataStream).thenAnswer((i) => Stream.value({ + 'name': 'test', + })); + when(channel.extraData).thenReturn({ + 'name': 'test', + }); + when(channelState.unreadCount).thenReturn(1); + when(channelState.unreadCountStream).thenAnswer((i) => Stream.value(1)); + when(channelState.membersStream).thenAnswer((i) => Stream.value([ + Member( + userId: 'user-id', + user: User(id: 'user-id'), + ) + ])); + when(channelState.members).thenReturn([ + Member( + userId: 'user-id', + user: User(id: 'user-id'), + ), + ]); + when(client.wsConnectionStatusStream) + .thenAnswer((_) => Stream.value(ConnectionStatus.disconnected)); + + await tester.pumpWidget(MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: Scaffold( + body: ChannelHeader( + showTypingIndicator: false, + showBackButton: false, + showConnectionStateTile: false, + ), + ), + ), + ), + )); + + expect(find.byType(StreamBackButton), findsNothing); + expect( + tester + .widget(find.byType(ChannelInfo)) + .showTypingIndicator, + false); + expect(tester.widget(find.byType(InfoTile)).showMessage, false); + }, + ); + + testWidgets( + 'should apply passed callbacks', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + final channel = MockChannel(); + final channelState = MockChannelState(); + final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(channel.lastMessageAt).thenReturn(lastMessageAt); + when(channel.state).thenReturn(channelState); + when(channel.client).thenReturn(client); + when(channel.isMuted).thenReturn(false); + when(channel.isMutedStream).thenAnswer((i) => Stream.value(false)); + when(channel.extraDataStream).thenAnswer((i) => Stream.value({ + 'name': 'test', + })); + when(channel.extraData).thenReturn({ + 'name': 'test', + }); + when(channelState.unreadCount).thenReturn(1); + when(channelState.unreadCountStream).thenAnswer((i) => Stream.value(1)); + when(channelState.membersStream).thenAnswer((i) => Stream.value([ + Member( + userId: 'user-id', + user: User(id: 'user-id'), + ) + ])); + when(channelState.members).thenReturn([ + Member( + userId: 'user-id', + user: User(id: 'user-id'), + ), + ]); + + var backPressed = false; + var imageTapped = false; + var titleTapped = false; + + await tester.pumpWidget(MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: Scaffold( + body: ChannelHeader( + onBackPressed: () => backPressed = true, + onImageTap: () => imageTapped = true, + onTitleTap: () => titleTapped = true, + ), + ), + ), + ), + )); + + await tester.tap(find.byType(StreamBackButton)); + await tester.tap(find.byType(ChannelImage)); + await tester.tap(find.byType(ChannelName)); + + expect(backPressed, true); + expect(imageTapped, true); + expect(titleTapped, true); + }, + ); +} diff --git a/packages/stream_chat_flutter/test/src/channel_image_test.dart b/packages/stream_chat_flutter/test/src/channel_image_test.dart new file mode 100644 index 00000000..cea48d2f --- /dev/null +++ b/packages/stream_chat_flutter/test/src/channel_image_test.dart @@ -0,0 +1,263 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:stream_chat_flutter/src/group_image.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import 'mocks.dart'; + +void main() { + testWidgets( + 'it should show the image in channel.extraData', + (tester) async { + final client = MockClient(); + final clientState = MockClientState(); + final channel = MockChannel(); + final channelState = MockChannelState(); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(channel.state).thenReturn(channelState); + when(channel.client).thenReturn(client); + when(channel.extraDataStream).thenAnswer((i) => Stream.value({ + 'name': 'test', + 'image': 'imagetest', + })); + when(channel.extraData).thenReturn({ + 'name': 'test', + 'image': 'imagetest', + }); + + await tester.pumpWidget(MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: Scaffold( + body: ChannelImage(), + ), + ), + ), + )); + + final image = + tester.widget(find.byType(CachedNetworkImage)); + expect(image.imageUrl, 'imagetest'); + }, + ); + + testWidgets( + 'it should show the the other member image', + (tester) async { + final client = MockClient(); + final clientState = MockClientState(); + final channel = MockChannel(); + final channelState = MockChannelState(); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(channel.state).thenReturn(channelState); + when(channel.client).thenReturn(client); + when(channel.extraDataStream).thenAnswer((i) => Stream.value({ + 'name': 'test', + })); + when(channel.extraData).thenReturn({ + 'name': 'test', + }); + when(channelState.membersStream).thenAnswer((i) => Stream.value([ + Member( + userId: 'user-id', + user: User(id: 'user-id'), + ), + Member( + userId: 'user-id2', + user: User( + id: 'user-id2', + extraData: { + 'image': 'testimage', + }, + ), + ) + ])); + when(channelState.members).thenReturn([ + Member( + userId: 'user-id2', + user: User( + id: 'user-id2', + extraData: { + 'image': 'testimage', + }, + ), + ), + Member( + userId: 'user-id', + user: User(id: 'user-id'), + ) + ]); + when(clientState.usersStream).thenAnswer((i) => Stream.value({ + 'user-id2': User( + id: 'user-id2', + extraData: { + 'image': 'testimage', + }, + ), + })); + when(channel.extraData).thenReturn({ + 'name': 'test', + }); + + await tester.pumpWidget(MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: Scaffold( + body: ChannelImage(), + ), + ), + ), + )); + + final image = + tester.widget(find.byType(CachedNetworkImage)); + expect(image.imageUrl, 'testimage'); + }, + ); + + testWidgets( + 'it should use a groupimage if more than 2 members', + (tester) async { + final client = MockClient(); + final clientState = MockClientState(); + final channel = MockChannel(); + final channelState = MockChannelState(); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(channel.state).thenReturn(channelState); + when(channel.client).thenReturn(client); + when(channel.extraDataStream).thenAnswer((i) => Stream.value({ + 'name': 'test', + })); + when(channel.extraData).thenReturn({ + 'name': 'test', + }); + when(channelState.membersStream).thenAnswer((i) => Stream.value([ + Member( + userId: 'user-id', + user: User( + id: 'user-id', + extraData: { + 'image': 'testimage1', + }, + ), + ), + Member( + userId: 'user-id2', + user: User( + id: 'user-id2', + extraData: { + 'image': 'testimage2', + }, + ), + ), + Member( + userId: 'user-id3', + user: User( + id: 'user-id3', + extraData: { + 'image': 'testimage3', + }, + ), + ), + ])); + when(channelState.members).thenReturn([ + Member( + userId: 'user-id', + user: User( + id: 'user-id', + extraData: { + 'image': 'testimage1', + }, + ), + ), + Member( + userId: 'user-id2', + user: User( + id: 'user-id2', + extraData: { + 'image': 'testimage2', + }, + ), + ), + Member( + userId: 'user-id3', + user: User( + id: 'user-id3', + extraData: { + 'image': 'testimage3', + }, + ), + ), + ]); + + await tester.pumpWidget(MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: Scaffold( + body: ChannelImage(), + ), + ), + ), + )); + + final image = tester.widget(find.byType(GroupImage)); + expect(image.images, [ + 'testimage2', + 'testimage3', + ]); + }, + ); + + testWidgets( + 'using select: true should show a selection border', + (tester) async { + final client = MockClient(); + final clientState = MockClientState(); + final channel = MockChannel(); + final channelState = MockChannelState(); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(channel.state).thenReturn(channelState); + when(channel.client).thenReturn(client); + when(channel.extraDataStream).thenAnswer((i) => Stream.value({ + 'name': 'test', + 'image': 'imagetest', + })); + when(channel.extraData).thenReturn({ + 'name': 'test', + 'image': 'imagetest', + }); + + await tester.pumpWidget(MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: Scaffold( + body: ChannelImage( + selected: true, + ), + ), + ), + ), + )); + + expect(find.byKey(Key('selectedImage')), findsOneWidget); + }, + ); +} diff --git a/packages/stream_chat_flutter/test/src/channel_list_header_test.dart b/packages/stream_chat_flutter/test/src/channel_list_header_test.dart new file mode 100644 index 00000000..6fc4d428 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/channel_list_header_test.dart @@ -0,0 +1,207 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import 'mocks.dart'; + +void main() { + testWidgets( + 'control test', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(client.wsConnectionStatusStream) + .thenAnswer((_) => Stream.value(ConnectionStatus.connected)); + + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + child: Scaffold( + body: ChannelListHeader(), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + final userAvatar = tester.widget(find.byType(UserAvatar)); + expect(userAvatar.user, clientState.user); + expect(find.byType(StreamNeumorphicButton), findsOneWidget); + expect(find.text('Stream Chat'), findsOneWidget); + }, + ); + + testWidgets( + 'it should show the InfoTile message if disconnected', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(client.wsConnectionStatusStream) + .thenAnswer((_) => Stream.value(ConnectionStatus.disconnected)); + + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + child: Scaffold( + body: ChannelListHeader( + showConnectionStateTile: true, + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(find.text('Disconnected'), findsOneWidget); + }, + ); + + testWidgets( + 'it should show the InfoTile message if connecting', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(client.wsConnectionStatusStream) + .thenAnswer((_) => Stream.value(ConnectionStatus.connecting)); + + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + child: Scaffold( + body: ChannelListHeader( + showConnectionStateTile: true, + ), + ), + ), + ), + ); + await tester.pump(); + + expect(find.text('Reconnecting...'), findsOneWidget); + }, + ); + + testWidgets( + 'it should apply passed properties', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(client.wsConnectionStatusStream) + .thenAnswer((_) => Stream.value(ConnectionStatus.connecting)); + + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + child: Scaffold( + body: ChannelListHeader( + titleBuilder: (context, status, client) { + return Text('TITLE'); + }, + subtitle: Text('SUBTITLE'), + leading: Text('LEADING'), + actions: [ + Text('ACTION'), + ], + client: client, + ), + ), + ), + ), + ); + await tester.pump(); + + expect(find.text('TITLE'), findsOneWidget); + expect(find.text('SUBTITLE'), findsOneWidget); + expect(find.text('LEADING'), findsOneWidget); + expect(find.text('ACTION'), findsOneWidget); + }, + ); + + testWidgets( + 'it should apply prenavigationcallback', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(client.wsConnectionStatusStream) + .thenAnswer((_) => Stream.value(ConnectionStatus.connecting)); + + var tapped = false; + + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + child: Scaffold( + body: ChannelListHeader( + preNavigationCallback: () { + tapped = true; + }, + ), + ), + ), + ), + ); + await tester.pump(); + + await tester.tap(find.byType(UserAvatar)); + expect(tapped, true); + }, + ); + + testWidgets( + 'it should apply passed callbacks', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(client.wsConnectionStatusStream) + .thenAnswer((_) => Stream.value(ConnectionStatus.connecting)); + + var tapped = 0; + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + child: Scaffold( + body: ChannelListHeader( + onUserAvatarTap: (u) { + tapped++; + }, + onNewChatButtonTap: () { + tapped++; + }, + ), + ), + ), + ), + ); + await tester.pump(); + + await tester.tap(find.byType(UserAvatar)); + await tester.tap(find.byType(StreamNeumorphicButton)); + expect(tapped, 2); + }, + ); +} diff --git a/packages/stream_chat_flutter/test/src/channel_name_test.dart b/packages/stream_chat_flutter/test/src/channel_name_test.dart new file mode 100644 index 00000000..bf6f7e55 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/channel_name_test.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import 'mocks.dart'; + +void main() { + testWidgets( + 'it should show channel name', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + final channel = MockChannel(); + final channelState = MockChannelState(); + final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(channel.lastMessageAt).thenReturn(lastMessageAt); + when(channel.state).thenReturn(channelState); + when(channel.client).thenReturn(client); + when(channel.isMuted).thenReturn(false); + when(channel.isMutedStream).thenAnswer((i) => Stream.value(false)); + when(channel.extraDataStream).thenAnswer((i) => Stream.value({ + 'name': 'test', + })); + when(channel.extraData).thenReturn({ + 'name': 'test', + }); + when(channelState.unreadCount).thenReturn(1); + when(channelState.unreadCountStream).thenAnswer((i) => Stream.value(1)); + when(channelState.membersStream).thenAnswer((i) => Stream.value([ + Member( + userId: 'user-id', + user: User(id: 'user-id'), + ) + ])); + when(channelState.members).thenReturn([ + Member( + userId: 'user-id', + user: User(id: 'user-id'), + ), + ]); + when(channelState.messages).thenReturn([ + Message( + text: 'hello', + user: User(id: 'other-user'), + ) + ]); + when(channelState.messagesStream).thenAnswer((i) => Stream.value([ + Message( + text: 'hello', + user: User(id: 'other-user'), + ) + ])); + + await tester.pumpWidget(MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: Scaffold( + body: ChannelName(), + ), + ), + ), + )); + + expect(find.text('test'), findsOneWidget); + }, + ); +} diff --git a/packages/stream_chat_flutter/test/src/channel_preview_test.dart b/packages/stream_chat_flutter/test/src/channel_preview_test.dart index 094c74a5..d8ed9f72 100644 --- a/packages/stream_chat_flutter/test/src/channel_preview_test.dart +++ b/packages/stream_chat_flutter/test/src/channel_preview_test.dart @@ -15,6 +15,7 @@ void main() { final channelState = MockChannelState(); final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); + when(channel.cid).thenReturn('cid'); when(client.state).thenReturn(clientState); when(clientState.user).thenReturn(OwnUser(id: 'user-id')); when(channel.lastMessageAt).thenReturn(lastMessageAt); @@ -28,6 +29,9 @@ void main() { when(channel.extraData).thenReturn({ 'name': 'test name', }); + when(clientState.channels).thenReturn({ + channel.cid: channel, + }); when(channelState.unreadCount).thenReturn(1); when(channelState.unreadCountStream).thenAnswer((i) => Stream.value(1)); when(channelState.membersStream).thenAnswer((i) => Stream.value([ diff --git a/packages/stream_chat_flutter/test/src/date_divider_test.dart b/packages/stream_chat_flutter/test/src/date_divider_test.dart new file mode 100644 index 00000000..4856a0d3 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/date_divider_test.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import 'mocks.dart'; + +void main() { + testWidgets( + 'it should show basic channel information', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + + await tester.pumpWidget(MaterialApp( + home: StreamChat( + client: client, + child: Scaffold( + body: DateDivider( + dateTime: DateTime.now(), + ), + ), + ), + )); + + expect(find.text('Today'), findsOneWidget); + }, + ); +} diff --git a/packages/stream_chat_flutter/test/src/deleted_message_test.dart b/packages/stream_chat_flutter/test/src/deleted_message_test.dart new file mode 100644 index 00000000..bb93b28b --- /dev/null +++ b/packages/stream_chat_flutter/test/src/deleted_message_test.dart @@ -0,0 +1,201 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:golden_toolkit/golden_toolkit.dart'; +import 'package:mockito/mockito.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import 'mocks.dart'; + +void main() { + testWidgets( + 'control test', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + + await tester.pumpWidget(MaterialApp( + home: StreamChat( + client: client, + child: Scaffold( + body: DeletedMessage( + messageTheme: MessageTheme( + createdAt: TextStyle( + color: Colors.black, + ), + messageText: TextStyle(), + ), + ), + ), + ), + )); + + expect(find.text('Message deleted'), findsOneWidget); + }, + ); + + testGoldens( + 'control golden light', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + final channel = MockChannel(); + final channelState = MockChannelState(); + final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(channel.lastMessageAt).thenReturn(lastMessageAt); + when(channel.state).thenReturn(channelState); + when(channel.client).thenReturn(client); + when(channel.initialized).thenAnswer((_) => Future.value(true)); + when(channel.extraDataStream).thenAnswer((i) => Stream.value({ + 'name': 'test', + })); + when(channel.extraData).thenReturn({ + 'name': 'test', + }); + + when(clientState.totalUnreadCount).thenReturn(10); + when(clientState.totalUnreadCountStream) + .thenAnswer((i) => Stream.value(10)); + + final materialTheme = ThemeData.light(); + final theme = StreamChatThemeData.fromTheme(materialTheme); + await tester.pumpWidgetBuilder( + materialAppWrapper( + theme: materialTheme, + )( + StreamChat( + streamChatThemeData: theme, + client: client, + child: StreamChannel( + showLoading: false, + channel: channel, + child: Center( + child: DeletedMessage( + messageTheme: theme.ownMessageTheme, + ), + ), + ), + ), + ), + surfaceSize: Size.square(200), + ); + + await screenMatchesGolden(tester, 'deleted_message_light'); + }, + ); + + testGoldens( + 'control golden dark', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + final channel = MockChannel(); + final channelState = MockChannelState(); + final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(channel.lastMessageAt).thenReturn(lastMessageAt); + when(channel.state).thenReturn(channelState); + when(channel.client).thenReturn(client); + when(channel.initialized).thenAnswer((_) => Future.value(true)); + when(channel.extraDataStream).thenAnswer((i) => Stream.value({ + 'name': 'test', + })); + when(channel.extraData).thenReturn({ + 'name': 'test', + }); + + when(clientState.totalUnreadCount).thenReturn(10); + when(clientState.totalUnreadCountStream) + .thenAnswer((i) => Stream.value(10)); + + final materialTheme = ThemeData.dark(); + final theme = StreamChatThemeData.fromTheme(materialTheme); + await tester.pumpWidgetBuilder( + materialAppWrapper( + theme: materialTheme, + )( + StreamChat( + streamChatThemeData: theme, + client: client, + child: StreamChannel( + showLoading: false, + channel: channel, + child: Center( + child: DeletedMessage( + messageTheme: theme.ownMessageTheme, + ), + ), + ), + ), + ), + surfaceSize: Size.square(200), + ); + + await screenMatchesGolden(tester, 'deleted_message_dark'); + }, + ); + + testGoldens( + 'golden customization test', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + final channel = MockChannel(); + final channelState = MockChannelState(); + final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(channel.lastMessageAt).thenReturn(lastMessageAt); + when(channel.state).thenReturn(channelState); + when(channel.client).thenReturn(client); + when(channel.initialized).thenAnswer((_) => Future.value(true)); + when(channel.extraDataStream).thenAnswer((i) => Stream.value({ + 'name': 'test', + })); + when(channel.extraData).thenReturn({ + 'name': 'test', + }); + + when(clientState.totalUnreadCount).thenReturn(10); + when(clientState.totalUnreadCountStream) + .thenAnswer((i) => Stream.value(10)); + + final materialTheme = ThemeData.light(); + final theme = StreamChatThemeData.fromTheme(materialTheme); + await tester.pumpWidgetBuilder( + materialAppWrapper( + theme: materialTheme, + )( + StreamChat( + streamChatThemeData: theme, + client: client, + child: StreamChannel( + showLoading: false, + channel: channel, + child: Center( + child: DeletedMessage( + messageTheme: theme.ownMessageTheme, + reverse: true, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ), + ), + ), + surfaceSize: Size.square(200), + ); + + await screenMatchesGolden(tester, 'deleted_message_custom'); + }, + ); +} diff --git a/packages/stream_chat_flutter/test/src/full_screen_media_test.dart b/packages/stream_chat_flutter/test/src/full_screen_media_test.dart new file mode 100644 index 00000000..55adf7d3 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/full_screen_media_test.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:photo_view/photo_view.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import 'mocks.dart'; + +void main() { + testWidgets( + 'it should show channel typing', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + final channel = MockChannel(); + final channelState = MockChannelState(); + final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(channel.lastMessageAt).thenReturn(lastMessageAt); + when(channel.state).thenReturn(channelState); + when(channel.client).thenReturn(client); + when(channel.isMuted).thenReturn(false); + when(channel.isMutedStream).thenAnswer((i) => Stream.value(false)); + when(channel.extraDataStream).thenAnswer((i) => Stream.value({ + 'name': 'test', + })); + when(channel.extraData).thenReturn({ + 'name': 'test', + }); + when(channelState.membersStream).thenAnswer((i) => Stream.value([ + Member( + userId: 'user-id', + user: User(id: 'user-id'), + ) + ])); + when(channelState.members).thenReturn([ + Member( + userId: 'user-id', + user: User(id: 'user-id'), + ), + ]); + when(channelState.messages).thenReturn([ + Message( + text: 'hello', + user: User(id: 'other-user'), + ) + ]); + when(channelState.messagesStream).thenAnswer((i) => Stream.value([ + Message( + text: 'hello', + user: User(id: 'other-user'), + ) + ])); + + when(channelState.typingEvents).thenAnswer((i) => [ + User(id: 'other-user', extraData: {'name': 'demo'}) + ]); + when(channelState.typingEventsStream).thenAnswer((i) => Stream.value([ + User(id: 'other-user', extraData: {'name': 'demo'}), + User(id: 'other-user', extraData: {'name': 'demo'}), + ])); + + await tester.pumpWidget(MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: FullScreenMedia( + mediaAttachments: [ + Attachment( + type: 'image', + title: 'demo image', + imageUrl: '', + ), + ], + message: Message( + createdAt: DateTime.now(), + ), + sentAt: DateTime.now(), + ), + ), + ), + )); + + expect(find.byType(PhotoView), findsOneWidget); + expect(find.byType(StreamSvgIcon), findsNWidgets(4)); + }, + ); +} diff --git a/packages/stream_chat_flutter/test/src/goldens/deleted_message_custom.png b/packages/stream_chat_flutter/test/src/goldens/deleted_message_custom.png new file mode 100644 index 0000000000000000000000000000000000000000..c464bc01de48fcef06c538bd284ed284370f55aa GIT binary patch literal 2936 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 literal 0 HcmV?d00001 diff --git a/packages/stream_chat_flutter/test/src/goldens/deleted_message_dark.png b/packages/stream_chat_flutter/test/src/goldens/deleted_message_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..7db0a6ef17a3f5586f9a0f7d67b1f49dd6fd5b60 GIT binary patch literal 2785 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 N!A|*klH4Qy{TBco{k#AG literal 0 HcmV?d00001 diff --git a/packages/stream_chat_flutter/test/src/goldens/reaction_bubble_0.png b/packages/stream_chat_flutter/test/src/goldens/reaction_bubble_0.png new file mode 100644 index 0000000000000000000000000000000000000000..12a654b9e6433499e1ff4a3a88d9540f98a8e299 GIT binary patch literal 410 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 literal 0 HcmV?d00001 diff --git a/packages/stream_chat_flutter/test/src/goldens/reaction_bubble_3_dark.png b/packages/stream_chat_flutter/test/src/goldens/reaction_bubble_3_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..2bb180b9394599a9b6533c197f95903b93fbda39 GIT binary patch literal 1976 zcmd5-`8V5%7XMO9q%x(v3bnL!ky;v~idq{%<9V@Ht2Bt$ii};Pqe79YRw%8G>O{*# zbZSZMj4h=m$&^|eZE9DIT^N+a5+?mG=ALu!=iGBYKYZ>z=bq1fceFDEtN{i90D`)N z^pqgwdxGR7mGS1HtpucEJ)IE%qgQiY(#XUjP~IR(B!WUR06;z%g}mr}iz*niiqzz( zvsPzEXB6{)L|GjcoRX8yI1_vTMtzsBdkJj0rKBKTg3bkSX^CldTcBl?*-tIe58ec; z$ioB6t1c^a(s41yvyXz~ee;ngCWfx&)+Qtg!tOcK_IrB@Z1m0m>d5?(*9pKmW5skN zRPjC>dN>GvqBHOkxg!UZc3fG`LHFM@=we`L!&gGFMR_TnU>7DdU?wjo)~Qz+=2QY@ zBwR}Kf-VRz8^~g{DvSWS+z@|mnxdr9)i3?CX_AfTSO;`5un?2(*(7~e7}a|b4-u!z zHc3K(lj#!1!SVzN1V#E$3y6< z6(&`zW2Iy+*K3d<3uF{u3|%nLo({VVW*s?6Apm#A#%wI`19e8j@IAK=O4NFNs>4y~ z8?}0Xy`mAqD`1tgy{OeQ#rdtCH9p?Wv7|+4@FAAIha6$Dm^kOjp(PNE?xBNA_#cKs z=;ro+=fH}~@T|3DKGox+S&nHuQ3w?66cyCFy7SQ56wfhP<7TcqTET~_Xv4^={p_3> z>hF;qa~}4?4U_QAc5>|z!UzNFfAm#^h6Vl$NiDNbht5W7_Dty#e=I1%ja+|O{9=#K z7Ig@jr;{fBPudvAL~jZvD#CY2>=&_4+=i#FrvPn1w~4YWElV~J@dXU6y!}3XS#y-N zw`Y_ntZeLBX`^h`w`5pF7VUlN`&1+LVNaw3G&3+y>lHiRNKcUk0Et(1yFM?*Te zqDa?Mw?wb+0e7~)m5+Z-7=PZ~oMt88*f%x=z<+(85XmG7mj9>*?$|CS%UlzR*hp2^ z-Z7m1wBfeg_6Cm$JNm@^*7R<2j}OEqsnTp}H*b7)FzE@oBktyF>Z4GP!71zH=Wcf5 zmm&IYl0#n?-5kvqyAd58Vqo?DI*;Z3vcI)*Qc+_)zeA)h!?h~P*O{R^{PE-3t~-4f zye;kq z?=qV=T?tm`+L(+HY+y+{&QfGmoI*6dHTvV$qQ80kUbYXGijw<*o|7OKJaO1 zdNA$+DZ@^@b=JV`)%(>TA`LACpH}f&{HqO9X-(IT+3ocUrAMu8pc=`sA$V-pVo=38 zoN-5ZGGeqTrM7IUCjGyjEw1_&CBPS>89jB(*`IJj6H7N)z}=Fh14!s*jzJNj;3$#gbJ^eDoGbwTra z2yBuF|Jc7TQwlI~8YE;v{kr-o5$bPBo7eW6PSTAjR8tAp@WECgUjybyE)6#1ar_uT zC+e!Bk`4lsvzUdC1mXg2kvl;8wPl`R4K2!KHA(y7>|rIU$|KLJ{T+C^P>qKM%=F^u cf2W;nosC%j&<5`3e|Ka6<%mWy5P_-x0IYzKumAu6 literal 0 HcmV?d00001 diff --git a/packages/stream_chat_flutter/test/src/goldens/reaction_bubble_3_light.png b/packages/stream_chat_flutter/test/src/goldens/reaction_bubble_3_light.png new file mode 100644 index 0000000000000000000000000000000000000000..ade5d82bade07e1410fa117fe494b9aa61c33300 GIT binary patch literal 2067 zcmd5-i8tGc6aPA!qKLBfbFOO%7Q4FcV@sTEqwaZjOh@v|Y0 zmf|fbx(!9LibTcPAkGpA5l8&8|HaOGGoLpzpLz4%%)B=Rw_r|}#1zB;0J!Ah4E5k+ z(LV|jM}E`fo{OBDQ@5~V0xpW~$gA=$?cvSK1LUBx{;J!q@( z_g`qV97gKQ{QO@el4Ol-X3fD9kFf9S&)>~<#poLt+>MV1cf~Zee|pJCRiRKQZCzcW zWLAd?OxNS$Jv3US-UD(`3mXhYg6d`c;)}YcHxD2fJJMC;+Wlb$OTJxy<^E0zip<(u zpU|*MKh&p#8|*n9<*r&S<)2+488X7N9IG84-QK{~EhC8}S%>2&utTZj&@Jj#53TcF!bow`~`*r>|y7A!PpgCwS zIvWm$Pq;#q9ZGF%Y$)D~)Oe+^Glfep%&6Xm!6PGP1|_a-_qa6yfA)mpOToESkCn?N(Ap9m52{xX!&&c z79a6f!NFyDLcpihpCzTFnER{OnZbs^EV(vL5aPf#1D-*qt12tn7So&+L zL7t3=r~Y>WfV=#lg7VJh#nIKD0rt-1(zx%AE#bV&5a$+pN?F(Ok`;JBxS5x$^W5$w z0OXfvhr+*Vi)VlTWy#ps_$x8!Lli)Uui$D5vtgituhEoUQo5~*gvHM6u7|NU%& zQ2AT!`oi2CR9YCARkQ*LDygUl0<^yXuGEopP4aumT++|8# zb~oBx=!>$|skmJI5Pfpk676M2TkXf&95D6j5jEeu4>=sqfD}T~m7<6`IbsF+4kLq? z5WDydUDkd={40~OEEUkoCr7m)`Uip4jm0Uq?ZuR<|a2i zRWs!=!?mK5UmAu6h4;2X&-Vug1fD&6W@KZ7)hR?Q-8Vk1tgH<2_+Pn2N_g5i@2p0u zN&L;OcIP$F(fA=XAya_I72oqL$(xG6o@;{sE(|WhVh1@H`uh58lWvK8l9s-qp|&rz z2R|Bx5dLaL!EgQGyZob}VHi5rxi>K>b*RcT&dA!j_}(jlmR}n1K^4tkaY={!qKlk( z*4jAEdP8LP%`qu9k$>_nW{!PpQ7-3LMi2WzlWolCCA>8;<9Sf~W0*E9xDLTB z=Fo0n;>%8!%RUuHW9bNfjX7(!r}Re;F~6v{FgHK+rur-#Egph{n+%Ux))h}k+QIE!LfBx)2ZQLIE?K0H;QPUQcJf6#IrXtr^^5$o4 zQMZr!OQxs27j)CX#CXztQ6_^_Ew0K6>s#oD3I945*_r9- zF!R?kP+*6xt{s$vYjrsrML=1FfLZo~0a>6ifKbgfnIjl}j-%Z7--;nh9bE+IH6 zNiID-9WGS>3-#NkP<-ywT&Fon2V=_F{{H@z=Z^8*@R4KyOV`Y+9(S}iRV-dPx!eBy z;N$W>vy2P8rJJQiMP2vReHua@ye*orG3qW85OC@QcKLU{sFloQ=PO--U>#%rV+ULu KU{JgrBK!Y4#nrhuOx}d}@AHd_rMRJYw#9*xb~w ziPwkXgoHjz9Ee|&clf9q8qS9%osgULQ(IoKToxL^ zV1w<1rk`ZUWax-!7 zA`}tIWHLb+^ewMhb7l1g1;^TJ^Eg#KI!%|L1#umEMy_7N^C|oi?zP0I^Re9B z6E7_REFvC$3bC-T;4x5J?{O^9x1MSo^~L8L%h)~?_yo#EH&W6=nz3j&QE!K zR@hoQv$kV9qv@bgL*Ijb2!+mrI&b@;pBd`p)Hzl`%O0li$I!(9K{ z)3gFYcbLh&2KyJNT!%!bltigg#Aw4a&vqvD11QP1Qy9I3%)LH+j^hMsY{S}n9T6a5flX-Ws_p$+7D8uypG)N^jr(7mS*{*9um`93hm=}*_AVU%sUs%>UtxlSr>#Bhtwez2rRvKm} z-7w6quBH>Gqp!1%sXeQ2$3xWrD2e7*MX=*o?CaMXwA_{qsrvQkK-}f~eK%?yQpVnm zP6{V>$G+Bz!R~}}#%6+3t+6Ui0zp_w5pO;~7Vq!93XdN9_Ewtl74}2!E4e+~5Z1t) zAvUx&Ixty;;c!1JqKg$RLTaIU#*q~iGE7-U+x#T>U-l&kBC%-IDgpmqgMck((UAeU zhRt~G<4l|4Y#XAU57&Ngua zWAwDO(G8u74kymv**y1W*wLPplZ#VdUY4I{c+7EumF)G*4aM&sykB;aLvh~oy@CZA{(rX@ubY^(qV-L; z#%ZCQh8wOe((s<*%E%B9<*dzm|GQv|;*_A<>9f_2CI1!Ai*-xXy=3AVD8dn~+mgs} z{_}(1uiu}H;(jjEV`vb`w$}94TeB}}dp(0RTCb$8ndbeZvQMMyLe}>4Rd(LNI@7&b z92@*Z7&H1rtQ5a4RPz+dSbE|0_xrtyzb&3mIAS=bxMSDdV?6O8m+w`b_n5@8@7jy_ zn3@Q;(6IlHn7lM~#JkO(Zgx^?owKkxhq&)S;T1-o0fTYcX3+q zJ1Zyiaw&@LG~|e0yZMu!nlQtG1OtoRe>PlgI{Vpw z@9qDrtrK2lpIz%J(f0nC|K$?x6bqSovHlWm2GWXaY+JHkFVW%3`1;TM(}xcmdf5*@ z6v&*FWHpzGVcoiQeRkn%U1uzekXY5*a`QCzzn@=Ur`-Ek(etsQ#-!Fn>fyV0?=pVv z-(R0xP|3+~yW$Yb(Fvt`lTSWb!t)^6uvJh?(QD;fzITfA=Fi_BvTtkI?uRG1rVFeM zoFn13%A&CA_3PJ*GtXJxN{Dk*T*9@b@%>?mVw3pabHAR@EM zz^L_^ChKee^(*s%vuDqqESa48!Q3fgp+vyyg)W+`OHDVK*zSmu?{#B{3xB!!CeMMY zSJgnUsXQfC4O`kn-o$%ZZ2kG; z({uhs8n-#(=eXV4>_FQe= zb43oOCzX6_OCKj4HPlExVYk!p!xX3bVqJ}S=dXV+pX#*y^1}ExwJmNOmMzz$93>7} zx>sKK5-Fvq!r?5DEO7q$foBqCH|Fg9-x3$tZr66DxoY3dS?f>#$aH#9?NRG1bR}=e w9~Vgn<`xF~?H~Caw@z2>5U`?ZX>hDwftkO;`riIFV1dTq>FVdQ&MBb@0L&pYlK=n! literal 0 HcmV?d00001 diff --git a/packages/stream_chat_flutter/test/src/goldens/system_message_dark.png b/packages/stream_chat_flutter/test/src/goldens/system_message_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..9055f705ebc7e3c25c3770057b9ffec701c9ac37 GIT binary patch literal 1812 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 Stream.value(false)); + when(channel.extraDataStream).thenAnswer((i) => Stream.value({ + 'name': 'test', + })); + when(channel.extraData).thenReturn({ + 'name': 'test', + }); + + await tester.pumpWidget(MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: WillPopScope( + onWillPop: () async => false, + child: Scaffold( + body: ImageFooter(), + ), + ), + ), + ), + )); + + expect(find.byType(StreamSvgIcon), findsNWidgets(2)); + }, + ); +} diff --git a/packages/stream_chat_flutter/test/src/info_tile_test.dart b/packages/stream_chat_flutter/test/src/info_tile_test.dart new file mode 100644 index 00000000..d304d48e --- /dev/null +++ b/packages/stream_chat_flutter/test/src/info_tile_test.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_portal/flutter_portal.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import 'mocks.dart'; + +void main() { + testWidgets( + 'control test', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + + await tester.pumpWidget(MaterialApp( + home: StreamChat( + client: client, + child: Scaffold( + body: Portal( + child: Container( + child: InfoTile( + showMessage: true, + message: 'message', + child: Text('test'), + ), + ), + ), + ), + ), + )); + + expect(find.text('message'), findsOneWidget); + }, + ); + + testWidgets( + 'it should hide when passing showMessage: false', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + + await tester.pumpWidget(MaterialApp( + home: StreamChat( + client: client, + child: Scaffold( + body: Portal( + child: Container( + child: InfoTile( + showMessage: false, + message: 'message', + child: Text('test'), + ), + ), + ), + ), + ), + )); + + expect(find.text('message'), findsNothing); + }, + ); +} diff --git a/packages/stream_chat_flutter/test/src/message_action_modal_test.dart b/packages/stream_chat_flutter/test/src/message_action_modal_test.dart index 3cb916d1..c744a6bb 100644 --- a/packages/stream_chat_flutter/test/src/message_action_modal_test.dart +++ b/packages/stream_chat_flutter/test/src/message_action_modal_test.dart @@ -38,16 +38,17 @@ void main() { ), ), ); - await tester.pump(); + await tester.pumpAndSettle(); - await tester.pump(Duration(milliseconds: 1000)); expect(find.byKey(Key('MessageWidget')), findsOneWidget); expect(find.text('Thread Reply'), findsOneWidget); + expect(find.text('Reply'), findsOneWidget); expect(find.text('Edit Message'), findsOneWidget); expect(find.text('Delete Message'), findsOneWidget); expect(find.text('Copy Message'), findsOneWidget); }, ); + testWidgets( 'it should show some actions', (WidgetTester tester) async { @@ -84,9 +85,8 @@ void main() { ), ), ); - await tester.pump(); + await tester.pumpAndSettle(); - await tester.pump(Duration(milliseconds: 1000)); expect(find.byKey(Key('MessageWidget')), findsOneWidget); expect(find.text('Reply'), findsNothing); expect(find.text('Thread reply'), findsNothing); @@ -95,4 +95,697 @@ void main() { expect(find.text('Copy message'), findsNothing); }, ); + + testWidgets( + 'it should show custom actions', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + + final themeData = ThemeData(); + final streamTheme = StreamChatThemeData.getDefaultTheme(themeData); + + var tapped = false; + await tester.pumpWidget( + MaterialApp( + theme: themeData, + home: StreamChat( + streamChatThemeData: streamTheme, + client: client, + child: Container( + child: MessageActionsModal( + message: Message( + text: 'test', + user: User( + id: 'user-id', + ), + ), + messageTheme: streamTheme.ownMessageTheme, + customActions: [ + MessageAction( + leading: Icon(Icons.check), + title: Text('title'), + onTap: (m) { + tapped = true; + }, + ), + ], + ), + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.check), findsOneWidget); + expect(find.text('title'), findsOneWidget); + + await tester.tap(find.text('title')); + + expect(tapped, true); + }, + ); + + testWidgets( + 'tapping on reply should call the callback', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + + final themeData = ThemeData(); + final streamTheme = StreamChatThemeData.getDefaultTheme(themeData); + + var tapped = false; + + await tester.pumpWidget( + MaterialApp( + theme: themeData, + home: StreamChat( + streamChatThemeData: streamTheme, + client: client, + child: Container( + child: MessageActionsModal( + onReplyTap: (m) { + return tapped = true; + }, + message: Message( + text: 'test', + user: User( + id: 'user-id', + ), + ), + messageTheme: streamTheme.ownMessageTheme, + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Reply')); + + expect(tapped, true); + }, + ); + + testWidgets( + 'tapping on thread reply should call the callback', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + + final themeData = ThemeData(); + final streamTheme = StreamChatThemeData.getDefaultTheme(themeData); + + var tapped = false; + + await tester.pumpWidget( + MaterialApp( + theme: themeData, + home: StreamChat( + streamChatThemeData: streamTheme, + client: client, + child: Container( + child: MessageActionsModal( + onThreadReplyTap: (m) { + return tapped = true; + }, + message: Message( + text: 'test', + user: User( + id: 'user-id', + ), + ), + messageTheme: streamTheme.ownMessageTheme, + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Thread Reply')); + + expect(tapped, true); + }, + ); + + testWidgets( + 'tapping on edit should show the edit bottom sheet', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + final channel = MockChannel(); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(channel.initialized).thenAnswer((_) => Future.value(true)); + + final themeData = ThemeData(); + final streamTheme = StreamChatThemeData.getDefaultTheme(themeData); + + await tester.pumpWidget( + MaterialApp( + builder: (context, child) { + return StreamChat( + client: client, + streamChatThemeData: streamTheme, + child: child, + ); + }, + theme: themeData, + home: StreamChannel( + showLoading: false, + channel: channel, + child: Container( + child: MessageActionsModal( + message: Message( + text: 'test', + user: User( + id: 'user-id', + ), + ), + messageTheme: streamTheme.ownMessageTheme, + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Edit Message')); + + await tester.pumpAndSettle(); + + expect(find.byType(MessageInput), findsOneWidget); + }, + ); + + testWidgets( + 'tapping on edit should show use the custom builder', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + final channel = MockChannel(); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(channel.initialized).thenAnswer((_) => Future.value(true)); + + final themeData = ThemeData(); + final streamTheme = StreamChatThemeData.getDefaultTheme(themeData); + + await tester.pumpWidget( + MaterialApp( + builder: (context, child) { + return StreamChat( + client: client, + streamChatThemeData: streamTheme, + child: child, + ); + }, + theme: themeData, + home: StreamChannel( + showLoading: false, + channel: channel, + child: Container( + child: MessageActionsModal( + editMessageInputBuilder: (context, m) { + return Text('test'); + }, + message: Message( + text: 'test', + user: User( + id: 'user-id', + ), + ), + messageTheme: streamTheme.ownMessageTheme, + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Edit Message')); + + await tester.pumpAndSettle(); + + expect(find.text('test'), findsOneWidget); + }, + ); + + testWidgets( + 'tapping on copy should use the callback', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + final channel = MockChannel(); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(channel.initialized).thenAnswer((_) => Future.value(true)); + + final themeData = ThemeData(); + final streamTheme = StreamChatThemeData.getDefaultTheme(themeData); + + var tapped = false; + + await tester.pumpWidget( + MaterialApp( + builder: (context, child) { + return StreamChat( + client: client, + streamChatThemeData: streamTheme, + child: child, + ); + }, + theme: themeData, + home: StreamChannel( + showLoading: false, + channel: channel, + child: Container( + child: MessageActionsModal( + onCopyTap: (m) => tapped = true, + message: Message( + text: 'test', + user: User( + id: 'user-id', + ), + ), + messageTheme: streamTheme.ownMessageTheme, + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Copy Message')); + + expect(tapped, true); + }, + ); + + testWidgets( + 'tapping on resend should call send message', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + final channel = MockChannel(); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(channel.initialized).thenAnswer((_) => Future.value(true)); + + final themeData = ThemeData(); + final streamTheme = StreamChatThemeData.getDefaultTheme(themeData); + + await tester.pumpWidget( + MaterialApp( + builder: (context, child) { + return StreamChat( + client: client, + streamChatThemeData: streamTheme, + child: child, + ); + }, + theme: themeData, + home: StreamChannel( + showLoading: false, + channel: channel, + child: Container( + child: MessageActionsModal( + message: Message( + status: MessageSendingStatus.failed, + text: 'test', + user: User( + id: 'user-id', + ), + ), + messageTheme: streamTheme.ownMessageTheme, + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Resend')); + + verify(channel.sendMessage(any)).called(1); + }, + ); + + testWidgets( + 'tapping on resend should call update message if editing the message', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + final channel = MockChannel(); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(channel.initialized).thenAnswer((_) => Future.value(true)); + + final themeData = ThemeData(); + final streamTheme = StreamChatThemeData.getDefaultTheme(themeData); + + await tester.pumpWidget( + MaterialApp( + builder: (context, child) { + return StreamChat( + client: client, + streamChatThemeData: streamTheme, + child: child, + ); + }, + theme: themeData, + home: StreamChannel( + showLoading: false, + channel: channel, + child: Container( + child: MessageActionsModal( + message: Message( + status: MessageSendingStatus.failed_update, + text: 'test', + user: User( + id: 'user-id', + ), + ), + messageTheme: streamTheme.ownMessageTheme, + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Resend Edited Message')); + + verify(channel.updateMessage(any)).called(1); + }, + ); + + testWidgets( + 'tapping on flag message should show the dialog', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + final channel = MockChannel(); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(channel.initialized).thenAnswer((_) => Future.value(true)); + + final themeData = ThemeData(); + final streamTheme = StreamChatThemeData.getDefaultTheme(themeData); + + await tester.pumpWidget( + MaterialApp( + builder: (context, child) { + return StreamChat( + client: client, + streamChatThemeData: streamTheme, + child: child, + ); + }, + theme: themeData, + home: StreamChannel( + showLoading: false, + channel: channel, + child: Container( + child: MessageActionsModal( + message: Message( + id: 'testid', + text: 'test', + user: User( + id: 'user-id', + ), + ), + messageTheme: streamTheme.ownMessageTheme, + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Flag Message')); + await tester.pumpAndSettle(); + + expect(find.text('Flag Message'), findsNWidgets(2)); + + await tester.tap(find.text('FLAG')); + await tester.pumpAndSettle(); + + verify(client.flagMessage('testid')).called(1); + }, + ); + + testWidgets( + 'if flagging a message throws an error the error dialog should appear', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + final channel = MockChannel(); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(channel.initialized).thenAnswer((_) => Future.value(true)); + when(client.flagMessage(any)).thenThrow(ApiError( + '{}', + 500, + )); + + final themeData = ThemeData(); + final streamTheme = StreamChatThemeData.getDefaultTheme(themeData); + + await tester.pumpWidget( + MaterialApp( + builder: (context, child) { + return StreamChat( + client: client, + streamChatThemeData: streamTheme, + child: child, + ); + }, + theme: themeData, + home: StreamChannel( + showLoading: false, + channel: channel, + child: Container( + child: MessageActionsModal( + message: Message( + id: 'testid', + text: 'test', + user: User( + id: 'user-id', + ), + ), + messageTheme: streamTheme.ownMessageTheme, + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Flag Message')); + await tester.pumpAndSettle(); + + expect(find.text('Flag Message'), findsNWidgets(2)); + + await tester.tap(find.text('FLAG')); + await tester.pumpAndSettle(); + + expect(find.text('Something went wrong'), findsOneWidget); + }, + ); + + testWidgets( + 'if flagging an already flagged message no error should appear', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + final channel = MockChannel(); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(channel.initialized).thenAnswer((_) => Future.value(true)); + when(client.flagMessage(any)).thenThrow(ApiError( + '{"code":4}', + 400, + )); + + final themeData = ThemeData(); + final streamTheme = StreamChatThemeData.getDefaultTheme(themeData); + + await tester.pumpWidget( + MaterialApp( + builder: (context, child) { + return StreamChat( + client: client, + streamChatThemeData: streamTheme, + child: child, + ); + }, + theme: themeData, + home: StreamChannel( + showLoading: false, + channel: channel, + child: Container( + child: MessageActionsModal( + message: Message( + id: 'testid', + text: 'test', + user: User( + id: 'user-id', + ), + ), + messageTheme: streamTheme.ownMessageTheme, + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Flag Message')); + await tester.pumpAndSettle(); + + expect(find.text('Flag Message'), findsNWidgets(2)); + + await tester.tap(find.text('FLAG')); + await tester.pumpAndSettle(); + + expect(find.text('Message flagged'), findsOneWidget); + }, + ); + + testWidgets( + 'tapping on delete message should call client.delete', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + final channel = MockChannel(); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(channel.initialized).thenAnswer((_) => Future.value(true)); + + final themeData = ThemeData(); + final streamTheme = StreamChatThemeData.getDefaultTheme(themeData); + + await tester.pumpWidget( + MaterialApp( + builder: (context, child) { + return StreamChat( + client: client, + streamChatThemeData: streamTheme, + child: child, + ); + }, + theme: themeData, + home: StreamChannel( + showLoading: false, + channel: channel, + child: Container( + child: MessageActionsModal( + message: Message( + id: 'testid', + text: 'test', + user: User( + id: 'user-id', + ), + ), + messageTheme: streamTheme.ownMessageTheme, + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Delete Message')); + await tester.pumpAndSettle(); + + expect(find.text('Delete message'), findsOneWidget); + + await tester.tap(find.text('DELETE')); + await tester.pumpAndSettle(); + + verify(channel.deleteMessage(any)).called(1); + }, + ); + + testWidgets( + 'tapping on delete message should call client.delete', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + final channel = MockChannel(); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(channel.initialized).thenAnswer((_) => Future.value(true)); + when(channel.deleteMessage(any)).thenThrow(ApiError( + '{}', + 500, + )); + + final themeData = ThemeData(); + final streamTheme = StreamChatThemeData.getDefaultTheme(themeData); + + await tester.pumpWidget( + MaterialApp( + builder: (context, child) { + return StreamChat( + client: client, + streamChatThemeData: streamTheme, + child: child, + ); + }, + theme: themeData, + home: StreamChannel( + showLoading: false, + channel: channel, + child: Container( + child: MessageActionsModal( + message: Message( + id: 'testid', + text: 'test', + user: User( + id: 'user-id', + ), + ), + messageTheme: streamTheme.ownMessageTheme, + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Delete Message')); + await tester.pumpAndSettle(); + + expect(find.text('Delete message'), findsOneWidget); + + await tester.tap(find.text('DELETE')); + await tester.pumpAndSettle(); + + expect(find.text('Something went wrong'), findsOneWidget); + }, + ); } diff --git a/packages/stream_chat_flutter/test/src/message_input_test.dart b/packages/stream_chat_flutter/test/src/message_input_test.dart new file mode 100644 index 00000000..38e80565 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/message_input_test.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import 'mocks.dart'; + +void main() { + testWidgets( + 'checks message input features', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + final channel = MockChannel(); + final channelState = MockChannelState(); + final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(channel.lastMessageAt).thenReturn(lastMessageAt); + when(channel.state).thenReturn(channelState); + when(channel.client).thenReturn(client); + when(channel.isMuted).thenReturn(false); + when(channel.isMutedStream).thenAnswer((i) => Stream.value(false)); + when(channel.extraDataStream).thenAnswer((i) => Stream.value({ + 'name': 'test', + })); + when(channel.extraData).thenReturn({ + 'name': 'test', + }); + when(channelState.membersStream).thenAnswer((i) => Stream.value([ + Member( + userId: 'user-id', + user: User(id: 'user-id'), + ) + ])); + when(channelState.members).thenReturn([ + Member( + userId: 'user-id', + user: User(id: 'user-id'), + ), + ]); + when(channelState.messages).thenReturn([ + Message( + text: 'hello', + user: User(id: 'other-user'), + ) + ]); + when(channelState.messagesStream).thenAnswer((i) => Stream.value([ + Message( + text: 'hello', + user: User(id: 'other-user'), + ) + ])); + + when(channelState.typingEvents).thenAnswer((i) => [ + User(id: 'other-user', extraData: {'name': 'demo'}) + ]); + when(channelState.typingEventsStream).thenAnswer((i) => Stream.value([ + User(id: 'other-user', extraData: {'name': 'demo'}), + User(id: 'other-user', extraData: {'name': 'demo'}), + ])); + + await tester.pumpWidget(MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: Scaffold( + body: MessageInput(), + ), + ), + ), + )); + + expect(find.byType(TextField), findsOneWidget); + expect(find.byType(StreamSvgIcon), findsNWidgets(8)); + expect(find.byKey(Key('messageInputText')), findsOneWidget); + }, + ); +} diff --git a/packages/stream_chat_flutter/test/src/message_reaction_modal_test.dart b/packages/stream_chat_flutter/test/src/message_reaction_modal_test.dart deleted file mode 100644 index 4e515a09..00000000 --- a/packages/stream_chat_flutter/test/src/message_reaction_modal_test.dart +++ /dev/null @@ -1,102 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/mockito.dart'; -import 'package:stream_chat_flutter/src/message_reactions_modal.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -import 'mocks.dart'; - -void main() { - testWidgets( - 'it should show one thumbs from the picker', - (WidgetTester tester) async { - final client = MockClient(); - final clientState = MockClientState(); - final themeData = ThemeData(); - - when(client.state).thenReturn(clientState); - when(clientState.user).thenReturn(OwnUser(id: 'user-id')); - - final streamTheme = StreamChatThemeData.getDefaultTheme(themeData); - - await tester.pumpWidget( - MaterialApp( - theme: themeData, - home: StreamChat( - client: client, - streamChatThemeData: streamTheme, - child: MessageReactionsModal( - message: Message( - id: 'test', - text: 'test message', - user: User( - id: 'test-user', - ), - ), - messageTheme: streamTheme.ownMessageTheme, - ), - ), - ), - ); - - await tester.pump(Duration(milliseconds: 1000)); - - expect(find.byKey(Key('MessageWidget')), findsOneWidget); - expect(find.byKey(Key('StreamSvgIcon-Icon_thumbs_up_reaction.svg')), - findsOneWidget); - }, - ); - - testWidgets( - 'it should show two reactions', - (WidgetTester tester) async { - final client = MockClient(); - final clientState = MockClientState(); - - when(client.state).thenReturn(clientState); - when(clientState.user).thenReturn(OwnUser(id: 'user-id')); - - final themeData = ThemeData(); - final streamTheme = StreamChatThemeData.getDefaultTheme(themeData); - final testUserId = 'test user'; - - await tester.pumpWidget( - MaterialApp( - theme: themeData, - home: StreamChat( - streamChatThemeData: streamTheme, - client: client, - child: MessageReactionsModal( - message: Message( - text: 'test message', - user: User( - id: 'test-user', - ), - latestReactions: [ - Reaction( - type: 'like', - user: User(id: testUserId), - ), - Reaction( - type: 'love', - user: User(id: testUserId), - ), - ], - ), - messageTheme: streamTheme.ownMessageTheme, - ), - ), - ), - ); - await tester.pump(); - - await tester.pump(Duration(milliseconds: 1000)); - expect(find.byKey(Key('MessageWidget')), findsOneWidget); - expect(find.byKey(Key('StreamSvgIcon-Icon_thumbs_up_reaction.svg')), - findsNWidgets(2)); - expect(find.byKey(Key('StreamSvgIcon-Icon_love_reaction.svg')), - findsNWidgets(2)); - expect(find.text(testUserId.split(' ')[0]), findsNWidgets(2)); - }, - ); -} diff --git a/packages/stream_chat_flutter/test/src/message_reactions_modal_test.dart b/packages/stream_chat_flutter/test/src/message_reactions_modal_test.dart new file mode 100644 index 00000000..044eb693 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/message_reactions_modal_test.dart @@ -0,0 +1,148 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:stream_chat_flutter/src/message_reactions_modal.dart'; +import 'package:stream_chat_flutter/src/reaction_bubble.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import 'mocks.dart'; + +void main() { + testWidgets( + 'control test', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + final themeData = ThemeData(); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + + final streamTheme = StreamChatThemeData.getDefaultTheme(themeData); + + final message = Message( + id: 'test', + text: 'test message', + user: User( + id: 'test-user', + ), + ); + await tester.pumpWidget( + MaterialApp( + theme: themeData, + home: StreamChat( + client: client, + streamChatThemeData: streamTheme, + child: MessageReactionsModal( + message: message, + messageTheme: streamTheme.ownMessageTheme, + ), + ), + ), + ); + + await tester.pump(Duration(milliseconds: 1000)); + + expect(find.byType(MessageWidget), findsOneWidget); + final messageWidget = + tester.widget(find.byType(MessageWidget)); + expect(messageWidget.message, message); + expect(messageWidget.messageTheme, streamTheme.ownMessageTheme); + expect(messageWidget.attachmentBorderRadiusGeometry, null); + expect(messageWidget.showUserAvatar, DisplayWidget.show); + expect(messageWidget.reverse, false); + expect(messageWidget.attachmentShape, null); + expect(messageWidget.shape, null); + expect(messageWidget.onUserAvatarTap, null); + expect(messageWidget.showReactions, false); + expect(messageWidget.showUsername, false); + expect(messageWidget.showThreadReplyIndicator, false); + expect(messageWidget.showTimestamp, false); + expect(messageWidget.translateUserAvatar, false); + expect(messageWidget.showSendingIndicator, false); + + expect(find.byType(ReactionBubble), findsNothing); + + //only one avatar (the message one) + expect(find.byType(UserAvatar), findsOneWidget); + }, + ); + + testWidgets( + 'it should apply passed parameters', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + final themeData = ThemeData(); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + + final streamTheme = StreamChatThemeData.getDefaultTheme(themeData); + + final message = Message( + id: 'test', + text: 'test message', + user: User( + id: 'test-user', + ), + latestReactions: [ + Reaction( + messageId: 'test', + user: User(id: 'testid'), + ), + ], + ); + + final onUserAvatarTap = (u) => print('ok'); + + await tester.pumpWidget( + MaterialApp( + theme: themeData, + home: StreamChat( + client: client, + streamChatThemeData: streamTheme, + child: MessageReactionsModal( + message: message, + messageTheme: streamTheme.ownMessageTheme, + showUserAvatar: DisplayWidget.gone, + reverse: true, + attachmentBorderRadiusGeometry: BorderRadius.circular(1), + attachmentShape: RoundedRectangleBorder(), + showReactions: false, + messageShape: RoundedRectangleBorder(), + onUserAvatarTap: onUserAvatarTap, + ), + ), + ), + ); + + await tester.pump(Duration(milliseconds: 1000)); + + expect(find.byType(MessageWidget), findsOneWidget); + final messageWidget = + tester.widget(find.byType(MessageWidget)); + expect(messageWidget.message, message); + expect(messageWidget.messageTheme, streamTheme.ownMessageTheme); + expect(messageWidget.attachmentBorderRadiusGeometry, + BorderRadius.circular(1)); + expect(messageWidget.showUserAvatar, DisplayWidget.gone); + expect(messageWidget.reverse, true); + expect(messageWidget.showReactions, false); + expect(messageWidget.attachmentShape, RoundedRectangleBorder()); + expect(messageWidget.shape, RoundedRectangleBorder()); + expect(messageWidget.showReactions, false); + expect(messageWidget.showUsername, false); + expect(messageWidget.showThreadReplyIndicator, false); + expect(messageWidget.showTimestamp, false); + expect(messageWidget.translateUserAvatar, false); + expect(messageWidget.showSendingIndicator, false); + + final userAvatar = tester.widget(find.byType(UserAvatar)); + expect(userAvatar.onTap, onUserAvatarTap); + + expect(find.byType(ReactionBubble), findsOneWidget); + expect(find.byType(UserAvatar), findsOneWidget); + }, + ); +} diff --git a/packages/stream_chat_flutter/test/src/message_text_test.dart b/packages/stream_chat_flutter/test/src/message_text_test.dart new file mode 100644 index 00000000..1d2ac8be --- /dev/null +++ b/packages/stream_chat_flutter/test/src/message_text_test.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import 'mocks.dart'; + +void main() { + testWidgets( + 'it should show correct message text', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + final channel = MockChannel(); + final channelState = MockChannelState(); + final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); + final themeData = ThemeData(); + final streamTheme = StreamChatThemeData.getDefaultTheme(themeData); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(channel.lastMessageAt).thenReturn(lastMessageAt); + when(channel.state).thenReturn(channelState); + when(channel.client).thenReturn(client); + when(channel.isMuted).thenReturn(false); + when(channel.isMutedStream).thenAnswer((i) => Stream.value(false)); + when(channel.extraDataStream).thenAnswer((i) => Stream.value({ + 'name': 'test', + })); + when(channel.extraData).thenReturn({ + 'name': 'test', + }); + + await tester.pumpWidget(MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: Scaffold( + body: MessageText( + message: Message( + text: 'demo', + ), + messageTheme: streamTheme.otherMessageTheme), + ), + ), + ), + )); + + expect(find.byType(MarkdownBody), findsOneWidget); + }, + ); +} diff --git a/packages/stream_chat_flutter/test/src/mocks.dart b/packages/stream_chat_flutter/test/src/mocks.dart index 0abbf3ca..50f7ee6a 100644 --- a/packages/stream_chat_flutter/test/src/mocks.dart +++ b/packages/stream_chat_flutter/test/src/mocks.dart @@ -1,3 +1,4 @@ +import 'package:flutter/material.dart'; import 'package:mockito/mockito.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; @@ -8,3 +9,9 @@ class MockClientState extends Mock implements ClientState {} class MockChannel extends Mock implements Channel {} class MockChannelState extends Mock implements ChannelClientState {} + +class MockNavigatorObserver extends Mock implements NavigatorObserver {} + +class MockVoidCallback extends Mock { + void call(); +} diff --git a/packages/stream_chat_flutter/test/src/reaction_bubble_test.dart b/packages/stream_chat_flutter/test/src/reaction_bubble_test.dart index 0b6c90ef..d4a3b6a6 100644 --- a/packages/stream_chat_flutter/test/src/reaction_bubble_test.dart +++ b/packages/stream_chat_flutter/test/src/reaction_bubble_test.dart @@ -1,18 +1,20 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:golden_toolkit/golden_toolkit.dart'; import 'package:mockito/mockito.dart'; import 'package:stream_chat_flutter/src/reaction_bubble.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'mocks.dart'; +import 'simple_frame.dart'; void main() { - testWidgets( + testGoldens( 'it should show no reactions', (WidgetTester tester) async { - await tester.pumpWidget( - MaterialApp( - home: StreamChatTheme( + await tester.pumpWidgetBuilder( + SimpleFrame( + child: StreamChatTheme( data: StreamChatThemeData(), child: Container( child: ReactionBubble( @@ -24,15 +26,173 @@ void main() { ), ), ), + surfaceSize: Size(100, 100), ); - - expect(find.byKey(Key('StreamSvgIcon-Icon_thumbs_up_reaction.svg')), - findsNothing); + await screenMatchesGolden(tester, 'reaction_bubble_0'); }, ); - testWidgets( - 'it should show a like', + testGoldens( + 'it should show a like - light theme', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + final themeData = ThemeData.light(); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + + final theme = StreamChatThemeData.getDefaultTheme(themeData); + await tester.pumpWidgetBuilder( + StreamChat( + client: client, + streamChatThemeData: theme, + child: Container( + child: ReactionBubble( + reactions: [ + Reaction( + type: 'like', + user: User(id: 'test'), + ), + ], + borderColor: theme.ownMessageTheme.reactionsBorderColor, + backgroundColor: theme.ownMessageTheme.reactionsBackgroundColor, + maskColor: theme.ownMessageTheme.reactionsMaskColor, + ), + ), + ), + surfaceSize: Size(100, 100), + ); + await screenMatchesGolden(tester, 'reaction_bubble_like_light'); + }, + ); + + testGoldens( + 'it should show a like - dark theme', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + final themeData = ThemeData.dark(); + final theme = StreamChatThemeData.getDefaultTheme(themeData); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + + await tester.pumpWidgetBuilder( + StreamChat( + client: client, + streamChatThemeData: StreamChatThemeData.getDefaultTheme(themeData), + child: Container( + color: Colors.black, + child: ReactionBubble( + reactions: [ + Reaction( + type: 'like', + user: User(id: 'test'), + ), + ], + borderColor: theme.ownMessageTheme.reactionsBorderColor, + backgroundColor: theme.ownMessageTheme.reactionsBackgroundColor, + maskColor: theme.ownMessageTheme.reactionsMaskColor, + ), + ), + ), + surfaceSize: Size(100, 100), + ); + await screenMatchesGolden(tester, 'reaction_bubble_like_dark'); + }, + ); + + testGoldens( + 'it should show three reactions - light theme', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + final themeData = ThemeData.light(); + final theme = StreamChatThemeData.getDefaultTheme(themeData); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + + await tester.pumpWidgetBuilder( + StreamChat( + client: client, + streamChatThemeData: StreamChatThemeData.getDefaultTheme(themeData), + child: Container( + color: Colors.black, + child: ReactionBubble( + reactions: [ + Reaction( + type: 'like', + user: User(id: 'test'), + ), + Reaction( + type: 'like', + user: User(id: 'user-id'), + ), + Reaction( + type: 'like', + user: User(id: 'test'), + ), + ], + borderColor: theme.ownMessageTheme.reactionsBorderColor, + backgroundColor: theme.ownMessageTheme.reactionsBackgroundColor, + maskColor: theme.ownMessageTheme.reactionsMaskColor, + ), + ), + ), + surfaceSize: Size(140, 140), + ); + await screenMatchesGolden(tester, 'reaction_bubble_3_light'); + }, + ); + + testGoldens( + 'it should show three reactions - dark theme', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + final themeData = ThemeData.dark(); + final theme = StreamChatThemeData.getDefaultTheme(themeData); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + + await tester.pumpWidgetBuilder( + StreamChat( + client: client, + streamChatThemeData: StreamChatThemeData.getDefaultTheme(themeData), + child: Container( + color: Colors.black, + child: ReactionBubble( + reactions: [ + Reaction( + type: 'like', + user: User(id: 'test'), + ), + Reaction( + type: 'like', + user: User(id: 'user-id'), + ), + Reaction( + type: 'like', + user: User(id: 'test'), + ), + ], + borderColor: theme.ownMessageTheme.reactionsBorderColor, + backgroundColor: theme.ownMessageTheme.reactionsBackgroundColor, + maskColor: theme.ownMessageTheme.reactionsMaskColor, + ), + ), + ), + surfaceSize: Size(140, 140), + ); + await screenMatchesGolden(tester, 'reaction_bubble_3_dark'); + }, + ); + + testGoldens( + 'it should show two reactions with customized ui', (WidgetTester tester) async { final client = MockClient(); final clientState = MockClientState(); @@ -41,74 +201,40 @@ void main() { when(client.state).thenReturn(clientState); when(clientState.user).thenReturn(OwnUser(id: 'user-id')); - await tester.pumpWidget( - MaterialApp( - theme: themeData, - home: StreamChat( - client: client, - streamChatThemeData: StreamChatThemeData.getDefaultTheme(themeData), - child: Container( - child: ReactionBubble( - reactions: [ - Reaction( - type: 'like', - user: User(id: 'test'), - ), - ], - borderColor: Colors.black, - backgroundColor: Colors.white, - maskColor: Colors.white, - ), + await tester.pumpWidgetBuilder( + StreamChat( + client: client, + streamChatThemeData: StreamChatThemeData.getDefaultTheme(themeData), + child: Container( + child: ReactionBubble( + reactions: [ + Reaction( + type: 'like', + user: User(id: 'test'), + ), + Reaction( + type: 'love', + user: User(id: 'user-id'), + ), + Reaction( + type: 'unknown', + user: User(id: 'test'), + ), + ], + borderColor: Colors.red, + backgroundColor: Colors.blue, + maskColor: Colors.green, + highlightOwnReactions: true, + reverse: true, + flipTail: true, + tailCirclesSpacing: 4, ), ), ), + surfaceSize: Size(200, 200), ); - expect(find.byKey(Key('StreamSvgIcon-Icon_thumbs_up_reaction.svg')), - findsOneWidget); - }, - ); - testWidgets( - 'it should show two reactions', - (WidgetTester tester) async { - final client = MockClient(); - final clientState = MockClientState(); - final themeData = ThemeData(); - - when(client.state).thenReturn(clientState); - when(clientState.user).thenReturn(OwnUser(id: 'user-id')); - - await tester.pumpWidget( - MaterialApp( - theme: themeData, - home: StreamChat( - client: client, - streamChatThemeData: StreamChatThemeData.getDefaultTheme(themeData), - child: Container( - child: ReactionBubble( - reactions: [ - Reaction( - type: 'like', - user: User(id: 'test'), - ), - Reaction( - type: 'love', - user: User(id: 'test'), - ), - ], - borderColor: Colors.black, - backgroundColor: Colors.white, - maskColor: Colors.white, - ), - ), - ), - ), - ); - - expect(find.byKey(Key('StreamSvgIcon-Icon_thumbs_up_reaction.svg')), - findsOneWidget); - expect(find.byKey(Key('StreamSvgIcon-Icon_love_reaction.svg')), - findsOneWidget); + await screenMatchesGolden(tester, 'reaction_bubble_2'); }, ); } diff --git a/packages/stream_chat_flutter/test/src/simple_frame.dart b/packages/stream_chat_flutter/test/src/simple_frame.dart new file mode 100644 index 00000000..c472df85 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/simple_frame.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; + +class SimpleFrame extends StatelessWidget { + final Widget child; + + const SimpleFrame({Key key, @required this.child}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: const Color(0xFFFFFFFF), + border: Border.all(color: const Color(0xFF9E9E9E)), + ), + child: child, + ); + } +} diff --git a/packages/stream_chat_flutter/test/src/system_message_test.dart b/packages/stream_chat_flutter/test/src/system_message_test.dart new file mode 100644 index 00000000..04967c39 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/system_message_test.dart @@ -0,0 +1,164 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:golden_toolkit/golden_toolkit.dart'; +import 'package:mockito/mockito.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import 'mocks.dart'; + +void main() { + testWidgets( + 'it should show total unread count', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + final channel = MockChannel(); + final channelState = MockChannelState(); + final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(channel.lastMessageAt).thenReturn(lastMessageAt); + when(channel.state).thenReturn(channelState); + when(channel.client).thenReturn(client); + when(channel.extraDataStream).thenAnswer((i) => Stream.value({ + 'name': 'test', + })); + when(channel.extraData).thenReturn({ + 'name': 'test', + }); + + when(clientState.totalUnreadCount).thenReturn(10); + when(clientState.totalUnreadCountStream) + .thenAnswer((i) => Stream.value(10)); + + var tapped = false; + + await tester.pumpWidget(MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: Scaffold( + body: SystemMessage( + onMessageTap: (m) => tapped = true, + message: Message( + text: 'demo message', + ), + ), + ), + ), + ), + )); + + await tester.tap(find.byType(SystemMessage)); + + expect(find.text('demo message'), findsOneWidget); + expect(tapped, true); + }, + ); + + testGoldens( + 'control golden light', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + final channel = MockChannel(); + final channelState = MockChannelState(); + final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(channel.lastMessageAt).thenReturn(lastMessageAt); + when(channel.state).thenReturn(channelState); + when(channel.client).thenReturn(client); + when(channel.initialized).thenAnswer((_) => Future.value(true)); + when(channel.extraDataStream).thenAnswer((i) => Stream.value({ + 'name': 'test', + })); + when(channel.extraData).thenReturn({ + 'name': 'test', + }); + + when(clientState.totalUnreadCount).thenReturn(10); + when(clientState.totalUnreadCountStream) + .thenAnswer((i) => Stream.value(10)); + + await tester.pumpWidgetBuilder( + materialAppWrapper( + theme: ThemeData.light(), + )( + StreamChat( + client: client, + child: StreamChannel( + showLoading: false, + channel: channel, + child: Center( + child: SystemMessage( + message: Message( + text: 'demo message', + ), + ), + ), + ), + ), + ), + surfaceSize: Size.square(200), + ); + + await screenMatchesGolden(tester, 'system_message_light'); + }, + ); + + testGoldens( + 'control golden dark', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + final channel = MockChannel(); + final channelState = MockChannelState(); + final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(channel.lastMessageAt).thenReturn(lastMessageAt); + when(channel.state).thenReturn(channelState); + when(channel.client).thenReturn(client); + when(channel.initialized).thenAnswer((_) => Future.value(true)); + when(channel.extraDataStream).thenAnswer((i) => Stream.value({ + 'name': 'test', + })); + when(channel.extraData).thenReturn({ + 'name': 'test', + }); + + when(clientState.totalUnreadCount).thenReturn(10); + when(clientState.totalUnreadCountStream) + .thenAnswer((i) => Stream.value(10)); + + await tester.pumpWidgetBuilder( + materialAppWrapper( + theme: ThemeData.dark(), + )( + StreamChat( + client: client, + child: StreamChannel( + showLoading: false, + channel: channel, + child: Center( + child: SystemMessage( + message: Message( + text: 'demo message', + ), + ), + ), + ), + ), + ), + surfaceSize: Size.square(200), + ); + + await screenMatchesGolden(tester, 'system_message_dark'); + }, + ); +} diff --git a/packages/stream_chat_flutter/test/src/thread_header_test.dart b/packages/stream_chat_flutter/test/src/thread_header_test.dart new file mode 100644 index 00000000..25987e31 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/thread_header_test.dart @@ -0,0 +1,140 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import 'mocks.dart'; + +void main() { + testWidgets( + 'control test', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + final channel = MockChannel(); + final channelState = MockChannelState(); + final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(channel.lastMessageAt).thenReturn(lastMessageAt); + when(channel.state).thenReturn(channelState); + when(channel.client).thenReturn(client); + when(channel.initialized).thenAnswer((_) => Future.value(true)); + when(channel.isMuted).thenReturn(false); + when(channel.isMutedStream).thenAnswer((i) => Stream.value(false)); + when(channel.extraDataStream).thenAnswer((i) => Stream.value({ + 'name': 'test', + })); + when(channel.extraData).thenReturn({ + 'name': 'test', + }); + when(channelState.unreadCount).thenReturn(1); + when(channelState.unreadCountStream).thenAnswer((i) => Stream.value(1)); + when(channelState.membersStream).thenAnswer((i) => Stream.value([ + Member( + userId: 'user-id', + user: User(id: 'user-id'), + ) + ])); + when(channelState.members).thenReturn([ + Member( + userId: 'user-id', + user: User(id: 'user-id'), + ), + ]); + + await tester.pumpWidget(MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: Scaffold( + body: ThreadHeader( + parent: Message(), + ), + ), + ), + ), + )); + await tester.pumpAndSettle(); + + expect(find.text('with '), findsOneWidget); + expect(find.byType(ChannelName), findsOneWidget); + expect(find.byType(StreamBackButton), findsOneWidget); + expect(find.text('Thread Reply'), findsOneWidget); + }, + ); + + testWidgets( + 'it should apply passed props', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + final channel = MockChannel(); + final channelState = MockChannelState(); + final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(channel.lastMessageAt).thenReturn(lastMessageAt); + when(channel.state).thenReturn(channelState); + when(channel.client).thenReturn(client); + when(channel.initialized).thenAnswer((_) => Future.value(true)); + when(channel.isMuted).thenReturn(false); + when(channel.isMutedStream).thenAnswer((i) => Stream.value(false)); + when(channel.extraDataStream).thenAnswer((i) => Stream.value({ + 'name': 'test', + })); + when(channel.extraData).thenReturn({ + 'name': 'test', + }); + when(channelState.unreadCount).thenReturn(1); + when(channelState.unreadCountStream).thenAnswer((i) => Stream.value(1)); + when(channelState.membersStream).thenAnswer((i) => Stream.value([ + Member( + userId: 'user-id', + user: User(id: 'user-id'), + ) + ])); + when(channelState.members).thenReturn([ + Member( + userId: 'user-id', + user: User(id: 'user-id'), + ), + ]); + + var tapped = false; + await tester.pumpWidget(MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: Scaffold( + body: ThreadHeader( + parent: Message(), + subtitle: Text('subtitle'), + leading: Text('leading'), + title: Text('title'), + onTitleTap: () { + tapped = true; + }, + actions: [ + Text('action'), + ], + ), + ), + ), + ), + )); + await tester.pumpAndSettle(); + + expect(find.text('title'), findsOneWidget); + await tester.tap(find.text('title')); + expect(tapped, true); + expect(find.text('subtitle'), findsOneWidget); + expect(find.text('action'), findsOneWidget); + expect(find.text('leading'), findsOneWidget); + }, + ); +} diff --git a/packages/stream_chat_flutter/test/src/typing_indicator_test.dart b/packages/stream_chat_flutter/test/src/typing_indicator_test.dart new file mode 100644 index 00000000..bbdbeb3e --- /dev/null +++ b/packages/stream_chat_flutter/test/src/typing_indicator_test.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import 'mocks.dart'; + +void main() { + testWidgets( + 'it should show channel typing', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + final channel = MockChannel(); + final channelState = MockChannelState(); + final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(channel.lastMessageAt).thenReturn(lastMessageAt); + when(channel.state).thenReturn(channelState); + when(channel.client).thenReturn(client); + when(channel.isMuted).thenReturn(false); + when(channel.isMutedStream).thenAnswer((i) => Stream.value(false)); + when(channel.extraDataStream).thenAnswer((i) => Stream.value({ + 'name': 'test', + })); + when(channel.extraData).thenReturn({ + 'name': 'test', + }); + when(channelState.membersStream).thenAnswer((i) => Stream.value([ + Member( + userId: 'user-id', + user: User(id: 'user-id'), + ) + ])); + when(channelState.members).thenReturn([ + Member( + userId: 'user-id', + user: User(id: 'user-id'), + ), + ]); + when(channelState.messages).thenReturn([ + Message( + text: 'hello', + user: User(id: 'other-user'), + ) + ]); + when(channelState.messagesStream).thenAnswer((i) => Stream.value([ + Message( + text: 'hello', + user: User(id: 'other-user'), + ) + ])); + + when(channelState.typingEvents).thenAnswer((i) => [ + User(id: 'other-user', extraData: {'name': 'demo'}) + ]); + when(channelState.typingEventsStream).thenAnswer((i) => Stream.value([ + User(id: 'other-user', extraData: {'name': 'demo'}), + User(id: 'other-user', extraData: {'name': 'demo'}), + ])); + + await tester.pumpWidget(MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: Scaffold( + body: TypingIndicator(), + ), + ), + ), + )); + + expect(find.byKey(Key('typings')), findsOneWidget); + }, + ); +} diff --git a/packages/stream_chat_flutter/test/src/unread_indicator_test.dart b/packages/stream_chat_flutter/test/src/unread_indicator_test.dart new file mode 100644 index 00000000..87018263 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/unread_indicator_test.dart @@ -0,0 +1,127 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import 'mocks.dart'; + +void main() { + testWidgets( + 'it should show total unread count', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + final channel = MockChannel(); + final channelState = MockChannelState(); + final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(channel.lastMessageAt).thenReturn(lastMessageAt); + when(channel.state).thenReturn(channelState); + when(channel.client).thenReturn(client); + when(channel.extraDataStream).thenAnswer((i) => Stream.value({ + 'name': 'test', + })); + when(channel.extraData).thenReturn({ + 'name': 'test', + }); + + when(clientState.totalUnreadCount).thenReturn(10); + when(clientState.totalUnreadCountStream) + .thenAnswer((i) => Stream.value(10)); + + await tester.pumpWidget(MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: Scaffold( + body: UnreadIndicator(), + ), + ), + ), + )); + + expect(find.text('10'), findsOneWidget); + }, + ); + + testWidgets( + 'it should show nothing if no unread messages', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + final channel = MockChannel(); + final channelState = MockChannelState(); + when(channel.cid).thenReturn('cid'); + final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(clientState.channels).thenReturn({ + channel.cid: channel, + }); + when(channel.lastMessageAt).thenReturn(lastMessageAt); + when(channel.state).thenReturn(channelState); + when(channel.client).thenReturn(client); + when(channelState.unreadCount).thenReturn(0); + when(channelState.unreadCountStream).thenAnswer((i) => Stream.value(0)); + + await tester.pumpWidget(MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: Scaffold( + body: UnreadIndicator( + cid: channel.cid, + ), + ), + ), + ), + )); + + expect(find.byType(SizedBox), findsOneWidget); + }, + ); + + testWidgets( + 'it should show 99+ if more than 99 unreads', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + final channel = MockChannel(); + when(channel.cid).thenReturn('cid'); + final channelState = MockChannelState(); + final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(clientState.channels).thenReturn({ + channel.cid: channel, + }); + when(channel.lastMessageAt).thenReturn(lastMessageAt); + when(channel.state).thenReturn(channelState); + when(channel.client).thenReturn(client); + when(channelState.unreadCount).thenReturn(100); + when(channelState.unreadCountStream).thenAnswer((i) => Stream.value(100)); + + await tester.pumpWidget(MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: Scaffold( + body: UnreadIndicator( + cid: channel.cid, + ), + ), + ), + ), + )); + + expect(find.text('99+'), findsOneWidget); + }, + ); +}