test: add tests for stream chat flutter core (#354)

* test(core): add tests for channels bloc

Signed-off-by: Sahil Kumar <xdsahil@gmail.com>

* test(core): refactor some tests due to changes in channels bloc

Signed-off-by: Sahil Kumar <xdsahil@gmail.com>

* test(core): add tests for channel list core

Signed-off-by: Sahil Kumar <xdsahil@gmail.com>

* test(core): add tests for users bloc, minor improvements

Signed-off-by: Sahil Kumar <xdsahil@gmail.com>

* test(core): add tests for user list core, minor improvements

Signed-off-by: Sahil Kumar <xdsahil@gmail.com>

* test(core): add tests for message search bloc, refactor

Signed-off-by: Sahil Kumar <xdsahil@gmail.com>

* test(core): add tests for message search list core, refactor

Signed-off-by: Sahil Kumar <xdsahil@gmail.com>

* test(core): add tests for lazy load scroll view

Signed-off-by: Sahil Kumar <xdsahil@gmail.com>

* test(core): add tests for stream chat core

Signed-off-by: Sahil Kumar <xdsahil@gmail.com>

* test(core): add tests for message list core

Signed-off-by: Sahil Kumar <xdsahil@gmail.com>

* test(core): add tests for stream channel

Signed-off-by: Sahil Kumar <xdsahil@gmail.com>

* Fix merge conflicts

Signed-off-by: Sahil Kumar <xdsahil@gmail.com>

* flutter format

Signed-off-by: Sahil Kumar <xdsahil@gmail.com>

* fixes

Signed-off-by: Sahil Kumar <xdsahil@gmail.com>

* update workflow

Signed-off-by: Sahil Kumar <xdsahil@gmail.com>

* chore(core): fix analyzer issues

Signed-off-by: Sahil Kumar <xdsahil@gmail.com>

* chore(ui-kit): fix analyzer issues

Signed-off-by: Sahil Kumar <xdsahil@gmail.com>
This commit is contained in:
Sahil Kumar
2021-03-30 13:46:12 +05:30
committed by GitHub
parent cba0b1a391
commit 420d6384c4
35 changed files with 4867 additions and 413 deletions
@@ -59,7 +59,7 @@ jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 5
timeout-minutes: 15
steps:
- uses: actions/checkout@v2
with:
@@ -98,7 +98,7 @@ jobs:
- uses: VeryGoodOpenSource/very_good_coverage@v1.1.1
with:
path: packages/stream_chat_flutter_core/coverage/lcov.info
min_coverage: 4.5
min_coverage: 90
- uses: VeryGoodOpenSource/very_good_coverage@v1.1.1
with:
path: packages/stream_chat_flutter/coverage/lcov.info
@@ -146,7 +146,7 @@ class Channel {
/// Channel extra data
Map<String, dynamic> get extraData =>
state?._channelState?.channel?.extraData;
state?._channelState?.channel?.extraData ?? _extraData;
/// Channel extra data as a stream
Stream<Map<String, dynamic>> get extraDataStream =>
@@ -104,4 +104,25 @@ class PaginationParams {
lessThan: lessThan ?? this.lessThan,
lessThanOrEqual: lessThanOrEqual ?? this.lessThanOrEqual,
);
@override
int get hashCode =>
runtimeType.hashCode ^
limit.hashCode ^
offset.hashCode ^
greaterThan.hashCode ^
greaterThanOrEqual.hashCode ^
lessThan.hashCode ^
lessThanOrEqual.hashCode;
@override
bool operator ==(covariant PaginationParams other) =>
identical(this, other) ||
runtimeType == other.runtimeType &&
limit == other.limit &&
offset == other.offset &&
greaterThan == other.greaterThan &&
greaterThanOrEqual == other.greaterThanOrEqual &&
lessThan == other.lessThan &&
lessThanOrEqual == other.lessThanOrEqual;
}
@@ -143,7 +143,7 @@ class _ChannelFileDisplayScreenState extends State<ChannelFileDisplayScreen> {
}
return LazyLoadScrollView(
onEndOfPage: () => messageSearchBloc.loadMore(
onEndOfPage: () => messageSearchBloc.search(
filter: {
'cid': {
r'$in': [StreamChannel.of(context).channel.cid]
@@ -166,7 +166,7 @@ class _ChannelMediaDisplayScreenState extends State<ChannelMediaDisplayScreen> {
}
return LazyLoadScrollView(
onEndOfPage: () => messageSearchBloc.loadMore(
onEndOfPage: () => messageSearchBloc.search(
filter: {
'cid': {
r'$in': [StreamChannel.of(context).channel.cid]
@@ -262,9 +262,7 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
);
if (widget.pullToRefresh) {
child = RefreshIndicator(
onRefresh: () async {
_messageSearchListController.loadData();
},
onRefresh: () => _messageSearchListController.loadData(),
child: child,
);
}
@@ -193,9 +193,7 @@ class _UserListViewState extends State<UserListView>
return child;
} else {
return RefreshIndicator(
onRefresh: () async {
_userListController.loadData();
},
onRefresh: () => _userListController.loadData(),
child: child,
);
}
@@ -44,6 +44,11 @@ dependencies:
path_provider: ^1.6.27
video_thumbnail: ^0.2.5+1
dependency_overrides:
stream_chat_flutter_core:
path: ../stream_chat_flutter_core
flutter:
assets:
- images/
@@ -130,10 +130,11 @@ class ChannelListCore extends StatefulWidget {
final PaginationParams pagination;
@override
_ChannelListCoreState createState() => _ChannelListCoreState();
ChannelListCoreState createState() => ChannelListCoreState();
}
class _ChannelListCoreState extends State<ChannelListCore> {
/// The current state of the [ChannelListCore].
class ChannelListCoreState extends State<ChannelListCore> {
@override
Widget build(BuildContext context) {
final channelsBloc = ChannelsBloc.of(context);
@@ -148,7 +149,7 @@ class _ChannelListCoreState extends State<ChannelListCore> {
stream: channelsBlocState.channelsStream,
builder: (context, snapshot) {
if (snapshot.hasError) {
return _buildErrorWidget(snapshot, context, channelsBlocState);
return widget.errorBuilder(context, snapshot.error);
}
if (!snapshot.hasData) {
return widget.loadingBuilder(context);
@@ -161,18 +162,7 @@ class _ChannelListCoreState extends State<ChannelListCore> {
},
);
Widget _buildErrorWidget(
AsyncSnapshot<List<Channel>> snapshot,
BuildContext context,
ChannelsBlocState channelsBlocState,
) {
if (snapshot.error is Error) {
print((snapshot.error as Error).stackTrace);
}
return widget.errorBuilder(context, snapshot.error);
}
/// Fetches initial channels and updates the widget
Future<void> loadData() {
final channelsBloc = ChannelsBloc.of(context);
return channelsBloc.queryChannels(
@@ -183,6 +173,7 @@ class _ChannelListCoreState extends State<ChannelListCore> {
);
}
/// Fetches more channels with updated pagination and updates the widget
Future<void> paginateData() {
final channelsBloc = ChannelsBloc.of(context);
return channelsBloc.queryChannels(
@@ -223,9 +214,9 @@ class _ChannelListCoreState extends State<ChannelListCore> {
if (widget.filter?.toString() != oldWidget.filter?.toString() ||
jsonEncode(widget.sort) != jsonEncode(oldWidget.sort) ||
widget.options?.toString() != oldWidget.options?.toString() ||
widget.pagination?.toJson()?.toString() !=
oldWidget.pagination?.toJson()?.toString() ||
widget.options?.toString() != oldWidget.options?.toString()) {
oldWidget.pagination?.toJson()?.toString()) {
loadData();
}
}
@@ -74,10 +74,9 @@ class ChannelsBlocState extends State<ChannelsBloc>
/// The current channel list as a stream
Stream<List<Channel>> get channelsStream => _channelsController.stream;
final BehaviorSubject<bool> _queryChannelsLoadingController =
BehaviorSubject.seeded(false);
final _queryChannelsLoadingController = BehaviorSubject.seeded(false);
final BehaviorSubject<List<Channel>> _channelsController = BehaviorSubject();
final _channelsController = BehaviorSubject<List<Channel>>();
/// The stream notifying the state of queryChannel call
Stream<bool> get queryChannelsLoading =>
@@ -91,15 +90,14 @@ class ChannelsBlocState extends State<ChannelsBloc>
List<SortOption<ChannelModel>> sortOptions,
PaginationParams paginationParams,
Map<String, dynamic> options,
bool onlyOffline = false,
}) async {
final client = StreamChatCore.of(context).client;
if (client.state?.user == null ||
_queryChannelsLoadingController.value == true) {
return;
if (_queryChannelsLoadingController.value == true) return;
if (_channelsController.hasValue) {
_queryChannelsLoadingController.add(true);
}
_queryChannelsLoadingController.sink.add(true);
try {
final clear = paginationParams == null ||
@@ -115,15 +113,20 @@ class ChannelsBlocState extends State<ChannelsBloc>
if (clear) {
_channelsController.add(channels);
} else {
final l = oldChannels + channels;
_channelsController.add(l);
final temp = oldChannels + channels;
_channelsController.add(temp);
}
if (_channelsController.hasValue &&
_queryChannelsLoadingController.value) {
_queryChannelsLoadingController.sink.add(false);
}
_queryChannelsLoadingController.sink.add(false);
}
} catch (err, stackTrace) {
print(err);
print(stackTrace);
_queryChannelsLoadingController.addError(err, stackTrace);
} catch (e, stk) {
if (_channelsController.hasValue) {
_queryChannelsLoadingController.addError(e, stk);
} else {
_channelsController.addError(e, stk);
}
}
}
@@ -139,15 +142,14 @@ class ChannelsBlocState extends State<ChannelsBloc>
_subscriptions.add(client.on(EventType.messageNew).listen((e) {
final newChannels = List<Channel>.from(channels ?? []);
final index = newChannels.indexWhere((c) => c.cid == e.cid);
if (index > -1) {
if (index != -1) {
if (index > 0) {
final channel = newChannels.removeAt(index);
newChannels.insert(0, channel);
}
} else if (widget.shouldAddChannel != null &&
widget.shouldAddChannel(e)) {
} else if (widget.shouldAddChannel?.call(e) == true) {
final hiddenIndex = _hiddenChannels.indexWhere((c) => c.cid == e.cid);
if (hiddenIndex > -1) {
if (hiddenIndex != -1) {
newChannels.insert(0, _hiddenChannels[hiddenIndex]);
_hiddenChannels.removeAt(hiddenIndex);
} else {
@@ -1,8 +1,7 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
// ignore: constant_identifier_names
enum _LoadingStatus { LOADING, STABLE }
enum _LoadingStatus { loading, stable }
/// Wrapper around a [Scrollable] which triggers [onEndOfPage]/[onStartOfPage] the Scrollable
/// reaches to the start or end of the view extent.
@@ -47,16 +46,16 @@ class LazyLoadScrollView extends StatefulWidget {
}
class _LazyLoadScrollViewState extends State<LazyLoadScrollView> {
_LoadingStatus _loadMoreStatus = _LoadingStatus.STABLE;
double _scrollPosition = 0;
_LoadingStatus _loadMoreStatus = _LoadingStatus.stable;
@override
Widget build(BuildContext context) => NotificationListener(
Widget build(BuildContext context) =>
NotificationListener<ScrollNotification>(
onNotification: _onNotification,
child: widget.child,
);
bool _onNotification(Notification notification) {
bool _onNotification(ScrollNotification notification) {
if (notification is ScrollStartNotification) {
if (widget.onPageScrollStart != null) {
widget.onPageScrollStart();
@@ -73,7 +72,7 @@ class _LazyLoadScrollViewState extends State<LazyLoadScrollView> {
final pixels = notification.metrics.pixels;
final maxScrollExtent = notification.metrics.maxScrollExtent;
final minScrollExtent = notification.metrics.minScrollExtent;
final scrollOffset = widget.scrollOffset;
final scrollOffset = widget.scrollOffset ?? 0;
if (pixels > (minScrollExtent + scrollOffset) &&
pixels < (maxScrollExtent - scrollOffset)) {
@@ -85,28 +84,15 @@ class _LazyLoadScrollViewState extends State<LazyLoadScrollView> {
final extentBefore = notification.metrics.extentBefore;
final extentAfter = notification.metrics.extentAfter;
final scrollingDown = _scrollPosition < pixels;
if (scrollOffset == null || scrollOffset == 0) {
if (extentAfter == 0) {
_onEndOfPage();
}
if (extentBefore == 0) {
_onStartOfPage();
}
} else {
if (scrollingDown) {
if (extentAfter <= scrollOffset) {
_onEndOfPage();
}
} else {
if (extentBefore <= scrollOffset) {
_onStartOfPage();
}
}
if (extentAfter <= scrollOffset) {
_onEndOfPage();
return true;
}
if (extentBefore <= scrollOffset) {
_onStartOfPage();
return true;
}
_scrollPosition = pixels;
return true;
}
if (notification is OverscrollNotification) {
if (notification.overscroll > 0) {
@@ -121,22 +107,22 @@ class _LazyLoadScrollViewState extends State<LazyLoadScrollView> {
}
void _onEndOfPage() {
if (_loadMoreStatus != null && _loadMoreStatus == _LoadingStatus.STABLE) {
_loadMoreStatus = _LoadingStatus.LOADING;
if (_loadMoreStatus != null && _loadMoreStatus == _LoadingStatus.stable) {
_loadMoreStatus = _LoadingStatus.loading;
if (widget.onEndOfPage != null) {
widget.onEndOfPage().whenComplete(() {
_loadMoreStatus = _LoadingStatus.STABLE;
_loadMoreStatus = _LoadingStatus.stable;
});
}
}
}
void _onStartOfPage() {
if (_loadMoreStatus != null && _loadMoreStatus == _LoadingStatus.STABLE) {
_loadMoreStatus = _LoadingStatus.LOADING;
if (_loadMoreStatus != null && _loadMoreStatus == _LoadingStatus.stable) {
_loadMoreStatus = _LoadingStatus.loading;
if (widget.onStartOfPage != null) {
widget.onStartOfPage().whenComplete(() {
_loadMoreStatus = _LoadingStatus.STABLE;
_loadMoreStatus = _LoadingStatus.stable;
});
}
}
@@ -114,28 +114,28 @@ class MessageListCore extends StatefulWidget {
final bool Function(Message) messageFilter;
@override
_MessageListCoreState createState() => _MessageListCoreState();
MessageListCoreState createState() => MessageListCoreState();
}
class _MessageListCoreState extends State<MessageListCore> {
StreamChannelState streamChannel;
/// The current state of the [MessageListCore].
class MessageListCoreState extends State<MessageListCore> {
StreamChannelState _streamChannel;
bool get _upToDate => streamChannel.channel.state.isUpToDate;
bool get _upToDate => _streamChannel.channel.state.isUpToDate;
bool get _isThreadConversation => widget.parentMessage != null;
OwnUser get _currentUser => streamChannel.channel.client.state.user;
OwnUser get _currentUser => _streamChannel.channel.client.state.user;
List<Message> messages = <Message>[];
bool initialMessageHighlightComplete = false;
var _messages = <Message>[];
@override
Widget build(BuildContext context) {
final messagesStream = _isThreadConversation
? streamChannel.channel.state.threadsStream
? _streamChannel.channel.state.threadsStream
.where((threads) => threads.containsKey(widget.parentMessage.id))
.map((threads) => threads[widget.parentMessage.id])
: streamChannel.channel.state?.messagesStream;
: _streamChannel.channel.state?.messagesStream;
bool defaultFilter(Message m) {
final isMyMessage = m.user.id == _currentUser.id;
@@ -148,10 +148,10 @@ class _MessageListCoreState extends State<MessageListCore> {
stream: messagesStream?.map((messages) =>
messages?.where(widget.messageFilter ?? defaultFilter)?.toList()),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return widget.loadingBuilder(context);
} else if (snapshot.hasError) {
if (snapshot.hasError) {
return widget.errorWidgetBuilder(context, snapshot.error);
} else if (!snapshot.hasData) {
return widget.loadingBuilder(context);
} else {
final messageList = snapshot.data?.reversed?.toList() ?? [];
if (messageList.isEmpty && !_isThreadConversation) {
@@ -159,29 +159,32 @@ class _MessageListCoreState extends State<MessageListCore> {
return widget.emptyBuilder(context);
}
} else {
messages = messageList;
_messages = messageList;
}
return widget.messageListBuilder(context, messages);
return widget.messageListBuilder(context, _messages);
}
},
);
}
/// Fetches more messages with updated pagination and updates the widget.
///
/// Optionally pass the fetch direction, defaults to [QueryDirection.bottom]
Future<void> paginateData(
{QueryDirection direction = QueryDirection.bottom}) {
if (!_isThreadConversation) {
return streamChannel.queryMessages(direction: direction);
return _streamChannel.queryMessages(direction: direction);
} else {
return streamChannel.getReplies(widget.parentMessage.id);
return _streamChannel.getReplies(widget.parentMessage.id);
}
}
@override
void initState() {
streamChannel = StreamChannel.of(context);
_streamChannel = StreamChannel.of(context);
if (_isThreadConversation) {
streamChannel.getReplies(widget.parentMessage.id);
_streamChannel.getReplies(widget.parentMessage.id);
}
if (widget.messageListController != null) {
@@ -194,7 +197,7 @@ class _MessageListCoreState extends State<MessageListCore> {
@override
void dispose() {
if (!_upToDate) {
streamChannel.reloadChannel();
_streamChannel.reloadChannel();
}
super.dispose();
}
@@ -203,5 +206,5 @@ class _MessageListCoreState extends State<MessageListCore> {
/// Controller used for paginating data in [ChannelListView]
class MessageListController {
/// Call this function to load further data
Function({QueryDirection direction}) paginateData;
Future<void> Function({QueryDirection direction}) paginateData;
}
@@ -48,17 +48,16 @@ class MessageSearchBlocState extends State<MessageSearchBloc>
Stream<List<GetMessageResponse>> get messagesStream =>
_messageResponses.stream;
final BehaviorSubject<List<GetMessageResponse>> _messageResponses =
BehaviorSubject();
final _messageResponses = BehaviorSubject<List<GetMessageResponse>>();
final BehaviorSubject<bool> _queryMessagesLoadingController =
BehaviorSubject.seeded(false);
final _queryMessagesLoadingController = BehaviorSubject.seeded(false);
/// The stream notifying the state of queryUsers call
Stream<bool> get queryMessagesLoading =>
_queryMessagesLoadingController.stream;
/// Calls [StreamChatClient.search] updating [messageResponses] stream
/// Calls [StreamChatClient.search] updating
/// [messagesStream] and [queryMessagesLoading] stream
Future<void> search({
Map<String, dynamic> filter,
Map<String, dynamic> messageFilter,
@@ -66,33 +65,13 @@ class MessageSearchBlocState extends State<MessageSearchBloc>
String query,
PaginationParams pagination,
}) async {
_messageResponses.add(null);
try {
final messages = await _search(
filter: filter,
messageFilter: messageFilter,
sort: sort,
query: query,
pagination: pagination,
);
_messageResponses.add(messages.results);
} catch (err, stk) {
_messageResponses.addError(err, stk);
}
}
final client = StreamChatCore.of(context).client;
/// Calls [StreamChatClient.search] updating [queryMessagesLoading] stream
Future<void> loadMore({
Map<String, dynamic> filter,
Map<String, dynamic> messageFilter,
List<SortOption> sort,
String query,
PaginationParams pagination,
}) async {
if (_queryMessagesLoadingController.value == true) {
return;
if (_queryMessagesLoadingController.value == true) return;
if (_messageResponses.hasValue) {
_queryMessagesLoadingController.add(true);
}
_queryMessagesLoadingController.add(true);
try {
final clear = pagination == null ||
pagination.offset == null ||
@@ -100,12 +79,12 @@ class MessageSearchBlocState extends State<MessageSearchBloc>
final oldMessages = List<GetMessageResponse>.from(messageResponses ?? []);
final messages = await _search(
filter: filter,
messageFilter: messageFilter,
final messages = await client.search(
filter,
sort: sort,
query: query,
pagination: pagination,
paginationParams: pagination,
messageFilters: messageFilter,
);
if (clear) {
@@ -114,30 +93,18 @@ class MessageSearchBlocState extends State<MessageSearchBloc>
final temp = oldMessages + messages.results;
_messageResponses.add(temp);
}
_queryMessagesLoadingController.add(false);
} catch (err, stackTrace) {
_queryMessagesLoadingController.addError(err, stackTrace);
if (_messageResponses.hasValue && _queryMessagesLoadingController.value) {
_queryMessagesLoadingController.add(false);
}
} catch (e, stk) {
if (_messageResponses.hasValue) {
_queryMessagesLoadingController.addError(e, stk);
} else {
_messageResponses.addError(e, stk);
}
}
}
Future<SearchMessagesResponse> _search({
Map<String, dynamic> filter,
Map<String, dynamic> messageFilter,
List<SortOption> sort,
String query,
PaginationParams pagination,
}) {
final client = StreamChatCore.of(context).client;
return client.search(
filter,
sort: sort,
query: query,
paginationParams: pagination,
messageFilters: messageFilter,
);
}
@override
Widget build(BuildContext context) {
super.build(context);
@@ -104,21 +104,15 @@ class MessageSearchListCore extends StatefulWidget {
final WidgetBuilder loadingBuilder;
@override
_MessageSearchListCoreState createState() => _MessageSearchListCoreState();
MessageSearchListCoreState createState() => MessageSearchListCoreState();
}
class _MessageSearchListCoreState extends State<MessageSearchListCore> {
/// The current state of the [MessageSearchListCore].
class MessageSearchListCoreState extends State<MessageSearchListCore> {
@override
void didChangeDependencies() {
super.didChangeDependencies();
MessageSearchBloc.of(context).search(
filter: widget.filters,
sort: widget.sortOptions,
query: widget.messageQuery,
pagination: widget.paginationParams,
messageFilter: widget.messageFilters,
);
loadData();
if (widget.messageSearchListController != null) {
widget.messageSearchListController.loadData = loadData;
widget.messageSearchListController.paginateData = paginateData;
@@ -136,29 +130,23 @@ class _MessageSearchListCoreState extends State<MessageSearchListCore> {
stream: messageSearchBloc.messagesStream,
builder: (context, snapshot) {
if (snapshot.hasError) {
if (snapshot.error is Error) {
print((snapshot.error as Error).stackTrace);
}
return widget.errorBuilder(context, snapshot.error);
}
if (!snapshot.hasData) {
return widget.loadingBuilder(context);
}
final items = snapshot.data;
if (items.isEmpty) {
return widget.emptyBuilder(context);
}
return widget.childBuilder(snapshot.data);
},
);
void loadData() {
MessageSearchBloc.of(context).search(
/// Fetches initial messages and updates the widget
Future<void> loadData() {
final messageSearchBloc = MessageSearchBloc.of(context);
return messageSearchBloc.search(
filter: widget.filters,
sort: widget.sortOptions,
query: widget.messageQuery,
@@ -167,10 +155,10 @@ class _MessageSearchListCoreState extends State<MessageSearchListCore> {
);
}
void paginateData() {
/// Fetches more messages with updated pagination and updates the widget
Future<void> paginateData() {
final messageSearchBloc = MessageSearchBloc.of(context);
messageSearchBloc.loadMore(
return messageSearchBloc.search(
filter: widget.filters,
sort: widget.sortOptions,
pagination: widget.paginationParams.copyWith(
@@ -186,18 +174,12 @@ class _MessageSearchListCoreState extends State<MessageSearchListCore> {
super.didUpdateWidget(oldWidget);
if (widget.filters?.toString() != oldWidget.filters?.toString() ||
jsonEncode(widget.sortOptions) != jsonEncode(oldWidget.sortOptions) ||
widget.paginationParams?.toJson()?.toString() !=
oldWidget.paginationParams?.toJson()?.toString() ||
widget.messageQuery?.toString() != oldWidget.messageQuery?.toString() ||
widget.messageFilters?.toString() !=
oldWidget.messageFilters?.toString()) {
MessageSearchBloc.of(context).search(
filter: widget.filters,
sort: widget.sortOptions,
query: widget.messageQuery,
pagination: widget.paginationParams,
messageFilter: widget.messageFilters,
);
oldWidget.messageFilters?.toString() ||
widget.paginationParams?.toJson()?.toString() !=
oldWidget.paginationParams?.toJson()?.toString()) {
loadData();
}
}
}
@@ -205,8 +187,8 @@ class _MessageSearchListCoreState extends State<MessageSearchListCore> {
/// Controller used for paginating data in [ChannelListView]
class MessageSearchListController {
/// Call this function to reload data
VoidCallback loadData;
AsyncCallback loadData;
/// Call this function to load further data
VoidCallback paginateData;
AsyncCallback paginateData;
}
@@ -207,15 +207,14 @@ class StreamChannelState extends State<StreamChannel> {
int after = 20,
bool preferOffline = false,
}) =>
queryAtMessage(
_queryAtMessage(
messageId: messageId,
before: before,
after: after,
preferOffline: preferOffline,
);
///
Future<void> queryAtMessage({
Future<void> _queryAtMessage({
String messageId,
int before = 20,
int after = 20,
@@ -297,7 +296,7 @@ class StreamChannelState extends State<StreamChannel> {
}
/// Reloads the channel with latest message
Future<void> reloadChannel() => queryAtMessage(before: 30);
Future<void> reloadChannel() => _queryAtMessage(before: 30);
List<Future<bool>> _futures;
@@ -305,8 +304,7 @@ class StreamChannelState extends State<StreamChannel> {
try {
await loadChannelAtMessage(initialMessageId);
return true;
} catch (e, stk) {
print('Error: $e\nStack: $stk');
} catch (_) {
rethrow;
}
}
@@ -349,9 +347,6 @@ class StreamChannelState extends State<StreamChannel> {
],
builder: (context, snapshot) {
if (snapshot.hasError) {
if (snapshot.error is Error) {
print((snapshot.error as Error).stackTrace);
}
var message = snapshot.error.toString();
if (snapshot.error is DioError) {
final dioError = snapshot.error as DioError;
@@ -361,9 +356,7 @@ class StreamChannelState extends State<StreamChannel> {
message = 'Check your connection and retry';
}
}
return Center(
child: Text(message),
);
return Center(child: Text(message));
}
final initialized = snapshot.data[0];
// ignore: avoid_bool_literals_in_conditional_expressions
@@ -93,10 +93,10 @@ class StreamChatCoreState extends State<StreamChatCore>
Widget build(BuildContext context) => widget.child;
/// The current user
User get user => widget.client.state.user;
User get user => client.state?.user;
/// The current user as a stream
Stream<User> get userStream => widget.client.state.userStream;
Stream<User> get userStream => client.state?.userStream;
@override
void initState() {
@@ -108,21 +108,25 @@ class StreamChatCoreState extends State<StreamChatCore>
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (client.state?.user != null) {
if (user != null) {
if (state == AppLifecycleState.paused) {
if (widget.onBackgroundEventReceived != null) {
_eventSubscription =
client.on().listen(widget.onBackgroundEventReceived);
_disconnectTimer = Timer(
widget.backgroundKeepAlive,
client.disconnect,
);
} else {
if (widget.onBackgroundEventReceived == null) {
client.disconnect();
return;
}
_eventSubscription = client.on().listen(
widget.onBackgroundEventReceived,
);
void onTimerComplete() {
_eventSubscription.cancel();
client.disconnect();
}
_disconnectTimer = Timer(widget.backgroundKeepAlive, onTimerComplete);
} else if (state == AppLifecycleState.resumed) {
_eventSubscription?.cancel();
if (_disconnectTimer?.isActive == true) {
_eventSubscription.cancel();
_disconnectTimer.cancel();
} else {
if (client.wsConnectionStatus == ConnectionStatus.disconnected) {
@@ -80,7 +80,7 @@ class UserListCore extends StatefulWidget {
final UserListController userListController;
/// The builder that will be used in case of error
final Widget Function(Error error) errorBuilder;
final Widget Function(Object error) errorBuilder;
/// The builder that will be used to build the list
final Widget Function(BuildContext context, List<ListItem> users) listBuilder;
@@ -120,21 +120,16 @@ class UserListCore extends StatefulWidget {
final bool groupAlphabetically;
@override
_UserListCoreState createState() => _UserListCoreState();
UserListCoreState createState() => UserListCoreState();
}
class _UserListCoreState extends State<UserListCore>
/// The current state of the [UserListCore].
class UserListCoreState extends State<UserListCore>
with WidgetsBindingObserver {
@override
void didChangeDependencies() {
super.didChangeDependencies();
UsersBloc.of(context).queryUsers(
filter: widget.filter,
sort: widget.sort,
pagination: widget.pagination,
options: widget.options,
);
loadData();
if (widget.userListController != null) {
widget.userListController.loadData = loadData;
widget.userListController.paginateData = paginateData;
@@ -144,11 +139,10 @@ class _UserListCoreState extends State<UserListCore>
@override
Widget build(BuildContext context) {
final _usersBloc = UsersBloc.of(context);
return _buildListView(_usersBloc);
}
bool get isListAlreadySorted =>
bool get _isListAlreadySorted =>
widget.sort?.any((e) => e.field == 'name' && e.direction == 1) ?? false;
Stream<List<ListItem>> _buildUserStream(
@@ -158,7 +152,7 @@ class _UserListCoreState extends State<UserListCore>
(users) {
if (widget.groupAlphabetically) {
var temp = users;
if (!isListAlreadySorted) {
if (!_isListAlreadySorted) {
temp = users
..sort((curr, next) => curr.name.compareTo(next.name));
}
@@ -186,33 +180,23 @@ class _UserListCoreState extends State<UserListCore>
stream: _buildUserStream(usersBlocState),
builder: (context, snapshot) {
if (snapshot.hasError) {
if (snapshot.error is Error) {
print((snapshot.error as Error).stackTrace);
}
return widget.errorBuilder(snapshot.error);
}
if (!snapshot.hasData) {
return widget.loadingBuilder(context);
}
final items = snapshot.data;
if (items.isEmpty) {
return widget.emptyBuilder(context);
}
if (items.isEmpty) {
return widget.emptyBuilder(context);
}
return widget.listBuilder(context, items);
},
);
void loadData() {
UsersBloc.of(context).queryUsers(
// ignore: public_member_api_docs
Future<void> loadData() {
final _usersBloc = UsersBloc.of(context);
return _usersBloc.queryUsers(
filter: widget.filter,
sort: widget.sort,
pagination: widget.pagination,
@@ -220,10 +204,10 @@ class _UserListCoreState extends State<UserListCore>
);
}
void paginateData() {
// ignore: public_member_api_docs
Future<void> paginateData() {
final _usersBloc = UsersBloc.of(context);
_usersBloc.queryUsers(
return _usersBloc.queryUsers(
filter: widget.filter,
sort: widget.sort,
pagination: widget.pagination.copyWith(
@@ -238,15 +222,10 @@ class _UserListCoreState extends State<UserListCore>
super.didUpdateWidget(oldWidget);
if (widget.filter?.toString() != oldWidget.filter?.toString() ||
jsonEncode(widget.sort) != jsonEncode(oldWidget.sort) ||
widget.options?.toString() != oldWidget.options?.toString() ||
widget.pagination?.toJson()?.toString() !=
oldWidget.pagination?.toJson()?.toString() ||
widget.options?.toString() != oldWidget.options?.toString()) {
UsersBloc.of(context).queryUsers(
filter: widget.filter,
sort: widget.sort,
pagination: widget.pagination,
options: widget.options,
);
oldWidget.pagination?.toJson()?.toString()) {
loadData();
}
}
}
@@ -255,11 +234,11 @@ class _UserListCoreState extends State<UserListCore>
/// Header items are prefixed with the key `HEADER` While users are prefixed
/// with `USER`.
abstract class ListItem {
// ignore: public_member_api_docs
/// Unique key per list item
String get key {
if (this is ListHeaderItem) {
final header = (this as ListHeaderItem).heading;
return 'HEADER-$header';
return 'HEADER-${header.toLowerCase()}';
}
if (this is ListUserItem) {
final user = (this as ListUserItem).user;
@@ -268,7 +247,8 @@ abstract class ListItem {
return null;
}
// ignore: public_member_api_docs
/// Helper function to build widget based on ListItem type
// ignore: missing_return
Widget when({
@required Widget Function(String heading) headerItem,
@required Widget Function(User user) userItem,
@@ -279,33 +259,32 @@ abstract class ListItem {
if (this is ListUserItem) {
return userItem((this as ListUserItem).user);
}
return const SizedBox();
}
}
// ignore: public_member_api_docs
/// Header Item
class ListHeaderItem extends ListItem {
// ignore: public_member_api_docs
/// Constructs a new [ListHeaderItem]
ListHeaderItem(this.heading);
// ignore: public_member_api_docs
/// Heading used to build the item.
final String heading;
}
// ignore: public_member_api_docs
/// User Item
class ListUserItem extends ListItem {
// ignore: public_member_api_docs
/// Constructs a new [ListUserItem]
ListUserItem(this.user);
// ignore: public_member_api_docs
/// [User] used to build the item.
final User user;
}
/// Controller used for paginating data in [ChannelListView]
class UserListController {
/// Call this function to reload data
VoidCallback loadData;
AsyncCallback loadData;
/// Call this function to load further data
VoidCallback paginateData;
AsyncCallback paginateData;
}
@@ -51,10 +51,9 @@ class UsersBlocState extends State<UsersBloc>
/// The current users list as a stream
Stream<List<User>> get usersStream => _usersController.stream;
final BehaviorSubject<List<User>> _usersController = BehaviorSubject();
final _usersController = BehaviorSubject<List<User>>();
final BehaviorSubject<bool> _queryUsersLoadingController =
BehaviorSubject.seeded(false);
final _queryUsersLoadingController = BehaviorSubject.seeded(false);
/// The stream notifying the state of queryUsers call
Stream<bool> get queryUsersLoading => _queryUsersLoadingController.stream;
@@ -70,11 +69,12 @@ class UsersBlocState extends State<UsersBloc>
}) async {
final client = StreamChatCore.of(context).client;
if (client.state?.user == null ||
_queryUsersLoadingController.value == true) {
return;
if (_queryUsersLoadingController.value == true) return;
if (_usersController.hasValue) {
_queryUsersLoadingController.add(true);
}
_queryUsersLoadingController.add(true);
try {
final clear = pagination == null ||
pagination.offset == null ||
@@ -95,10 +95,15 @@ class UsersBlocState extends State<UsersBloc>
final temp = oldUsers + usersResponse.users;
_usersController.add(temp);
}
_queryUsersLoadingController.add(false);
} catch (err, stackTrace) {
_queryUsersLoadingController.addError(err, stackTrace);
if (_usersController.hasValue && _queryUsersLoadingController.value) {
_queryUsersLoadingController.add(false);
}
} catch (e, stk) {
if (_usersController.hasValue) {
_queryUsersLoadingController.addError(e, stk);
} else {
_usersController.addError(e, stk);
}
}
}
@@ -2,14 +2,14 @@ library stream_chat_flutter_core;
export 'package:stream_chat/stream_chat.dart';
export 'src/channel_list_core.dart';
export 'src/channel_list_core.dart' hide ChannelListCoreState;
export 'src/channels_bloc.dart';
export 'src/lazy_load_scroll_view.dart';
export 'src/message_list_core.dart';
export 'src/message_list_core.dart' hide MessageListCoreState;
export 'src/message_search_bloc.dart';
export 'src/message_search_list_core.dart';
export 'src/message_search_list_core.dart' hide MessageSearchListCoreState;
export 'src/stream_channel.dart';
export 'src/stream_chat_core.dart';
export 'src/typedef.dart';
export 'src/user_list_core.dart';
export 'src/user_list_core.dart' hide UserListCoreState;
export 'src/users_bloc.dart';
@@ -15,8 +15,11 @@ dependencies:
rxdart: ^0.25.0
stream_chat: ^1.5.0
dependency_overrides:
stream_chat:
path: ../stream_chat
dev_dependencies:
fake_async: ^1.1.0
flutter_test:
sdk: flutter
mockito: ^4.1.4
mockito: ^4.1.3
@@ -0,0 +1,500 @@
import 'dart:async';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/widgets.dart';
import 'package:mockito/mockito.dart';
import 'package:stream_chat_flutter_core/src/channel_list_core.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'mocks.dart';
void main() {
const pagination = PaginationParams(offset: 0, limit: 25);
List<Channel> _generateChannels(
StreamChatClient client, {
int count = 3,
int offset = 0,
}) {
return List.generate(
count,
(index) {
index = index + offset;
return Channel(
client,
'testType$index',
'testId$index',
{'extra_data_key': 'extra_data_value_$index'},
);
},
);
}
test(
'should throw assertion error in case listBuilder is null',
() {
final channelListCore = () => ChannelListCore(
listBuilder: null,
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (BuildContext context, Object error) => Offstage(),
);
expect(channelListCore, throwsA(isA<AssertionError>()));
},
);
test(
'should throw assertion error in case loadingBuilder is null',
() {
final channelListCore = () => ChannelListCore(
listBuilder: (_, __) => Offstage(),
loadingBuilder: null,
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (BuildContext context, Object error) => Offstage(),
);
expect(channelListCore, throwsA(isA<AssertionError>()));
},
);
test(
'should throw assertion error in case emptyBuilder is null',
() {
final channelListCore = () => ChannelListCore(
listBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: null,
errorBuilder: (BuildContext context, Object error) => Offstage(),
);
expect(channelListCore, throwsA(isA<AssertionError>()));
},
);
test(
'should throw assertion error in case errorBuilder is null',
() {
final channelListCore = () => ChannelListCore(
listBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: null,
);
expect(channelListCore, throwsA(isA<AssertionError>()));
},
);
testWidgets(
'should throw if ChannelListCore is used where ChannelsBloc is not present '
'in the widget tree',
(tester) async {
const channelListCoreKey = Key('channelListCore');
final channelListCore = ChannelListCore(
key: channelListCoreKey,
listBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (BuildContext context, Object error) => Offstage(),
);
await tester.pumpWidget(channelListCore);
expect(find.byKey(channelListCoreKey), findsNothing);
expect(tester.takeException(), isInstanceOf<Exception>());
},
);
testWidgets(
'should render ChannelListCore if used with ChannelsBloc as an ancestor',
(tester) async {
const channelListCoreKey = Key('channelListCore');
final channelListCore = ChannelListCore(
key: channelListCoreKey,
listBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (BuildContext context, Object error) => Offstage(),
);
final mockClient = MockClient();
when(mockClient.on(any, any, any, any)).thenAnswer((_) => Stream.empty());
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: ChannelsBloc(
child: channelListCore,
),
),
);
expect(find.byKey(channelListCoreKey), findsOneWidget);
},
);
testWidgets(
'should assign loadData and paginateData callback to '
'ChannelListController if passed',
(tester) async {
const channelListCoreKey = Key('channelListCore');
final controller = ChannelListController();
final channelListCore = ChannelListCore(
key: channelListCoreKey,
listBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (BuildContext context, Object error) => Offstage(),
channelListController: controller,
);
expect(controller.loadData, isNull);
expect(controller.paginateData, isNull);
final mockClient = MockClient();
when(mockClient.on(any, any, any, any)).thenAnswer((_) => Stream.empty());
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: ChannelsBloc(
child: channelListCore,
),
),
);
expect(find.byKey(channelListCoreKey), findsOneWidget);
expect(controller.loadData, isNotNull);
expect(controller.paginateData, isNotNull);
},
);
testWidgets(
'should build error widget if channelsBlocState.channelsStream emits error',
(tester) async {
const channelListCoreKey = Key('channelListCore');
const errorWidgetKey = Key('errorWidget');
final channelListCore = ChannelListCore(
key: channelListCoreKey,
listBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (BuildContext context, Object error) =>
Container(key: errorWidgetKey),
);
final mockClient = MockClient();
when(mockClient.on(any, any, any, any)).thenAnswer((_) => Stream.empty());
const error = 'Error! Error! Error!';
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: pagination,
)).thenThrow(error);
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: ChannelsBloc(
child: channelListCore,
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(errorWidgetKey), findsOneWidget);
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: pagination,
)).called(1);
},
);
testWidgets(
'should build empty widget if channelsBlocState.channelsStream emits empty data',
(tester) async {
const channelListCoreKey = Key('channelListCore');
const emptyWidgetKey = Key('emptyWidget');
final channelListCore = ChannelListCore(
key: channelListCoreKey,
listBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Container(key: emptyWidgetKey),
errorBuilder: (BuildContext context, Object error) => Offstage(),
);
final mockClient = MockClient();
when(mockClient.on(any, any, any, any)).thenAnswer((_) => Stream.empty());
const channels = <Channel>[];
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: pagination,
)).thenAnswer((_) => Stream.value(channels));
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: ChannelsBloc(
child: channelListCore,
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(emptyWidgetKey), findsOneWidget);
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: pagination,
)).called(1);
},
);
testWidgets(
'should build list widget if channelsBlocState.channelsStream emits some data',
(tester) async {
const channelListCoreKey = Key('channelListCore');
const listWidgetKey = Key('listWidget');
final channelListCore = ChannelListCore(
key: channelListCoreKey,
listBuilder: (_, __) => Container(key: listWidgetKey),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (BuildContext context, Object error) => Offstage(),
);
final mockClient = MockClient();
when(mockClient.on(any, any, any, any)).thenAnswer((_) => Stream.empty());
final channels = _generateChannels(mockClient);
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: pagination,
)).thenAnswer((_) => Stream.value(channels));
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: ChannelsBloc(
child: channelListCore,
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(listWidgetKey), findsOneWidget);
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: pagination,
)).called(1);
},
);
testWidgets(
'should build list widget with paginated data '
'on calling channelListCoreState.paginateData',
(tester) async {
const channelListCoreKey = Key('channelListCore');
const listWidgetKey = Key('listWidget');
final channelListCore = ChannelListCore(
key: channelListCoreKey,
listBuilder: (_, channels) {
return Container(
key: listWidgetKey,
child: Text(
channels.map((e) => e.cid).join(','),
),
);
},
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (BuildContext context, Object error) => Offstage(),
);
final mockClient = MockClient();
when(mockClient.on(any, any, any, any)).thenAnswer((_) => Stream.empty());
final channels = _generateChannels(mockClient);
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: pagination,
)).thenAnswer((_) => Stream.value(channels));
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StreamChatCore(
client: mockClient,
child: ChannelsBloc(
child: channelListCore,
),
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(listWidgetKey), findsOneWidget);
expect(find.text(channels.map((e) => e.cid).join(',')), findsOneWidget);
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: pagination,
)).called(1);
final channelListCoreState = tester.state<ChannelListCoreState>(
find.byKey(channelListCoreKey),
);
final offset = channels.length;
final paginatedChannels = _generateChannels(mockClient, offset: offset);
final updatedPagination = pagination.copyWith(offset: offset);
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: updatedPagination,
)).thenAnswer((_) => Stream.value(paginatedChannels));
await channelListCoreState.paginateData();
await tester.pumpAndSettle();
expect(find.byKey(listWidgetKey), findsOneWidget);
expect(
find.text([
...channels,
...paginatedChannels,
].map((e) => e.cid).join(',')),
findsOneWidget,
);
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: updatedPagination,
)).called(1);
},
);
testWidgets(
'should rebuild ChannelListCore with updated widget data '
'on calling setState()',
(tester) async {
StateSetter _stateSetter;
int limit = pagination.limit;
const channelListCoreKey = Key('channelListCore');
const listWidgetKey = Key('listWidget');
ChannelListCore channelListCoreBuilder(int limit) => ChannelListCore(
key: channelListCoreKey,
listBuilder: (_, channels) {
return Container(
key: listWidgetKey,
child: Text(
channels.map((e) => e.cid).join(','),
),
);
},
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (BuildContext context, Object error) => Offstage(),
pagination: pagination.copyWith(limit: limit),
);
final mockClient = MockClient();
when(mockClient.on(any, any, any, any)).thenAnswer((_) => Stream.empty());
final channels = _generateChannels(mockClient);
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: pagination,
)).thenAnswer((_) => Stream.value(channels));
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StreamChatCore(
client: mockClient,
child: ChannelsBloc(
child: StatefulBuilder(builder: (context, stateSetter) {
// Assigning stateSetter for rebuilding ChannelListCore
_stateSetter = stateSetter;
return channelListCoreBuilder(limit);
}),
),
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(listWidgetKey), findsOneWidget);
expect(find.text(channels.map((e) => e.cid).join(',')), findsOneWidget);
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: pagination,
)).called(1);
// Rebuilding ChannelListCore with new pagination limit
_stateSetter(() => limit = 6);
final updatedChannels = _generateChannels(mockClient, count: limit);
final updatedPagination = pagination.copyWith(limit: limit);
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: updatedPagination,
)).thenAnswer((_) => Stream.value(updatedChannels));
await tester.pumpAndSettle();
expect(find.byKey(listWidgetKey), findsOneWidget);
expect(
find.text(updatedChannels.map((e) => e.cid).join(',')),
findsOneWidget,
);
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: updatedPagination,
)).called(1);
},
);
}
@@ -0,0 +1,847 @@
import 'dart:async';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'matchers/channel_matcher.dart';
import 'mocks.dart';
void main() {
List<Channel> _generateChannels(
StreamChatClient client, {
int count = 3,
int offset = 0,
}) {
return List.generate(
count,
(index) {
index = index + offset;
return Channel(
client,
'testType$index',
'testId$index',
{'extra_data_key': 'extra_data_value_$index'},
);
},
);
}
test(
'should throw assertion error if child is null',
() async {
const channelsBlocKey = Key('channelsBloc');
final channelsBloc = () => ChannelsBloc(
key: channelsBlocKey,
child: null,
);
expect(channelsBloc, throwsA(isA<AssertionError>()));
},
);
testWidgets(
'should throw if ChannelsBloc is used where StreamChat is not present in the widget tree',
(tester) async {
const channelsBlocKey = Key('channelsBloc');
const childKey = Key('child');
final channelsBloc = ChannelsBloc(
key: channelsBlocKey,
child: Offstage(key: childKey),
);
await tester.pumpWidget(channelsBloc);
expect(find.byKey(channelsBlocKey), findsNothing);
expect(find.byKey(childKey), findsNothing);
expect(tester.takeException(), isInstanceOf<Exception>());
},
);
testWidgets(
'should render ChannelsBloc if used with StreamChatCore as an ancestor',
(tester) async {
const channelsBlocKey = Key('channelsBloc');
const childKey = Key('child');
final channelsBloc = ChannelsBloc(
key: channelsBlocKey,
child: Offstage(key: childKey),
);
final mockClient = MockClient();
when(mockClient.on(any, any, any, any)).thenAnswer((_) => Stream.empty());
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: channelsBloc,
),
);
expect(find.byKey(channelsBlocKey), findsOneWidget);
expect(find.byKey(childKey), findsOneWidget);
},
);
testWidgets(
'channelsBlocState.queryChannels() should emit data through channelsStream',
(tester) async {
const channelsBlocKey = Key('channelsBloc');
const childKey = Key('child');
final channelsBloc = ChannelsBloc(
key: channelsBlocKey,
child: Builder(
key: childKey,
builder: (context) {
return Offstage();
},
),
);
final mockClient = MockClient();
when(mockClient.on(any, any, any, any)).thenAnswer((_) => Stream.empty());
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: channelsBloc,
),
);
final channelsBlocState = tester.state<ChannelsBlocState>(
find.byKey(channelsBlocKey),
);
final offlineChannels = _generateChannels(mockClient);
final onlineChannels = _generateChannels(mockClient, offset: 3);
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).thenAnswer(
(_) => Stream.fromIterable([offlineChannels, onlineChannels]),
);
channelsBlocState.queryChannels();
await expectLater(
channelsBlocState.channelsStream,
emitsInOrder([
isSameChannelListAs(offlineChannels),
isSameChannelListAs(onlineChannels),
]),
);
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).called(1);
},
);
testWidgets(
'channelsBlocState.channelsStream should emit error '
'if client.queryChannels() throws',
(tester) async {
const channelsBlocKey = Key('channelsBloc');
const childKey = Key('child');
final channelsBloc = ChannelsBloc(
key: channelsBlocKey,
child: Builder(
key: childKey,
builder: (context) {
return Offstage();
},
),
);
final mockClient = MockClient();
when(mockClient.on(any, any, any, any)).thenAnswer((_) => Stream.empty());
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: channelsBloc,
),
);
final channelsBlocState = tester.state<ChannelsBlocState>(
find.byKey(channelsBlocKey),
);
final error = 'Error! Error! Error!';
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).thenThrow(error);
channelsBlocState.queryChannels();
await expectLater(
channelsBlocState.channelsStream,
emitsError(error),
);
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).called(1);
},
);
testWidgets(
'calling channelsBlocState.queryChannels() again with an offset '
'should emit new data through channelsStream and also emit loading state '
'through queryChannelsLoading',
(tester) async {
const channelsBlocKey = Key('channelsBloc');
final channelsBloc = ChannelsBloc(
key: channelsBlocKey,
child: Offstage(),
);
final mockClient = MockClient();
when(mockClient.on(any, any, any, any)).thenAnswer((_) => Stream.empty());
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: channelsBloc,
),
);
final channelsBlocState = tester.state<ChannelsBlocState>(
find.byKey(channelsBlocKey),
);
final channels = _generateChannels(mockClient);
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).thenAnswer((_) => Stream.value(channels));
channelsBlocState.queryChannels();
await expectLater(
channelsBlocState.channelsStream,
emits(isSameChannelListAs(channels)),
);
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).called(1);
final offset = channels.length;
final paginationParams = PaginationParams(offset: offset);
final newChannels = _generateChannels(mockClient, offset: offset);
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: paginationParams,
)).thenAnswer(
(_) => Stream.value(newChannels),
);
channelsBlocState.queryChannels(paginationParams: paginationParams);
await Future.wait([
expectLater(
channelsBlocState.queryChannelsLoading,
emitsInOrder([true, false]),
),
expectLater(
channelsBlocState.channelsStream,
emits(isSameChannelListAs(channels + newChannels)),
),
]);
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: paginationParams,
)).called(1);
},
);
testWidgets(
'calling channelsBlocState.queryChannels() again with an offset '
'should emit error through queryChannelsLoading if '
'client.queryChannels() throws',
(tester) async {
const channelsBlocKey = Key('channelsBloc');
final channelsBloc = ChannelsBloc(
key: channelsBlocKey,
child: Offstage(),
);
final mockClient = MockClient();
when(mockClient.on(any, any, any, any)).thenAnswer((_) => Stream.empty());
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: channelsBloc,
),
);
final channelsBlocState = tester.state<ChannelsBlocState>(
find.byKey(channelsBlocKey),
);
final channels = _generateChannels(mockClient);
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).thenAnswer((_) => Stream.value(channels));
channelsBlocState.queryChannels();
await expectLater(
channelsBlocState.channelsStream,
emits(isSameChannelListAs(channels)),
);
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).called(1);
final offset = channels.length;
final paginationParams = PaginationParams(offset: offset);
final error = 'Error! Error! Error!';
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: paginationParams,
)).thenThrow(error);
channelsBlocState.queryChannels(paginationParams: paginationParams);
await expectLater(
channelsBlocState.queryChannelsLoading,
emitsError(error),
);
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: paginationParams,
)).called(1);
},
);
group('event controller test', () {
StreamController<Event> eventController;
setUp(() {
eventController = StreamController<Event>.broadcast();
});
testWidgets(
'channel should get hide when EventType.channelHidden event is received',
(tester) async {
final mockClient = MockClient();
const channelsBlocKey = Key('channelsBloc');
final channelsBloc = ChannelsBloc(
key: channelsBlocKey,
child: Offstage(),
);
when(mockClient.on(any, any, any, any))
.thenAnswer((_) => Stream.empty());
when(mockClient.on(
EventType.channelHidden,
)).thenAnswer((_) => eventController.stream);
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: channelsBloc,
),
);
final channelsBlocState = tester.state<ChannelsBlocState>(
find.byKey(channelsBlocKey),
);
final channels = _generateChannels(mockClient);
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).thenAnswer(
(_) => Stream.value(channels),
);
await channelsBlocState.queryChannels();
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).called(1);
final channelHiddenEvent = Event(
type: EventType.channelHidden,
cid: channels.first.cid,
);
eventController.add(channelHiddenEvent);
final newChannels = [...channels]
..removeWhere((it) => it.cid == channelHiddenEvent.cid);
await expectLater(
channelsBlocState.channelsStream,
emitsInOrder([
isSameChannelListAs(channels),
isSameChannelListAs(newChannels),
]),
);
verify(mockClient.on(EventType.channelHidden)).called(1);
},
);
testWidgets(
'channel should get removed when EventType.channelDeleted or '
'EventType.notificationRemovedFromChannel, event is received',
(tester) async {
final mockClient = MockClient();
const channelsBlocKey = Key('channelsBloc');
final channelsBloc = ChannelsBloc(
key: channelsBlocKey,
child: Offstage(),
);
when(mockClient.on(any, any, any, any))
.thenAnswer((_) => Stream.empty());
when(mockClient.on(
EventType.channelDeleted,
EventType.notificationRemovedFromChannel,
)).thenAnswer((_) => eventController.stream);
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: channelsBloc,
),
);
final channelsBlocState = tester.state<ChannelsBlocState>(
find.byKey(channelsBlocKey),
);
final channels = _generateChannels(mockClient);
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).thenAnswer(
(_) => Stream.value(channels),
);
await channelsBlocState.queryChannels();
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).called(1);
final channelDeletedOrNotificationRemovedEvent = Event(
channel: EventChannel(cid: channels.first.cid),
);
eventController.add(channelDeletedOrNotificationRemovedEvent);
final channelCid = channelDeletedOrNotificationRemovedEvent.channel.cid;
final newChannels = [...channels]
..removeWhere((it) => it.cid == channelCid);
await expectLater(
channelsBlocState.channelsStream,
emitsInOrder([
isSameChannelListAs(channels),
isSameChannelListAs(newChannels),
]),
);
verify(mockClient.on(
EventType.channelDeleted,
EventType.notificationRemovedFromChannel,
)).called(1);
},
);
testWidgets(
'event channel should be moved to top of the list if present when'
'EventType.messageNew event is received',
(tester) async {
final mockClient = MockClient();
const channelsBlocKey = Key('channelsBloc');
final channelsBloc = ChannelsBloc(
key: channelsBlocKey,
child: Offstage(),
);
when(mockClient.on(any, any, any, any))
.thenAnswer((_) => Stream.empty());
when(mockClient.on(
EventType.messageNew,
)).thenAnswer((_) => eventController.stream);
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: channelsBloc,
),
);
final channelsBlocState = tester.state<ChannelsBlocState>(
find.byKey(channelsBlocKey),
);
final channels = _generateChannels(mockClient);
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).thenAnswer(
(_) => Stream.value(channels),
);
await channelsBlocState.queryChannels();
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).called(1);
final messageNewEvent = Event(
type: EventType.messageNew,
cid: channels.last.cid,
);
eventController.add(messageNewEvent);
final channelCid = messageNewEvent.cid;
final index = channels.indexWhere((it) => it.cid == channelCid);
final updatedChannel = channels[index];
final newChannels = [...channels]
..removeAt(index)
..insert(0, updatedChannel);
await expectLater(
channelsBlocState.channelsStream,
emitsInOrder([
isSameChannelListAs(channels),
isSameChannelListAs(newChannels),
]),
);
verify(mockClient.on(EventType.messageNew)).called(1);
},
);
testWidgets(
'event channel should be moved to top of the list if present inside '
'hiddenChannels list and shouldAddChannel is true when '
'EventType.messageNew event is received',
(tester) async {
final hiddenChannelEventController = StreamController<Event>();
addTearDown(() {
hiddenChannelEventController.close();
});
final mockClient = MockClient();
final channels = _generateChannels(mockClient);
const channelsBlocKey = Key('channelsBloc');
final channelsBloc = ChannelsBloc(
key: channelsBlocKey,
child: Offstage(),
shouldAddChannel: (e) => channels.map((it) => it.cid).contains(e.cid),
);
when(mockClient.on(any, any, any, any))
.thenAnswer((_) => Stream.empty());
when(mockClient.on(
EventType.channelHidden,
)).thenAnswer((_) => hiddenChannelEventController.stream);
when(mockClient.on(
EventType.messageNew,
)).thenAnswer((_) => eventController.stream);
final messageNewEvent = Event(
type: EventType.messageNew,
cid: channels.last.cid,
);
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: channelsBloc,
),
);
final channelsBlocState = tester.state<ChannelsBlocState>(
find.byKey(channelsBlocKey),
);
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).thenAnswer(
(_) => Stream.value(channels),
);
await channelsBlocState.queryChannels();
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).called(1);
final channelHiddenEvent = Event(
type: EventType.channelHidden,
cid: channels.last.cid,
);
// Hiding the channel before passing messageNew event
hiddenChannelEventController.add(channelHiddenEvent);
final channelsAfterHiddenEvent = [...channels]..removeLast();
eventController.add(messageNewEvent);
final channelCid = messageNewEvent.cid;
final index = channels.indexWhere((it) => it.cid == channelCid);
final newChannels = [...channels]
..removeAt(index)
..insert(0, channels[index]);
await expectLater(
channelsBlocState.channelsStream,
emitsInOrder([
isSameChannelListAs(channels),
isSameChannelListAs(channelsAfterHiddenEvent),
isSameChannelListAs(newChannels),
]),
);
verify(mockClient.on(EventType.channelHidden)).called(1);
verify(mockClient.on(EventType.messageNew)).called(1);
},
);
testWidgets(
'event channel should be moved to top of the list if present inside '
'channel state and shouldAddChannel is true when '
'EventType.messageNew event is received',
(tester) async {
final mockClient = MockClient();
final channels = _generateChannels(mockClient);
final stateChannels = {
for (var c in _generateChannels(mockClient, offset: 5)) c.cid: c
};
const channelsBlocKey = Key('channelsBloc');
final channelsBloc = ChannelsBloc(
key: channelsBlocKey,
child: Offstage(),
shouldAddChannel: (_) => true,
);
when(mockClient.state.channels).thenReturn(stateChannels);
when(mockClient.on(any, any, any, any))
.thenAnswer((_) => Stream.empty());
when(mockClient.on(
EventType.messageNew,
)).thenAnswer((_) => eventController.stream);
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: channelsBloc,
),
);
final channelsBlocState = tester.state<ChannelsBlocState>(
find.byKey(channelsBlocKey),
);
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).thenAnswer(
(_) => Stream.value(channels),
);
await channelsBlocState.queryChannels();
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).called(1);
final messageNewEvent = Event(
type: EventType.messageNew,
cid: stateChannels.keys.first,
);
eventController.add(messageNewEvent);
final newChannels = [...channels]
..insert(0, stateChannels[stateChannels.keys.first]);
await expectLater(
channelsBlocState.channelsStream,
emitsInOrder([
isSameChannelListAs(channels),
isSameChannelListAs(newChannels),
]),
);
verify(mockClient.on(EventType.messageNew)).called(1);
},
);
testWidgets(
'channels should get sorted according to channelsComparator when '
'EventType.messageNew event is received',
(tester) async {
final mockClient = MockClient();
final channels = _generateChannels(mockClient);
int channelComparator(Channel a, Channel b) {
final aData = a.extraData['extra_data_key'] as String;
final bData = b.extraData['extra_data_key'] as String;
return bData.compareTo(aData);
}
const channelsBlocKey = Key('channelsBloc');
final channelsBloc = ChannelsBloc(
key: channelsBlocKey,
child: Offstage(),
shouldAddChannel: (_) => true,
channelsComparator: channelComparator,
);
when(mockClient.on(any, any, any, any))
.thenAnswer((_) => Stream.empty());
when(mockClient.on(
EventType.messageNew,
)).thenAnswer((_) => eventController.stream);
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: channelsBloc,
),
);
final channelsBlocState = tester.state<ChannelsBlocState>(
find.byKey(channelsBlocKey),
);
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).thenAnswer(
(_) => Stream.value(channels),
);
await channelsBlocState.queryChannels();
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).called(1);
final messageNewEvent = Event(
type: EventType.messageNew,
cid: channels.first.cid,
);
eventController.add(messageNewEvent);
final newChannels = [...channels]..sort(channelComparator);
await expectLater(
channelsBlocState.channelsStream,
emitsInOrder([
isSameChannelListAs(channels),
isSameChannelListAs(newChannels),
]),
);
verify(mockClient.on(EventType.messageNew)).called(1);
},
);
tearDown(() {
eventController.close();
});
});
}
@@ -0,0 +1,294 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:stream_chat_flutter_core/src/lazy_load_scroll_view.dart';
void main() {
test(
'should throw assertion error if child is null',
() async {
const lazyLoadScrollViewKey = Key('lazyLoadScrollView');
final lazyLoadScrollView = () => LazyLoadScrollView(
key: lazyLoadScrollViewKey,
child: null,
);
expect(lazyLoadScrollView, throwsA(isA<AssertionError>()));
},
);
testWidgets(
'should render LazyLoadScrollView if child is provided',
(tester) async {
const lazyLoadScrollViewKey = Key('lazyLoadScrollView');
final lazyLoadScrollView = LazyLoadScrollView(
key: lazyLoadScrollViewKey,
child: Offstage(),
);
await tester.pumpWidget(lazyLoadScrollView);
expect(find.byKey(lazyLoadScrollViewKey), findsOneWidget);
},
);
testWidgets(
'should render LazyLoadScrollView if child is provided',
(tester) async {
const lazyLoadScrollViewKey = Key('lazyLoadScrollView');
const childKey = Key('childKey');
final lazyLoadScrollView = LazyLoadScrollView(
key: lazyLoadScrollViewKey,
child: Offstage(key: childKey),
);
await tester.pumpWidget(lazyLoadScrollView);
expect(find.byKey(lazyLoadScrollViewKey), findsOneWidget);
expect(find.byKey(childKey), findsOneWidget);
},
);
testWidgets(
'should call onPageScrollStart if child scrollView scrolls',
(tester) async {
const lazyLoadScrollViewKey = Key('lazyLoadScrollView');
const childListViewKey = Key('childListView');
bool onPageScrollStartCalled = false;
final lazyLoadScrollView = LazyLoadScrollView(
key: lazyLoadScrollViewKey,
onPageScrollStart: () {
onPageScrollStartCalled = true;
},
child: ListView(
key: childListViewKey,
children: List.generate(
12,
(index) => Container(
height: 100,
child: Text('Item #$index'),
),
),
),
);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: lazyLoadScrollView,
),
);
expect(find.byKey(lazyLoadScrollViewKey), findsOneWidget);
expect(find.byKey(childListViewKey), findsOneWidget);
expect(onPageScrollStartCalled, isFalse);
await tester.startGesture(const Offset(100.0, 100.0));
await tester.pump(const Duration(seconds: 1));
expect(onPageScrollStartCalled, isTrue);
},
);
testWidgets(
'should call onPageScrollStart, onPageScrollEnd '
'if child scrollView starts scrolling and ends',
(tester) async {
const lazyLoadScrollViewKey = Key('lazyLoadScrollView');
const childListViewKey = Key('childListView');
bool onPageScrollStartCalled = false;
bool onPageScrollEndCalled = false;
final lazyLoadScrollView = LazyLoadScrollView(
key: lazyLoadScrollViewKey,
onPageScrollStart: () {
onPageScrollStartCalled = true;
},
onPageScrollEnd: () {
onPageScrollEndCalled = true;
},
child: ListView(
key: childListViewKey,
children: List.generate(
12,
(index) => Container(
height: 100,
child: Text('Item #$index'),
),
),
),
);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: lazyLoadScrollView,
),
);
expect(find.byKey(lazyLoadScrollViewKey), findsOneWidget);
expect(find.byKey(childListViewKey), findsOneWidget);
expect(onPageScrollStartCalled, isFalse);
expect(onPageScrollEndCalled, isFalse);
final gesture = await tester.createGesture();
await gesture.down(const Offset(100.0, 100.0));
await tester.pump(const Duration(seconds: 1));
expect(onPageScrollStartCalled, isTrue);
await gesture.up();
await tester.pump(const Duration(seconds: 1));
expect(onPageScrollEndCalled, isTrue);
},
);
testWidgets(
'should call onInBetweenOfPage if child scrollView '
'scroll position is in-between of view',
(tester) async {
const lazyLoadScrollViewKey = Key('lazyLoadScrollView');
const childListViewKey = Key('childListView');
bool onInBetweenOfPageCalled = false;
final lazyLoadScrollView = LazyLoadScrollView(
key: lazyLoadScrollViewKey,
onInBetweenOfPage: () async {
onInBetweenOfPageCalled = true;
},
child: ListView(
key: childListViewKey,
children: List.generate(
12,
(index) => Container(
height: 100,
child: Text('Item #$index'),
),
),
),
);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: lazyLoadScrollView,
),
);
expect(find.byKey(lazyLoadScrollViewKey), findsOneWidget);
expect(find.byKey(childListViewKey), findsOneWidget);
expect(onInBetweenOfPageCalled, isFalse);
final gesture = await tester.createGesture();
await gesture.down(const Offset(100.0, 100.0));
await tester.pump(const Duration(seconds: 1));
await gesture.moveBy(const Offset(-200.0, -200.0));
await tester.pump(const Duration(seconds: 1));
expect(onInBetweenOfPageCalled, isTrue);
},
);
testWidgets(
'should call onStartOfPage if child scrollView '
'tends to reach the start',
(tester) async {
const lazyLoadScrollViewKey = Key('lazyLoadScrollView');
const childListViewKey = Key('childListView');
bool onStartOfPageCalled = false;
final lazyLoadScrollView = LazyLoadScrollView(
key: lazyLoadScrollViewKey,
onStartOfPage: () async {
onStartOfPageCalled = true;
},
child: ListView(
key: childListViewKey,
children: List.generate(
12,
(index) => Container(
height: 100,
child: Text('Item #$index'),
),
),
),
);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: lazyLoadScrollView,
),
);
expect(find.byKey(lazyLoadScrollViewKey), findsOneWidget);
expect(find.byKey(childListViewKey), findsOneWidget);
expect(onStartOfPageCalled, isFalse);
final gesture = await tester.createGesture();
await gesture.down(const Offset(100.0, 100.0));
await tester.pump(const Duration(seconds: 1));
await gesture.moveBy(const Offset(-200.0, -200.0));
await tester.pump(const Duration(seconds: 1));
await gesture.moveBy(const Offset(201.0, 201.0));
await tester.pump(const Duration(seconds: 1));
expect(onStartOfPageCalled, isTrue);
},
);
testWidgets(
'should call onEndOfPage if child scrollView '
'tends to reach the end',
(tester) async {
const lazyLoadScrollViewKey = Key('lazyLoadScrollView');
const childListViewKey = Key('childListView');
bool onEndOfPageCalled = false;
final lazyLoadScrollView = LazyLoadScrollView(
key: lazyLoadScrollViewKey,
onEndOfPage: () async {
onEndOfPageCalled = true;
},
child: ListView(
key: childListViewKey,
children: List.generate(
12,
(index) => Container(
height: 100,
child: Text('Item #$index'),
),
),
),
);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: lazyLoadScrollView,
),
);
expect(find.byKey(lazyLoadScrollViewKey), findsOneWidget);
expect(find.byKey(childListViewKey), findsOneWidget);
expect(onEndOfPageCalled, isFalse);
final gesture = await tester.createGesture();
await gesture.down(const Offset(100.0, 100.0));
await tester.pump(const Duration(seconds: 1));
await gesture.moveBy(const Offset(-601.0, -601.0));
await tester.pump(const Duration(seconds: 1));
expect(onEndOfPageCalled, isTrue);
},
);
}
@@ -0,0 +1,49 @@
import 'package:meta/meta.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
Matcher isSameChannelAs(Channel targetChannel) =>
_IsSameChannelAs(targetChannel: targetChannel);
class _IsSameChannelAs extends Matcher {
const _IsSameChannelAs({
@required this.targetChannel,
}) : assert(targetChannel != null, '');
final Channel targetChannel;
@override
bool matches(covariant Channel channel, Map matchState) =>
channel.cid == targetChannel.cid;
@override
Description describe(Description description) =>
description.add('is same channel as $targetChannel');
}
Matcher isSameChannelListAs(List<Channel> targetChannelList) =>
_IsSameChannelListAs(targetChannelList: targetChannelList);
class _IsSameChannelListAs extends Matcher {
const _IsSameChannelListAs({
@required this.targetChannelList,
}) : assert(targetChannelList != null, '');
final List<Channel> targetChannelList;
@override
bool matches(covariant List<Channel> channelList, Map matchState) {
bool matches = true;
for (var i = 0; i < channelList.length; i++) {
final channel = channelList[i];
final targetChannel = targetChannelList[i];
matches = isSameChannelAs(targetChannel).matches(channel, matchState);
if (!matches) break;
}
return matches;
}
@override
Description describe(Description description) =>
description.add('is same channelList as $targetChannelList');
}
@@ -0,0 +1,55 @@
import 'package:meta/meta.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
Matcher isSameMessageResponseAs(GetMessageResponse targetResponse) =>
_IsSameMessageResponseAs(targetResponse: targetResponse);
class _IsSameMessageResponseAs extends Matcher {
const _IsSameMessageResponseAs({
@required this.targetResponse,
}) : assert(targetResponse != null, '');
final GetMessageResponse targetResponse;
@override
bool matches(covariant GetMessageResponse response, Map matchState) =>
response.message.id == targetResponse.message.id &&
response.channel.cid == targetResponse.channel.cid;
@override
Description describe(Description description) =>
description.add('is same message response as $targetResponse');
}
Matcher isSameMessageResponseListAs(
List<GetMessageResponse> targetResponseList) =>
_IsSameMessageResponseListAs(targetResponseList: targetResponseList);
class _IsSameMessageResponseListAs extends Matcher {
const _IsSameMessageResponseListAs({
@required this.targetResponseList,
}) : assert(targetResponseList != null, '');
final List<GetMessageResponse> targetResponseList;
@override
bool matches(
covariant List<GetMessageResponse> responseList, Map matchState) {
bool matches = true;
for (var i = 0; i < responseList.length; i++) {
final response = responseList[i];
final targetResponse = targetResponseList[i];
matches = isSameMessageResponseAs(targetResponse).matches(
response,
matchState,
);
if (!matches) break;
}
return matches;
}
@override
Description describe(Description description) =>
description.add('is same userResponseList as $targetResponseList');
}
@@ -0,0 +1,49 @@
import 'package:meta/meta.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
Matcher isSameMessageAs(Message targetMessage) =>
_IsSameMessageAs(targetMessage: targetMessage);
class _IsSameMessageAs extends Matcher {
const _IsSameMessageAs({
@required this.targetMessage,
}) : assert(targetMessage != null, '');
final Message targetMessage;
@override
bool matches(covariant Message message, Map matchState) =>
message.id == targetMessage.id;
@override
Description describe(Description description) =>
description.add('is same message as $targetMessage');
}
Matcher isSameMessageListAs(List<Message> targetMessageList) =>
_IsSameMessageListAs(targetMessageList: targetMessageList);
class _IsSameMessageListAs extends Matcher {
const _IsSameMessageListAs({
@required this.targetMessageList,
}) : assert(targetMessageList != null, '');
final List<Message> targetMessageList;
@override
bool matches(covariant List<Message> messageList, Map matchState) {
bool matches = true;
for (var i = 0; i < messageList.length; i++) {
final message = messageList[i];
final targetMessage = targetMessageList[i];
matches = isSameMessageAs(targetMessage).matches(message, matchState);
if (!matches) break;
}
return matches;
}
@override
Description describe(Description description) =>
description.add('is same messageList as $targetMessageList');
}
@@ -0,0 +1,47 @@
import 'package:meta/meta.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
Matcher isSameUserAs(User targetUser) => _IsSameUserAs(targetUser: targetUser);
class _IsSameUserAs extends Matcher {
const _IsSameUserAs({
@required this.targetUser,
}) : assert(targetUser != null, '');
final User targetUser;
@override
bool matches(covariant User user, Map matchState) => user.id == targetUser.id;
@override
Description describe(Description description) =>
description.add('is same user as $targetUser');
}
Matcher isSameUserListAs(List<User> targetUserList) =>
_IsSameUserListAs(targetUserList: targetUserList);
class _IsSameUserListAs extends Matcher {
const _IsSameUserListAs({
@required this.targetUserList,
}) : assert(targetUserList != null, '');
final List<User> targetUserList;
@override
bool matches(covariant List<User> userList, Map matchState) {
bool matches = true;
for (var i = 0; i < userList.length; i++) {
final user = userList[i];
final targetUser = targetUserList[i];
matches = isSameUserAs(targetUser).matches(user, matchState);
if (!matches) break;
}
return matches;
}
@override
Description describe(Description description) =>
description.add('is same userList as $targetUserList');
}
@@ -0,0 +1,413 @@
import 'dart:async';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:stream_chat_flutter_core/src/message_list_core.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'mocks.dart';
void main() {
List<Message> _generateMessages({
int count = 3,
int offset = 0,
bool threads = false,
}) {
final users = List.generate(count, (index) {
index = count + offset;
return User(id: 'testUserId$index');
});
final messages = List.generate(
count,
(index) {
index = index + offset;
return Message(
id: 'testMessageId$index',
type: 'testType',
user: users[index],
createdAt: DateTime.now(),
shadowed: false,
replyCount: index,
updatedAt: DateTime.now(),
extraData: {'extra_test_field': 'extraTestData'},
text: 'Dummy text #$index',
pinned: true,
pinnedAt: DateTime.now(),
pinnedBy: User(id: 'testUserId$index'),
);
},
);
final threadMessages = List.generate(
count,
(index) {
index = index + offset;
return Message(
id: 'testThreadMessageId$index',
type: 'testType',
user: users[index],
parentId: messages[0].id,
createdAt: DateTime.now(),
shadowed: false,
replyCount: index,
updatedAt: DateTime.now(),
extraData: {'extra_test_field': 'extraTestData'},
text: 'Dummy text #$index',
pinned: true,
pinnedAt: DateTime.now(),
pinnedBy: User(id: 'testUserId$index'),
);
},
);
return threads ? threadMessages : messages;
}
test(
'should throw assertion error in case messageListBuilder is null',
() {
final messageListCore = () => MessageListCore(
messageListBuilder: null,
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorWidgetBuilder: (BuildContext context, Object error) =>
Offstage(),
);
expect(messageListCore, throwsA(isA<AssertionError>()));
},
);
test(
'should throw assertion error in case loadingBuilder is null',
() {
final messageListCore = () => MessageListCore(
messageListBuilder: (_, __) => Offstage(),
loadingBuilder: null,
emptyBuilder: (BuildContext context) => Offstage(),
errorWidgetBuilder: (BuildContext context, Object error) =>
Offstage(),
);
expect(messageListCore, throwsA(isA<AssertionError>()));
},
);
test(
'should throw assertion error in case emptyBuilder is null',
() {
final messageListCore = () => MessageListCore(
messageListBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: null,
errorWidgetBuilder: (BuildContext context, Object error) =>
Offstage(),
);
expect(messageListCore, throwsA(isA<AssertionError>()));
},
);
test(
'should throw assertion error in case errorWidgetBuilder is null',
() {
final messageListCore = () => MessageListCore(
messageListBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorWidgetBuilder: null,
);
expect(messageListCore, throwsA(isA<AssertionError>()));
},
);
testWidgets(
'should throw if MessageListCore is used where StreamChannel is not present '
'in the widget tree',
(tester) async {
const messageListCoreKey = Key('messageListCore');
final messageListCore = MessageListCore(
key: messageListCoreKey,
messageListBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorWidgetBuilder: (BuildContext context, Object error) => Offstage(),
);
await tester.pumpWidget(messageListCore);
expect(find.byKey(messageListCoreKey), findsNothing);
expect(tester.takeException(), isInstanceOf<Exception>());
},
);
testWidgets(
'should render MessageListCore if used with StreamChannel as an ancestor',
(tester) async {
const messageListCoreKey = Key('messageListCore');
final messageListCore = MessageListCore(
key: messageListCoreKey,
messageListBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorWidgetBuilder: (BuildContext context, Object error) => Offstage(),
);
final mockChannel = MockChannel();
when(mockChannel.state.isUpToDate).thenReturn(true);
await tester.pumpWidget(
StreamChannel(
channel: mockChannel,
child: messageListCore,
),
);
expect(find.byKey(messageListCoreKey), findsOneWidget);
},
);
testWidgets(
'should assign paginateData callback to MessageListController if passed',
(tester) async {
const messageListCoreKey = Key('messageListCore');
final controller = MessageListController();
final messageListCore = MessageListCore(
key: messageListCoreKey,
messageListBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorWidgetBuilder: (BuildContext context, Object error) => Offstage(),
messageListController: controller,
);
expect(controller.paginateData, isNull);
final mockChannel = MockChannel();
when(mockChannel.state.isUpToDate).thenReturn(true);
await tester.pumpWidget(
StreamChannel(
channel: mockChannel,
child: messageListCore,
),
);
expect(find.byKey(messageListCoreKey), findsOneWidget);
expect(controller.paginateData, isNotNull);
},
);
testWidgets(
'should build error widget if messagesStream emits error',
(tester) async {
const messageListCoreKey = Key('messageListCore');
const errorWidgetKey = Key('errorWidget');
final messageListCore = MessageListCore(
key: messageListCoreKey,
messageListBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorWidgetBuilder: (BuildContext context, Object error) => Offstage(
key: errorWidgetKey,
),
);
final mockChannel = MockChannel();
when(mockChannel.state.isUpToDate).thenReturn(true);
when(mockChannel.initialized).thenAnswer((_) async => true);
const error = 'Error! Error! Error!';
when(mockChannel.state.messagesStream)
.thenAnswer((_) => Stream.error(error));
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StreamChannel(
channel: mockChannel,
child: messageListCore,
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(errorWidgetKey), findsOneWidget);
},
);
testWidgets(
'should build empty widget if the channel is upToDate and '
'messagesStream emits empty data',
(tester) async {
const messageListCoreKey = Key('messageListCore');
const emptyWidgetKey = Key('emptyWidget');
final messageListCore = MessageListCore(
key: messageListCoreKey,
messageListBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(key: emptyWidgetKey),
errorWidgetBuilder: (BuildContext context, Object error) => Offstage(),
);
final mockChannel = MockChannel();
when(mockChannel.state.isUpToDate).thenReturn(true);
when(mockChannel.initialized).thenAnswer((_) async => true);
const messages = <Message>[];
when(mockChannel.state.messagesStream)
.thenAnswer((_) => Stream.value(messages));
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StreamChannel(
channel: mockChannel,
child: messageListCore,
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(emptyWidgetKey), findsOneWidget);
},
);
testWidgets(
'should build list widget if the channel is not upToDate and '
'messagesStream emits empty data',
(tester) async {
const messageListCoreKey = Key('messageListCore');
const listWidgetKey = Key('listWidget');
final messageListCore = MessageListCore(
key: messageListCoreKey,
messageListBuilder: (_, __) => Offstage(key: listWidgetKey),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorWidgetBuilder: (BuildContext context, Object error) => Offstage(),
);
final mockChannel = MockChannel();
when(mockChannel.state.isUpToDate).thenReturn(false);
when(mockChannel.initialized).thenAnswer((_) async => true);
const messages = <Message>[];
when(mockChannel.state.messagesStream)
.thenAnswer((_) => Stream.value(messages));
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StreamChannel(
channel: mockChannel,
child: messageListCore,
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(listWidgetKey), findsOneWidget);
},
);
testWidgets(
'should build list widget and save it for future rebuilds '
'if messagesStream emits some data',
(tester) async {
const messageListCoreKey = Key('messageListCore');
const listWidgetKey = Key('listWidget');
final messageListCore = MessageListCore(
key: messageListCoreKey,
messageListBuilder: (_, messages) => Container(
key: listWidgetKey,
child: Text(
messages.reversed.map((it) => it.id).join(','),
),
),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorWidgetBuilder: (BuildContext context, Object error) => Offstage(),
);
final mockChannel = MockChannel();
when(mockChannel.state.isUpToDate).thenReturn(true);
when(mockChannel.initialized).thenAnswer((_) async => true);
final messages = _generateMessages();
when(mockChannel.state.messagesStream)
.thenAnswer((_) => Stream.value(messages));
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StreamChannel(
channel: mockChannel,
child: messageListCore,
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(listWidgetKey), findsOneWidget);
expect(find.text(messages.map((it) => it.id).join(',')), findsOneWidget);
},
);
testWidgets(
'should build list widget with thread messages if parentMessage is passed',
(tester) async {
final messages = _generateMessages(threads: true);
final parentMessage = Message(id: messages.first.parentId);
const messageListCoreKey = Key('messageListCore');
const listWidgetKey = Key('listWidget');
final messageListCore = MessageListCore(
key: messageListCoreKey,
messageListBuilder: (_, messages) => Container(
key: listWidgetKey,
child: Text(
messages.reversed.map((it) => '${it.parentId}-${it.id}').join(','),
),
),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorWidgetBuilder: (BuildContext context, Object error) => Offstage(),
parentMessage: parentMessage,
);
final mockChannel = MockChannel();
when(mockChannel.state.isUpToDate).thenReturn(true);
when(mockChannel.initialized).thenAnswer((_) async => true);
final threads = {parentMessage.id: messages};
when(mockChannel.state.threads).thenReturn(threads);
when(mockChannel.state.threadsStream)
.thenAnswer((_) => Stream.value(threads));
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StreamChannel(
channel: mockChannel,
child: messageListCore,
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(listWidgetKey), findsOneWidget);
expect(
find.text(messages.map((it) => '${it.parentId}-${it.id}').join(',')),
findsOneWidget,
);
},
);
}
@@ -0,0 +1,344 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter_core/src/message_search_bloc.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'matchers/get_message_response_matcher.dart';
import 'mocks.dart';
void main() {
List<GetMessageResponse> _generateMessages({
int count = 3,
int offset = 0,
}) {
return List.generate(
count,
(index) {
index = index + offset;
return GetMessageResponse()
..message = Message(
id: 'testId$index',
text: 'testTextData$index',
)
..channel = ChannelModel(
cid: 'testCid',
);
},
);
}
test(
'should throw assertion error if child is null',
() async {
const messageSearchBlocKey = Key('messageSearchBloc');
final messageSearchBloc = () => MessageSearchBloc(
key: messageSearchBlocKey,
child: null,
);
expect(messageSearchBloc, throwsA(isA<AssertionError>()));
},
);
testWidgets(
'messageSearchBlocState.search() should throw if used where '
'StreamChat is not present in the widget tree',
(tester) async {
const messageSearchBlocKey = Key('messageSearchBloc');
const childKey = Key('child');
final messageSearchBloc = MessageSearchBloc(
key: messageSearchBlocKey,
child: Offstage(key: childKey),
);
await tester.pumpWidget(messageSearchBloc);
expect(find.byKey(messageSearchBlocKey), findsOneWidget);
expect(find.byKey(childKey), findsOneWidget);
final usersBlocState = tester.state<MessageSearchBlocState>(
find.byKey(messageSearchBlocKey),
);
try {
await usersBlocState.search();
} catch (e) {
expect(e, isInstanceOf<Exception>());
}
},
);
testWidgets(
'messageSearchBlocState.search() should emit data through usersStream',
(tester) async {
const messageSearchBlocKey = Key('messageSearchBloc');
const childKey = Key('child');
final messageSearchBloc = MessageSearchBloc(
key: messageSearchBlocKey,
child: Offstage(key: childKey),
);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: messageSearchBloc,
),
);
final messageSearchBlocState = tester.state<MessageSearchBlocState>(
find.byKey(messageSearchBlocKey),
);
final messageResponseList = _generateMessages();
when(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: anyNamed('paginationParams'),
)).thenAnswer(
(_) async => SearchMessagesResponse()..results = messageResponseList,
);
messageSearchBlocState.search();
await expectLater(
messageSearchBlocState.messagesStream,
emits(isSameMessageResponseListAs(messageResponseList)),
);
verify(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: anyNamed('paginationParams'),
)).called(1);
},
);
testWidgets(
'messageSearchBlocState.messagesStream should emit error '
'if client.search() throws',
(tester) async {
const messageSearchBlocKey = Key('messageSearchBloc');
const childKey = Key('child');
final messageSearchBloc = MessageSearchBloc(
key: messageSearchBlocKey,
child: Offstage(key: childKey),
);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: messageSearchBloc,
),
);
final messageSearchBlocState = tester.state<MessageSearchBlocState>(
find.byKey(messageSearchBlocKey),
);
const error = 'Error! Error! Error!';
when(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: anyNamed('paginationParams'),
)).thenThrow(error);
messageSearchBlocState.search();
await expectLater(
messageSearchBlocState.messagesStream,
emitsError(error),
);
verify(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: anyNamed('paginationParams'),
)).called(1);
},
);
testWidgets(
'calling messageSearchBlocState.search() again with an offset '
'should emit new data through messagesStream and also emit loading state '
'through queryMessagesLoading',
(tester) async {
const messageSearchBlocKey = Key('messageSearchBloc');
const childKey = Key('child');
final messageSearchBloc = MessageSearchBloc(
key: messageSearchBlocKey,
child: Offstage(key: childKey),
);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: messageSearchBloc,
),
);
final messageSearchBlocState = tester.state<MessageSearchBlocState>(
find.byKey(messageSearchBlocKey),
);
final messageResponseList = _generateMessages();
when(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: anyNamed('paginationParams'),
)).thenAnswer(
(_) async => SearchMessagesResponse()..results = messageResponseList,
);
messageSearchBlocState.search();
await expectLater(
messageSearchBlocState.messagesStream,
emits(isSameMessageResponseListAs(messageResponseList)),
);
verify(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: anyNamed('paginationParams'),
)).called(1);
final offset = messageResponseList.length;
final paginatedMessageResponseList = _generateMessages(offset: offset);
final pagination = PaginationParams(offset: offset);
when(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: pagination,
)).thenAnswer(
(_) async =>
SearchMessagesResponse()..results = paginatedMessageResponseList,
);
messageSearchBlocState.search(pagination: pagination);
await Future.wait([
expectLater(
messageSearchBlocState.queryMessagesLoading,
emitsInOrder([true, false]),
),
expectLater(
messageSearchBlocState.messagesStream,
emits(isSameMessageResponseListAs(
messageResponseList + paginatedMessageResponseList,
)),
),
]);
verify(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: pagination,
)).called(1);
},
);
testWidgets(
'calling messageSearchBlocState.search() again with an offset '
'should emit error through queryUsersLoading if '
'client.search() throws',
(tester) async {
const messageSearchBlocKey = Key('messageSearchBloc');
const childKey = Key('child');
final messageSearchBloc = MessageSearchBloc(
key: messageSearchBlocKey,
child: Offstage(key: childKey),
);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: messageSearchBloc,
),
);
final messageSearchBlocState = tester.state<MessageSearchBlocState>(
find.byKey(messageSearchBlocKey),
);
final messageResponseList = _generateMessages();
when(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: anyNamed('paginationParams'),
)).thenAnswer(
(_) async => SearchMessagesResponse()..results = messageResponseList,
);
messageSearchBlocState.search();
await expectLater(
messageSearchBlocState.messagesStream,
emits(isSameMessageResponseListAs(messageResponseList)),
);
verify(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: anyNamed('paginationParams'),
)).called(1);
final offset = messageResponseList.length;
final pagination = PaginationParams(offset: offset);
const error = 'Error! Error! Error!';
when(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: pagination,
)).thenThrow(error);
messageSearchBlocState.search(pagination: pagination);
await expectLater(
messageSearchBlocState.queryMessagesLoading,
emitsError(error),
);
verify(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: pagination,
)).called(1);
},
);
}
@@ -0,0 +1,535 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'package:stream_chat_flutter_core/src/message_search_list_core.dart';
import 'mocks.dart';
void main() {
List<GetMessageResponse> _generateMessages({
int count = 3,
int offset = 0,
}) {
return List.generate(
count,
(index) {
index = index + offset;
return GetMessageResponse()
..message = Message(
id: 'testId$index',
text: 'testTextData$index',
)
..channel = ChannelModel(
cid: 'testCid',
);
},
);
}
test(
'should throw assertion error in case childBuilder is null',
() {
final messageSearchListCore = () => MessageSearchListCore(
childBuilder: null,
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (BuildContext context, Object error) => Offstage(),
);
expect(messageSearchListCore, throwsA(isA<AssertionError>()));
},
);
test(
'should throw assertion error in case loadingBuilder is null',
() {
final messageSearchListCore = () => MessageSearchListCore(
childBuilder: (List<GetMessageResponse> messages) => Offstage(),
loadingBuilder: null,
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (BuildContext context, Object error) => Offstage(),
);
expect(messageSearchListCore, throwsA(isA<AssertionError>()));
},
);
test(
'should throw assertion error in case emptyBuilder is null',
() {
final messageSearchListCore = () => MessageSearchListCore(
childBuilder: (List<GetMessageResponse> messages) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: null,
errorBuilder: (BuildContext context, Object error) => Offstage(),
);
expect(messageSearchListCore, throwsA(isA<AssertionError>()));
},
);
test(
'should throw assertion error in case errorBuilder is null',
() {
final messageSearchListCore = () => MessageSearchListCore(
childBuilder: (List<GetMessageResponse> messages) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: null,
);
expect(messageSearchListCore, throwsA(isA<AssertionError>()));
},
);
testWidgets(
'should throw if MessageSearchListCore is used where MessageSearchBloc '
'is not present in the widget tree',
(tester) async {
const messageSearchListCoreKey = Key('messageSearchListCore');
final messageSearchListCore = MessageSearchListCore(
key: messageSearchListCoreKey,
childBuilder: (List<GetMessageResponse> messages) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (BuildContext context, Object error) => Offstage(),
);
await tester.pumpWidget(messageSearchListCore);
expect(find.byKey(messageSearchListCoreKey), findsNothing);
expect(tester.takeException(), isInstanceOf<Exception>());
},
);
testWidgets(
'should render MessageSearchListCore if used with '
'MessageSearchListCore as an ancestor',
(tester) async {
const messageSearchListCoreKey = Key('messageSearchListCore');
final messageSearchListCore = MessageSearchListCore(
key: messageSearchListCoreKey,
childBuilder: (List<GetMessageResponse> messages) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (BuildContext context, Object error) => Offstage(),
);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: MessageSearchBloc(
child: messageSearchListCore,
),
),
);
expect(find.byKey(messageSearchListCoreKey), findsOneWidget);
},
);
testWidgets(
'should assign loadData and paginateData callback to '
'UserListController if passed',
(tester) async {
const messageSearchListCoreKey = Key('messageSearchListCore');
final controller = MessageSearchListController();
final messageSearchListCore = MessageSearchListCore(
key: messageSearchListCoreKey,
childBuilder: (List<GetMessageResponse> messages) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (BuildContext context, Object error) => Offstage(),
messageSearchListController: controller,
);
expect(controller.loadData, isNull);
expect(controller.paginateData, isNull);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: MessageSearchBloc(
child: messageSearchListCore,
),
),
);
expect(find.byKey(messageSearchListCoreKey), findsOneWidget);
expect(controller.loadData, isNotNull);
expect(controller.paginateData, isNotNull);
},
);
testWidgets(
'should build error widget if messageSearchBloc.messagesStream emits error',
(tester) async {
const messageSearchListCoreKey = Key('messageSearchListCore');
const errorWidgetKey = Key('errorWidget');
final messageSearchListCore = MessageSearchListCore(
key: messageSearchListCoreKey,
childBuilder: (List<GetMessageResponse> messages) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (BuildContext context, Object error) => Offstage(
key: errorWidgetKey,
),
);
final mockClient = MockClient();
const error = 'Error! Error! Error!';
when(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: anyNamed('paginationParams'),
)).thenThrow(error);
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: MessageSearchBloc(
child: messageSearchListCore,
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(errorWidgetKey), findsOneWidget);
verify(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: anyNamed('paginationParams'),
)).called(1);
},
);
testWidgets(
'should build empty widget if messageSearchBloc.messagesStream '
'emits empty data',
(tester) async {
const messageSearchListCoreKey = Key('messageSearchListCore');
const emptyWidgetKey = Key('emptyWidget');
final messageSearchListCore = MessageSearchListCore(
key: messageSearchListCoreKey,
childBuilder: (List<GetMessageResponse> messages) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(key: emptyWidgetKey),
errorBuilder: (BuildContext context, Object error) => Offstage(),
);
final mockClient = MockClient();
final messageResponseList = <GetMessageResponse>[];
when(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: anyNamed('paginationParams'),
)).thenAnswer(
(_) async => SearchMessagesResponse()..results = messageResponseList,
);
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: MessageSearchBloc(
child: messageSearchListCore,
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(emptyWidgetKey), findsOneWidget);
verify(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: anyNamed('paginationParams'),
)).called(1);
},
);
testWidgets(
'should build child widget if usersBlocState.usersStream emits some data',
(tester) async {
const messageSearchListCoreKey = Key('messageSearchListCore');
const childWidgetKey = Key('childWidget');
final messageSearchListCore = MessageSearchListCore(
key: messageSearchListCoreKey,
childBuilder: (List<GetMessageResponse> messages) => Offstage(
key: childWidgetKey,
),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (BuildContext context, Object error) => Offstage(),
);
final mockClient = MockClient();
final messageResponseList = _generateMessages();
when(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: anyNamed('paginationParams'),
)).thenAnswer(
(_) async => SearchMessagesResponse()..results = messageResponseList,
);
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: MessageSearchBloc(
child: messageSearchListCore,
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(childWidgetKey), findsOneWidget);
verify(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: anyNamed('paginationParams'),
)).called(1);
},
);
testWidgets(
'should build child widget with paginated data '
'on calling channelListCoreState.paginateData',
(tester) async {
const messageSearchListCoreKey = Key('messageSearchListCore');
const childWidgetKey = Key('childWidget');
const pagination = PaginationParams();
final messageSearchListCore = MessageSearchListCore(
key: messageSearchListCoreKey,
childBuilder: (List<GetMessageResponse> messages) => Container(
key: childWidgetKey,
child: Text(
messages.map((e) => '${e.channel.cid}-${e.message.id}').join(','),
),
),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (BuildContext context, Object error) => Offstage(),
paginationParams: pagination,
);
final mockClient = MockClient();
final messageResponseList = _generateMessages();
when(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: pagination,
)).thenAnswer(
(_) async => SearchMessagesResponse()..results = messageResponseList,
);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StreamChatCore(
client: mockClient,
child: MessageSearchBloc(
child: messageSearchListCore,
),
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(childWidgetKey), findsOneWidget);
expect(
find.text(
messageResponseList
.map((e) => '${e.channel.cid}-${e.message.id}')
.join(','),
),
findsOneWidget,
);
verify(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: pagination,
)).called(1);
final messageSearchListCoreState =
tester.state<MessageSearchListCoreState>(
find.byKey(messageSearchListCoreKey),
);
final offset = messageResponseList.length;
final paginatedMessageResponseList = _generateMessages(offset: offset);
final updatedPagination = pagination.copyWith(offset: offset);
when(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: updatedPagination,
)).thenAnswer(
(_) async =>
SearchMessagesResponse()..results = paginatedMessageResponseList,
);
await messageSearchListCoreState.paginateData();
await tester.pumpAndSettle();
expect(find.byKey(childWidgetKey), findsOneWidget);
expect(
find.text([
...messageResponseList,
...paginatedMessageResponseList,
].map((e) => '${e.channel.cid}-${e.message.id}').join(',')),
findsOneWidget,
);
verify(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: updatedPagination,
)).called(1);
},
);
testWidgets(
'should rebuild MessageSearchListCore with updated widget data '
'on calling setState()',
(tester) async {
const pagination = PaginationParams();
StateSetter _stateSetter;
int limit = pagination.limit;
const messageSearchListCoreKey = Key('messageSearchListCore');
const childWidgetKey = Key('childWidget');
MessageSearchListCore messageSearchListCoreBuilder(int limit) =>
MessageSearchListCore(
key: messageSearchListCoreKey,
childBuilder: (List<GetMessageResponse> messages) => Container(
key: childWidgetKey,
child: Text(
messages
.map((e) => '${e.channel.cid}-${e.message.id}')
.join(','),
),
),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (BuildContext context, Object error) => Offstage(),
paginationParams: pagination.copyWith(limit: limit),
);
final mockClient = MockClient();
final messageResponseList = _generateMessages();
when(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: pagination,
)).thenAnswer(
(_) async => SearchMessagesResponse()..results = messageResponseList,
);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StreamChatCore(
client: mockClient,
child: MessageSearchBloc(
child: StatefulBuilder(builder: (context, stateSetter) {
// Assigning stateSetter for rebuilding UserListCore
_stateSetter = stateSetter;
return messageSearchListCoreBuilder(limit);
}),
),
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(childWidgetKey), findsOneWidget);
expect(
find.text(
messageResponseList
.map((e) => '${e.channel.cid}-${e.message.id}')
.join(','),
),
findsOneWidget,
);
verify(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: pagination,
)).called(1);
// Rebuilding MessageSearchListCore with new pagination limit
_stateSetter(() => limit = 6);
final updatedMessageResponseList = _generateMessages(count: limit);
final updatedPagination = pagination.copyWith(limit: limit);
when(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: updatedPagination,
)).thenAnswer(
(_) async =>
SearchMessagesResponse()..results = updatedMessageResponseList,
);
await tester.pumpAndSettle();
expect(find.byKey(childWidgetKey), findsOneWidget);
expect(
find.text(updatedMessageResponseList
.map((e) => '${e.channel.cid}-${e.message.id}')
.join(',')),
findsOneWidget,
);
verify(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: updatedPagination,
)).called(1);
},
);
}
@@ -1,10 +1,39 @@
import 'package:mockito/mockito.dart';
import 'package:stream_chat/stream_chat.dart';
class MockClient extends Mock implements StreamChatClient {}
class MockLogger extends Mock implements Logger {}
class MockClientState extends Mock implements ClientState {}
class MockClient extends Mock implements StreamChatClient {
final Logger logger = MockLogger();
class MockChannel extends Mock implements Channel {}
ClientState _state;
@override
ClientState get state => _state ??= MockClientState();
}
class MockClientState extends Mock implements ClientState {
OwnUser _user;
@override
OwnUser get user => _user ??= OwnUser(
id: 'testUserId',
role: 'admin',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
}
class MockChannel extends Mock implements Channel {
ChannelClientState _state;
@override
ChannelClientState get state => _state ??= MockChannelState();
StreamChatClient _client;
@override
StreamChatClient get client => _client ??= MockClient();
}
class MockChannelState extends Mock implements ChannelClientState {}
@@ -0,0 +1,343 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'mocks.dart';
void main() {
List<Message> _generateMessages({
int count = 3,
int offset = 0,
bool threads = false,
}) {
final users = List.generate(count, (index) {
index = count + offset;
return User(id: 'testUserId$index');
});
final messages = List.generate(
count,
(index) {
index = index + offset;
return Message(
id: 'testMessageId$index',
type: 'testType',
user: users[index],
createdAt: DateTime.now(),
shadowed: false,
replyCount: index,
updatedAt: DateTime.now(),
extraData: {'extra_test_field': 'extraTestData'},
text: 'Dummy text #$index',
pinned: true,
pinnedAt: DateTime.now(),
pinnedBy: User(id: 'testUserId$index'),
);
},
);
final threadMessages = List.generate(
count,
(index) {
index = index + offset;
return Message(
id: 'testThreadMessageId$index',
type: 'testType',
user: users[index],
parentId: messages[0].id,
createdAt: DateTime.now(),
shadowed: false,
replyCount: index,
updatedAt: DateTime.now(),
extraData: {'extra_test_field': 'extraTestData'},
text: 'Dummy text #$index',
pinned: true,
pinnedAt: DateTime.now(),
pinnedBy: User(id: 'testUserId$index'),
);
},
);
return threads ? threadMessages : messages;
}
test(
'should throw assertion error if child is null',
() async {
final mockChannel = MockChannel();
const streamChannelKey = Key('streamChannel');
final streamChannel = () => StreamChannel(
key: streamChannelKey,
channel: mockChannel,
child: null,
);
expect(streamChannel, throwsA(isA<AssertionError>()));
},
);
test(
'should throw assertion error if channel is null',
() async {
const streamChannelKey = Key('streamChannel');
final streamChannel = () => StreamChannel(
key: streamChannelKey,
child: Offstage(),
channel: null,
);
expect(streamChannel, throwsA(isA<AssertionError>()));
},
);
testWidgets(
'should render StreamChannel if both channel and child is provided',
(tester) async {
final mockChannel = MockChannel();
const streamChannelKey = Key('streamChannel');
const childKey = Key('childKey');
final streamChannel = StreamChannel(
key: streamChannelKey,
channel: mockChannel,
child: Offstage(key: childKey),
);
await tester.pumpWidget(streamChannel);
expect(find.byKey(streamChannelKey), findsOneWidget);
expect(find.byKey(childKey), findsOneWidget);
},
);
testWidgets(
'should build error widget if channel.initialized throws',
(tester) async {
final mockChannel = MockChannel();
const streamChannelKey = Key('streamChannel');
const childKey = Key('childKey');
final streamChannel = StreamChannel(
key: streamChannelKey,
channel: mockChannel,
child: Offstage(key: childKey),
);
final errorMessage = 'Error! Error! Error!';
final error = DioError(type: DioErrorType.RESPONSE, error: errorMessage);
when(mockChannel.initialized).thenAnswer((_) => Future.error(error));
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: streamChannel,
),
);
await tester.pumpAndSettle();
expect(find.text(errorMessage), findsOneWidget);
verify(mockChannel.initialized).called(1);
},
);
testWidgets(
'should show circular progress indicator showLoading is true '
'and channel is still not initialized',
(tester) async {
final mockChannel = MockChannel();
const streamChannelKey = Key('streamChannel');
const childKey = Key('childKey');
final streamChannel = StreamChannel(
key: streamChannelKey,
channel: mockChannel,
child: Offstage(key: childKey),
showLoading: true,
);
when(mockChannel.initialized).thenAnswer((_) async => false);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: streamChannel,
),
);
await tester.pump(const Duration(seconds: 1));
expect(find.byType(CircularProgressIndicator), findsOneWidget);
verify(mockChannel.initialized).called(1);
},
);
testWidgets(
'should preload extra messages if initialMessageId is provided',
(tester) async {
final mockChannel = MockChannel();
const streamChannelKey = Key('streamChannel');
const childKey = Key('childKey');
final streamChannel = StreamChannel(
key: streamChannelKey,
channel: mockChannel,
child: Offstage(key: childKey),
initialMessageId: 'testInitialMessageId',
);
when(mockChannel.initialized).thenAnswer((_) async => true);
final messages = _generateMessages();
when(mockChannel.query(
options: anyNamed('options'),
messagesPagination: anyNamed('messagesPagination'),
membersPagination: anyNamed('membersPagination'),
watchersPagination: anyNamed('watchersPagination'),
preferOffline: anyNamed('preferOffline'),
)).thenAnswer((_) async => ChannelState(messages: messages));
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: streamChannel,
),
);
await tester.pumpAndSettle();
verify(mockChannel.initialized).called(1);
verify(mockChannel.query(
options: anyNamed('options'),
messagesPagination: anyNamed('messagesPagination'),
membersPagination: anyNamed('membersPagination'),
watchersPagination: anyNamed('watchersPagination'),
preferOffline: anyNamed('preferOffline'),
)).called(
2, // Fetching After messages + Fetching Before messages,
);
},
);
testWidgets(
'should rebuild StreamChannel with updated widget data '
'on calling setState()',
(tester) async {
StateSetter _stateSetter;
var initialMessageId = 'testInitialMessageId';
final mockChannel = MockChannel();
const streamChannelKey = Key('streamChannel');
const childKey = Key('childKey');
StreamChannel streamChannelBuilder(String initialMessageId) =>
StreamChannel(
key: streamChannelKey,
channel: mockChannel,
child: Offstage(key: childKey),
initialMessageId: initialMessageId,
);
final beforePagination = PaginationParams(
lessThan: initialMessageId,
limit: 20,
);
final afterPagination = PaginationParams(
greaterThanOrEqual: initialMessageId,
limit: 20,
);
when(mockChannel.initialized).thenAnswer((_) async => true);
final messages = _generateMessages();
when(mockChannel.query(
options: anyNamed('options'),
messagesPagination: beforePagination,
membersPagination: anyNamed('membersPagination'),
watchersPagination: anyNamed('watchersPagination'),
preferOffline: anyNamed('preferOffline'),
)).thenAnswer((_) async => ChannelState(messages: messages));
when(mockChannel.query(
options: anyNamed('options'),
messagesPagination: afterPagination,
membersPagination: anyNamed('membersPagination'),
watchersPagination: anyNamed('watchersPagination'),
preferOffline: anyNamed('preferOffline'),
)).thenAnswer((_) async => ChannelState(messages: messages));
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (context, stateSetter) {
// Assigning stateSetter for rebuilding StreamChannel
_stateSetter = stateSetter;
return streamChannelBuilder(initialMessageId);
},
),
),
);
await tester.pumpAndSettle();
verify(mockChannel.query(
options: anyNamed('options'),
messagesPagination: beforePagination,
membersPagination: anyNamed('membersPagination'),
watchersPagination: anyNamed('watchersPagination'),
preferOffline: anyNamed('preferOffline'),
)).called(1);
verify(mockChannel.query(
options: anyNamed('options'),
messagesPagination: afterPagination,
membersPagination: anyNamed('membersPagination'),
watchersPagination: anyNamed('watchersPagination'),
preferOffline: anyNamed('preferOffline'),
)).called(1);
_stateSetter(() => initialMessageId = 'testInitialMessageId2');
final updatedBeforePagination = beforePagination.copyWith(
lessThan: initialMessageId,
);
final updatedAfterPagination = afterPagination.copyWith(
greaterThanOrEqual: initialMessageId,
);
when(mockChannel.query(
options: anyNamed('options'),
messagesPagination: updatedBeforePagination,
membersPagination: anyNamed('membersPagination'),
watchersPagination: anyNamed('watchersPagination'),
preferOffline: anyNamed('preferOffline'),
)).thenAnswer((_) async => ChannelState(messages: messages));
when(mockChannel.query(
options: anyNamed('options'),
messagesPagination: updatedAfterPagination,
membersPagination: anyNamed('membersPagination'),
watchersPagination: anyNamed('watchersPagination'),
preferOffline: anyNamed('preferOffline'),
)).thenAnswer((_) async => ChannelState(messages: messages));
await tester.pumpAndSettle();
verify(mockChannel.query(
options: anyNamed('options'),
messagesPagination: updatedBeforePagination,
membersPagination: anyNamed('membersPagination'),
watchersPagination: anyNamed('watchersPagination'),
preferOffline: anyNamed('preferOffline'),
)).called(1);
verify(mockChannel.query(
options: anyNamed('options'),
messagesPagination: updatedAfterPagination,
membersPagination: anyNamed('membersPagination'),
watchersPagination: anyNamed('watchersPagination'),
preferOffline: anyNamed('preferOffline'),
)).called(1);
},
);
}
@@ -1,6 +1,5 @@
import 'dart:async';
import 'package:fake_async/fake_async.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
@@ -8,169 +7,283 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'mocks.dart';
class MockShowLocalNotifications extends Mock {
class MockOnBackgroundEventReceived extends Mock {
void call(Event event);
}
void main() {
testWidgets(
'StreamChatCore.of(context) should throw an exception',
(WidgetTester tester) async {
await tester.pumpWidget(
Builder(
builder: (context) {
expect(() => StreamChatCore.of(context), throwsException);
return Container();
},
),
);
test(
'should throw assertion error in case client is null',
() {
final streamChatCore = () => StreamChatCore(
client: null,
child: Offstage(),
);
expect(streamChatCore, throwsA(isA<AssertionError>()));
},
);
test(
'should throw assertion error in case child is null',
() {
final mockClient = MockClient();
final streamChatCore = () => StreamChatCore(
client: mockClient,
child: null,
);
expect(streamChatCore, throwsA(isA<AssertionError>()));
},
);
testWidgets(
'StreamChatCore.of(context) should return the StreamChatCore ancestor',
(WidgetTester tester) async {
final client = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: client,
child: Builder(
builder: (context) {
final sc = StreamChatCore.of(context);
expect(sc, isNotNull);
return Container();
},
),
),
'should render StreamChatCore if both client and child is provided',
(tester) async {
final mockClient = MockClient();
const streamChatCoreKey = Key('streamChatCore');
const childKey = Key('child');
final streamChatCore = StreamChatCore(
key: streamChatCoreKey,
client: mockClient,
child: Offstage(key: childKey),
);
await tester.pumpWidget(streamChatCore);
expect(find.byKey(streamChatCoreKey), findsOneWidget);
expect(find.byKey(childKey), findsOneWidget);
},
);
testWidgets(
'StreamChatCore.of(context).client should return the client',
(WidgetTester tester) async {
final client = MockClient();
final clientState = MockClientState();
when(client.state).thenReturn(clientState);
when(clientState.user).thenReturn(
OwnUser(
id: 'test',
),
);
final userStream = Stream<OwnUser>.value(
OwnUser(
id: 'test',
),
);
when(clientState.userStream).thenAnswer(
(_) => userStream,
'should render StreamChatCore if both client and child is provided',
(tester) async {
final mockClient = MockClient();
const streamChatCoreKey = Key('streamChatCore');
const childKey = Key('child');
final streamChatCore = StreamChatCore(
key: streamChatCoreKey,
client: mockClient,
child: Offstage(key: childKey),
);
await tester.pumpWidget(
StreamChatCore(
client: client,
child: Builder(
builder: (context) {
final sc = StreamChatCore.of(context);
expect(sc.client, client);
expect(sc.user, client.state.user);
expect(sc.userStream, userStream);
return Container();
},
),
),
);
await tester.pumpWidget(streamChatCore);
expect(find.byKey(streamChatCoreKey), findsOneWidget);
expect(find.byKey(childKey), findsOneWidget);
},
);
testWidgets(
'StreamChatCore should disconnect on background',
(WidgetTester tester) async {
fakeAsync((_async) {
final client = MockClient();
final clientState = MockClientState();
final channel = MockChannel();
when(client.state).thenReturn(clientState);
when(clientState.user).thenReturn(
OwnUser(
id: 'test',
),
);
final showLocalNotificationMock = MockShowLocalNotifications().call;
final eventStreamController = StreamController<Event>();
when(client.on()).thenAnswer((_) => eventStreamController.stream);
'didChangeAppLifecycleState should call client.disconnect() and return '
'if onBackgroundEventReceived is null and the widget lifestyle changes to '
'AppLifecycleState.paused',
(tester) async {
final mockClient = MockClient();
const streamChatCoreKey = Key('streamChatCore');
const childKey = Key('child');
final streamChatCore = StreamChatCore(
key: streamChatCoreKey,
client: mockClient,
child: Offstage(key: childKey),
);
when(client.channel('test', id: 'testid')).thenReturn(channel);
await tester.pumpWidget(streamChatCore);
final scKey = GlobalKey<StreamChatCoreState>();
tester.pumpWidget(
StreamChatCore(
key: scKey,
client: client,
onBackgroundEventReceived: showLocalNotificationMock,
backgroundKeepAlive: const Duration(seconds: 4),
child: Builder(
builder: (context) => Container(),
),
),
expect(find.byKey(streamChatCoreKey), findsOneWidget);
expect(find.byKey(childKey), findsOneWidget);
when(mockClient.disconnect()).thenAnswer((_) async {
return;
});
final streamChatCoreState = tester.state<StreamChatCoreState>(
find.byKey(streamChatCoreKey),
);
streamChatCoreState.didChangeAppLifecycleState(AppLifecycleState.paused);
verify(mockClient.disconnect()).called(1);
},
);
testWidgets(
'didChangeAppLifecycleState should subscribe to client events for '
'backgroundKeepAlive duration if onBackgroundEventReceived is provided '
'and the widget lifestyle changes to AppLifecycleState.paused',
(tester) async {
await tester.runAsync(() async {
final mockClient = MockClient();
final mockOnBackgroundEventReceived = MockOnBackgroundEventReceived();
const backgroundKeepAlive = const Duration(seconds: 3);
const streamChatCoreKey = Key('streamChatCore');
const childKey = Key('child');
final streamChatCore = StreamChatCore(
key: streamChatCoreKey,
client: mockClient,
child: Offstage(key: childKey),
onBackgroundEventReceived: mockOnBackgroundEventReceived,
backgroundKeepAlive: backgroundKeepAlive,
);
scKey.currentState.didChangeAppLifecycleState(AppLifecycleState.paused);
await tester.pumpWidget(streamChatCore);
_async.elapse(const Duration(seconds: 5));
expect(find.byKey(streamChatCoreKey), findsOneWidget);
expect(find.byKey(childKey), findsOneWidget);
verify(client.disconnect()).called(1);
eventStreamController.close();
final event = Event();
when(mockClient.on()).thenAnswer((_) => Stream.value(event));
when(mockClient.disconnect()).thenAnswer((_) async {
return;
});
final streamChatCoreState = tester.state<StreamChatCoreState>(
find.byKey(streamChatCoreKey),
);
streamChatCoreState
.didChangeAppLifecycleState(AppLifecycleState.paused);
await untilCalled(mockOnBackgroundEventReceived.call(event));
verify(mockOnBackgroundEventReceived.call(event)).called(1);
await Future.delayed(backgroundKeepAlive);
verify(mockClient.disconnect()).called(1);
verifyNever(mockOnBackgroundEventReceived.call(event));
});
},
);
testWidgets(
'StreamChatCore should handle notifications when on background and '
'connected',
(WidgetTester tester) async {
final client = MockClient();
final clientState = MockClientState();
final channel = MockChannel();
when(client.state).thenReturn(clientState);
when(clientState.user).thenReturn(
OwnUser(
id: 'test',
),
);
final showLocalNotificationMock = MockShowLocalNotifications().call;
final eventStreamController = StreamController<Event>();
when(client.on()).thenAnswer((_) => eventStreamController.stream);
'didChangeAppLifecycleState should cancel the backgroundKeepAlive timer '
'if it is currently running in case the widget lifestyle changes to '
'AppLifecycleState.resume',
(tester) async {
await tester.runAsync(() async {
final mockClient = MockClient();
final mockOnBackgroundEventReceived = MockOnBackgroundEventReceived();
const backgroundKeepAlive = const Duration(seconds: 3);
const streamChatCoreKey = Key('streamChatCore');
const childKey = Key('child');
final streamChatCore = StreamChatCore(
key: streamChatCoreKey,
client: mockClient,
child: Offstage(key: childKey),
onBackgroundEventReceived: mockOnBackgroundEventReceived,
backgroundKeepAlive: backgroundKeepAlive,
);
when(client.channel('test', id: 'testid')).thenReturn(channel);
await tester.pumpWidget(streamChatCore);
final scKey = GlobalKey<StreamChatCoreState>();
await tester.pumpWidget(
StreamChatCore(
key: scKey,
client: client,
onBackgroundEventReceived: showLocalNotificationMock,
backgroundKeepAlive: const Duration(seconds: 4),
child: Builder(
builder: (context) => Container(),
),
),
);
expect(find.byKey(streamChatCoreKey), findsOneWidget);
expect(find.byKey(childKey), findsOneWidget);
scKey.currentState.didChangeAppLifecycleState(AppLifecycleState.paused);
final event = Event(
type: EventType.messageNew,
message: Message(text: 'hey'),
channelType: 'test',
channelId: 'testid',
user: User(id: 'other user'),
);
eventStreamController.add(event);
final event = Event();
when(mockClient.on()).thenAnswer((_) => Stream.value(event));
await untilCalled(showLocalNotificationMock(event));
final streamChatCoreState = tester.state<StreamChatCoreState>(
find.byKey(streamChatCoreKey),
);
verify(showLocalNotificationMock(event)).called(1);
eventStreamController.close();
streamChatCoreState
.didChangeAppLifecycleState(AppLifecycleState.paused);
await untilCalled(mockOnBackgroundEventReceived.call(event));
verify(mockOnBackgroundEventReceived.call(event)).called(1);
streamChatCoreState
.didChangeAppLifecycleState(AppLifecycleState.resumed);
verifyNever(mockOnBackgroundEventReceived.call(event));
});
},
);
testWidgets(
'didChangeAppLifecycleState should call client.connect() '
'if the connectionStatus is ConnectionStatus.disconnected in case the '
'widget lifestyle changes to AppLifecycleState.resume',
(tester) async {
await tester.runAsync(() async {
final mockClient = MockClient();
const streamChatCoreKey = Key('streamChatCore');
const childKey = Key('child');
final streamChatCore = StreamChatCore(
key: streamChatCoreKey,
client: mockClient,
child: Offstage(key: childKey),
);
await tester.pumpWidget(streamChatCore);
expect(find.byKey(streamChatCoreKey), findsOneWidget);
expect(find.byKey(childKey), findsOneWidget);
final event = Event();
when(mockClient.on()).thenAnswer((_) => Stream.value(event));
when(mockClient.connect()).thenAnswer((_) async => event);
when(mockClient.wsConnectionStatus)
.thenReturn(ConnectionStatus.disconnected);
final streamChatCoreState = tester.state<StreamChatCoreState>(
find.byKey(streamChatCoreKey),
);
streamChatCoreState
.didChangeAppLifecycleState(AppLifecycleState.paused);
await Future.delayed(const Duration(seconds: 1));
streamChatCoreState
.didChangeAppLifecycleState(AppLifecycleState.resumed);
verify(mockClient.connect()).called(1);
});
},
);
testWidgets(
'streamChatCoreState.userStream should emit all the user events '
'provided by client',
(tester) async {
await tester.runAsync(() async {
final userController = StreamController<OwnUser>();
final mockClient = MockClient();
const streamChatCoreKey = Key('streamChatCore');
const childKey = Key('child');
final streamChatCore = StreamChatCore(
key: streamChatCoreKey,
client: mockClient,
child: Offstage(key: childKey),
);
await tester.pumpWidget(streamChatCore);
expect(find.byKey(streamChatCoreKey), findsOneWidget);
expect(find.byKey(childKey), findsOneWidget);
when(mockClient.state.userStream)
.thenAnswer((_) => userController.stream);
final streamChatCoreState = tester.state<StreamChatCoreState>(
find.byKey(streamChatCoreKey),
);
final ownUser = OwnUser(id: 'testUserId');
userController.add(ownUser);
await expectLater(
streamChatCoreState.userStream,
emits(ownUser),
);
addTearDown(() {
userController.close();
});
});
},
);
}
@@ -0,0 +1,572 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:stream_chat_flutter_core/src/user_list_core.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'mocks.dart';
void main() {
const alphabets = 'abcdefghijklmnopqrstuvwxyz';
List<User> _generateUsers({
int count = 3,
int offset = 0,
}) {
return List.generate(
count,
(index) {
index = index + offset;
return User(
id: 'testId$index',
role: 'testRole$index',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
lastActive: DateTime.now(),
online: true,
banned: false,
extraData: {
'name': '${alphabets[index]}-testName',
},
);
},
);
}
test(
'should throw assertion error in case listBuilder is null',
() {
final userListCore = () => UserListCore(
listBuilder: null,
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (Object error) => Offstage(),
);
expect(userListCore, throwsA(isA<AssertionError>()));
},
);
test(
'should throw assertion error in case loadingBuilder is null',
() {
final userListCore = () => UserListCore(
listBuilder: (_, __) => Offstage(),
loadingBuilder: null,
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (Object error) => Offstage(),
);
expect(userListCore, throwsA(isA<AssertionError>()));
},
);
test(
'should throw assertion error in case emptyBuilder is null',
() {
final userListCore = () => UserListCore(
listBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: null,
errorBuilder: (Object error) => Offstage(),
);
expect(userListCore, throwsA(isA<AssertionError>()));
},
);
test(
'should throw assertion error in case errorBuilder is null',
() {
final userListCore = () => UserListCore(
listBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: null,
);
expect(userListCore, throwsA(isA<AssertionError>()));
},
);
testWidgets(
'should throw if UserListCore is used where UsersBloc is not present '
'in the widget tree',
(tester) async {
const userListCoreKey = Key('userListCore');
final userListCore = UserListCore(
key: userListCoreKey,
listBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (Object error) => Offstage(),
);
await tester.pumpWidget(userListCore);
expect(find.byKey(userListCoreKey), findsNothing);
expect(tester.takeException(), isInstanceOf<Exception>());
},
);
testWidgets(
'should render UserListCore if used with UsersBloc as an ancestor',
(tester) async {
const userListCoreKey = Key('userListCore');
final userListCore = UserListCore(
key: userListCoreKey,
listBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (Object error) => Offstage(),
);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: UsersBloc(
child: userListCore,
),
),
);
expect(find.byKey(userListCoreKey), findsOneWidget);
},
);
testWidgets(
'should assign loadData and paginateData callback to '
'UserListController if passed',
(tester) async {
const userListCoreKey = Key('userListCore');
final controller = UserListController();
final userListCore = UserListCore(
key: userListCoreKey,
listBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (Object error) => Offstage(),
userListController: controller,
);
expect(controller.loadData, isNull);
expect(controller.paginateData, isNull);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: UsersBloc(
child: userListCore,
),
),
);
expect(find.byKey(userListCoreKey), findsOneWidget);
expect(controller.loadData, isNotNull);
expect(controller.paginateData, isNotNull);
},
);
testWidgets(
'should build error widget if usersBlocState.usersStream emits error',
(tester) async {
const userListCoreKey = Key('userListCore');
const errorWidgetKey = Key('errorWidget');
final userListCore = UserListCore(
key: userListCoreKey,
listBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (Object error) => Container(key: errorWidgetKey),
);
final mockClient = MockClient();
const error = 'Error! Error! Error!';
when(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).thenThrow(error);
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: UsersBloc(
child: userListCore,
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(errorWidgetKey), findsOneWidget);
verify(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).called(1);
},
);
testWidgets(
'should build empty widget if usersBlocState.usersStream emits empty data',
(tester) async {
const userListCoreKey = Key('userListCore');
const emptyWidgetKey = Key('emptyWidget');
final userListCore = UserListCore(
key: userListCoreKey,
listBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Container(key: emptyWidgetKey),
errorBuilder: (Object error) => Offstage(),
);
final mockClient = MockClient();
const users = <User>[];
when(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).thenAnswer((_) async => QueryUsersResponse()..users = users);
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: UsersBloc(
child: userListCore,
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(emptyWidgetKey), findsOneWidget);
verify(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).called(1);
},
);
testWidgets(
'should build list widget if usersBlocState.usersStream emits some data',
(tester) async {
const userListCoreKey = Key('userListCore');
const listWidgetKey = Key('listWidget');
final userListCore = UserListCore(
key: userListCoreKey,
listBuilder: (_, __) => Container(key: listWidgetKey),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (Object error) => Offstage(),
);
final mockClient = MockClient();
final users = _generateUsers();
when(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).thenAnswer((_) async => QueryUsersResponse()..users = users);
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: UsersBloc(
child: userListCore,
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(listWidgetKey), findsOneWidget);
verify(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).called(1);
},
);
testWidgets(
'should build list widget with grouped data if groupAlphabetically is true',
(tester) async {
const userListCoreKey = Key('userListCore');
const listWidgetKey = Key('listWidget');
final userListCore = UserListCore(
key: userListCoreKey,
listBuilder: (_, items) => Container(
key: listWidgetKey,
child: ListView(
children: items.map((e) {
return Container(
key: Key(e.key),
child: e.when(
headerItem: (heading) => Text(heading),
userItem: (user) => Text(user.id),
),
);
}).toList(growable: false),
),
),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (Object error) => Offstage(),
groupAlphabetically: true,
);
final mockClient = MockClient();
final users = _generateUsers();
when(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).thenAnswer((_) async => QueryUsersResponse()..users = users);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StreamChatCore(
client: mockClient,
child: UsersBloc(
child: userListCore,
),
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(listWidgetKey), findsOneWidget);
for (final user in users) {
expect(find.byKey(Key('HEADER-${user.name[0]}')), findsOneWidget);
expect(find.byKey(Key('USER-${user.id}')), findsOneWidget);
}
verify(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).called(1);
},
);
testWidgets(
'should build list widget with paginated data '
'on calling channelListCoreState.paginateData',
(tester) async {
const userListCoreKey = Key('userListCore');
const listWidgetKey = Key('listWidget');
const pagination = PaginationParams();
final userListCore = UserListCore(
key: userListCoreKey,
listBuilder: (_, items) => Container(
key: listWidgetKey,
child: ListView(
children: items.map((e) {
return Container(
key: Key(e.key),
child: e.when(
headerItem: (heading) => Text(heading),
userItem: (user) => Text(user.id),
),
);
}).toList(growable: false),
),
),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (Object error) => Offstage(),
pagination: pagination,
groupAlphabetically: true,
);
final mockClient = MockClient();
final users = _generateUsers();
when(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).thenAnswer((_) async => QueryUsersResponse()..users = users);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StreamChatCore(
client: mockClient,
child: UsersBloc(
child: userListCore,
),
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(listWidgetKey), findsOneWidget);
for (final user in users) {
expect(find.byKey(Key('HEADER-${user.name[0]}')), findsOneWidget);
expect(find.byKey(Key('USER-${user.id}')), findsOneWidget);
}
verify(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).called(1);
final userListCoreState = tester.state<UserListCoreState>(
find.byKey(userListCoreKey),
);
final offset = users.length;
final paginatedUsers = _generateUsers(offset: offset);
final updatedPagination = pagination.copyWith(offset: offset);
when(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: updatedPagination,
)).thenAnswer((_) async => QueryUsersResponse()..users = paginatedUsers);
await userListCoreState.paginateData();
await tester.pumpAndSettle();
for (final user in users + paginatedUsers) {
expect(find.byKey(Key('HEADER-${user.name[0]}')), findsOneWidget);
expect(find.byKey(Key('USER-${user.id}')), findsOneWidget);
}
verify(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: updatedPagination,
)).called(1);
},
);
testWidgets(
'should rebuild UserListCore with updated widget data '
'on calling setState()',
(tester) async {
const pagination = PaginationParams();
StateSetter _stateSetter;
int limit = pagination.limit;
const userListCoreKey = Key('userListCore');
const listWidgetKey = Key('listWidget');
UserListCore userListCoreBuilder(int limit) => UserListCore(
key: userListCoreKey,
listBuilder: (_, items) => Container(
key: listWidgetKey,
child: ListView(
children: items.map((e) {
return Container(
key: Key(e.key),
child: e.when(
headerItem: (heading) => Text(heading),
userItem: (user) => Text(user.id),
),
);
}).toList(growable: false),
),
),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (Object error) => Offstage(),
pagination: pagination.copyWith(limit: limit),
groupAlphabetically: true,
);
final mockClient = MockClient();
final users = _generateUsers();
when(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).thenAnswer((_) async => QueryUsersResponse()..users = users);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StreamChatCore(
client: mockClient,
child: UsersBloc(
child: StatefulBuilder(builder: (context, stateSetter) {
// Assigning stateSetter for rebuilding UserListCore
_stateSetter = stateSetter;
return userListCoreBuilder(limit);
}),
),
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(listWidgetKey), findsOneWidget);
for (final user in users) {
expect(find.byKey(Key('HEADER-${user.name[0]}')), findsOneWidget);
expect(find.byKey(Key('USER-${user.id}')), findsOneWidget);
}
verify(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).called(1);
// Rebuilding UserListCore with new pagination limit
_stateSetter(() => limit = 6);
final updatedUsers = _generateUsers(count: limit);
final updatedPagination = pagination.copyWith(limit: limit);
when(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: updatedPagination,
)).thenAnswer((_) async => QueryUsersResponse()..users = updatedUsers);
await tester.pumpAndSettle();
for (final user in updatedUsers) {
expect(find.byKey(Key('HEADER-${user.name[0]}')), findsOneWidget);
expect(find.byKey(Key('USER-${user.id}')), findsOneWidget);
}
verify(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: updatedPagination,
)).called(1);
},
);
}
@@ -0,0 +1,327 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter_core/src/stream_chat_core.dart';
import 'package:stream_chat_flutter_core/src/users_bloc.dart';
import 'package:mockito/mockito.dart';
import 'matchers/users_matcher.dart';
import 'mocks.dart';
void main() {
List<User> _generateUsers({
int count = 3,
int offset = 0,
}) {
return List.generate(
count,
(index) {
index = index + offset;
return User(
id: 'testId$index',
role: 'testRole$index',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
lastActive: DateTime.now(),
online: true,
banned: false,
extraData: {'extra_data_key': 'extraDataValue'},
);
},
);
}
test(
'should throw assertion error if child is null',
() async {
const usersBlocKey = Key('usersBloc');
final usersBloc = () => UsersBloc(
key: usersBlocKey,
child: null,
);
expect(usersBloc, throwsA(isA<AssertionError>()));
},
);
testWidgets(
'usersBlocState.queryUsers() should throw if used where '
'StreamChat is not present in the widget tree',
(tester) async {
const usersBlocKey = Key('usersBloc');
const childKey = Key('child');
final usersBloc = UsersBloc(
key: usersBlocKey,
child: Offstage(key: childKey),
);
await tester.pumpWidget(usersBloc);
expect(find.byKey(usersBlocKey), findsOneWidget);
expect(find.byKey(childKey), findsOneWidget);
final usersBlocState = tester.state<UsersBlocState>(
find.byKey(usersBlocKey),
);
try {
await usersBlocState.queryUsers();
} catch (e) {
expect(e, isInstanceOf<Exception>());
}
},
);
testWidgets(
'usersBlocState.queryUsers() should emit data through usersStream',
(tester) async {
const usersBlocKey = Key('usersBloc');
const childKey = Key('child');
final usersBloc = UsersBloc(
key: usersBlocKey,
child: Offstage(key: childKey),
);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: usersBloc,
),
);
final usersBlocState = tester.state<UsersBlocState>(
find.byKey(usersBlocKey),
);
final users = _generateUsers();
when(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).thenAnswer((_) async => QueryUsersResponse()..users = users);
usersBlocState.queryUsers();
await expectLater(
usersBlocState.usersStream,
emits(isSameUserListAs(users)),
);
verify(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).called(1);
},
);
testWidgets(
'usersBlocState.usersStream should emit error '
'if client.queryUsers() throws',
(tester) async {
const usersBlocKey = Key('usersBloc');
const childKey = Key('child');
final usersBloc = UsersBloc(
key: usersBlocKey,
child: Offstage(key: childKey),
);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: usersBloc,
),
);
final usersBlocState = tester.state<UsersBlocState>(
find.byKey(usersBlocKey),
);
final error = 'Error! Error! Error!';
when(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).thenThrow(error);
usersBlocState.queryUsers();
await expectLater(
usersBlocState.usersStream,
emitsError(error),
);
verify(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).called(1);
},
);
testWidgets(
'calling usersBlocState.queryUsers() again with an offset '
'should emit new data through usersStream and also emit loading state '
'through queryUsersLoading',
(tester) async {
const usersBlocKey = Key('usersBloc');
const childKey = Key('child');
final usersBloc = UsersBloc(
key: usersBlocKey,
child: Offstage(key: childKey),
);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: usersBloc,
),
);
final usersBlocState = tester.state<UsersBlocState>(
find.byKey(usersBlocKey),
);
final users = _generateUsers();
when(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).thenAnswer((_) async => QueryUsersResponse()..users = users);
usersBlocState.queryUsers();
await expectLater(
usersBlocState.usersStream,
emits(isSameUserListAs(users)),
);
verify(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).called(1);
final offset = users.length;
final paginatedUsers = _generateUsers(offset: offset);
final pagination = PaginationParams(offset: offset);
when(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: pagination,
)).thenAnswer((_) async => QueryUsersResponse()..users = paginatedUsers);
usersBlocState.queryUsers(pagination: pagination);
await Future.wait([
expectLater(
usersBlocState.queryUsersLoading,
emitsInOrder([true, false]),
),
expectLater(
usersBlocState.usersStream,
emits(isSameUserListAs(users + paginatedUsers)),
),
]);
verify(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: pagination,
)).called(1);
},
);
testWidgets(
'calling usersBlocState.queryUsers() again with an offset '
'should emit error through queryUsersLoading if '
'client.queryUsers() throws',
(tester) async {
const usersBlocKey = Key('usersBloc');
const childKey = Key('child');
final usersBloc = UsersBloc(
key: usersBlocKey,
child: Offstage(key: childKey),
);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: usersBloc,
),
);
final usersBlocState = tester.state<UsersBlocState>(
find.byKey(usersBlocKey),
);
final users = _generateUsers();
when(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).thenAnswer((_) async => QueryUsersResponse()..users = users);
usersBlocState.queryUsers();
await expectLater(
usersBlocState.usersStream,
emits(isSameUserListAs(users)),
);
verify(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).called(1);
final offset = users.length;
final pagination = PaginationParams(offset: offset);
final error = 'Error! Error! Error!';
when(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: pagination,
)).thenThrow(error);
usersBlocState.queryUsers(pagination: pagination);
await expectLater(
usersBlocState.queryUsersLoading,
emitsError(error),
);
verify(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: pagination,
)).called(1);
},
);
}