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 00000000..c464bc01 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/goldens/deleted_message_custom.png differ 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 00000000..7db0a6ef Binary files /dev/null and b/packages/stream_chat_flutter/test/src/goldens/deleted_message_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/goldens/deleted_message_light.png b/packages/stream_chat_flutter/test/src/goldens/deleted_message_light.png new file mode 100644 index 00000000..d5a453ff Binary files /dev/null and b/packages/stream_chat_flutter/test/src/goldens/deleted_message_light.png differ 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 00000000..12a654b9 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/goldens/reaction_bubble_0.png differ diff --git a/packages/stream_chat_flutter/test/src/goldens/reaction_bubble_2.png b/packages/stream_chat_flutter/test/src/goldens/reaction_bubble_2.png new file mode 100644 index 00000000..48bb62da Binary files /dev/null and b/packages/stream_chat_flutter/test/src/goldens/reaction_bubble_2.png differ 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 00000000..2bb180b9 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/goldens/reaction_bubble_3_dark.png differ 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 00000000..ade5d82b Binary files /dev/null and b/packages/stream_chat_flutter/test/src/goldens/reaction_bubble_3_light.png differ diff --git a/packages/stream_chat_flutter/test/src/goldens/reaction_bubble_like_dark.png b/packages/stream_chat_flutter/test/src/goldens/reaction_bubble_like_dark.png new file mode 100644 index 00000000..5a862031 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/goldens/reaction_bubble_like_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/goldens/reaction_bubble_like_light.png b/packages/stream_chat_flutter/test/src/goldens/reaction_bubble_like_light.png new file mode 100644 index 00000000..47f78e30 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/goldens/reaction_bubble_like_light.png differ 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 00000000..9055f705 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/goldens/system_message_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/goldens/system_message_light.png b/packages/stream_chat_flutter/test/src/goldens/system_message_light.png new file mode 100644 index 00000000..d82d8d60 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/goldens/system_message_light.png differ diff --git a/packages/stream_chat_flutter/test/src/image_footer_test.dart b/packages/stream_chat_flutter/test/src/image_footer_test.dart new file mode 100644 index 00000000..2fe72aaa --- /dev/null +++ b/packages/stream_chat_flutter/test/src/image_footer_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 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', + }); + + 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); + }, + ); +}