test: add tests for stream chat flutter core (#354)
* test(core): add tests for channels bloc Signed-off-by: Sahil Kumar <[email protected]> * test(core): refactor some tests due to changes in channels bloc Signed-off-by: Sahil Kumar <[email protected]> * test(core): add tests for channel list core Signed-off-by: Sahil Kumar <[email protected]> * test(core): add tests for users bloc, minor improvements Signed-off-by: Sahil Kumar <[email protected]> * test(core): add tests for user list core, minor improvements Signed-off-by: Sahil Kumar <[email protected]> * test(core): add tests for message search bloc, refactor Signed-off-by: Sahil Kumar <[email protected]> * test(core): add tests for message search list core, refactor Signed-off-by: Sahil Kumar <[email protected]> * test(core): add tests for lazy load scroll view Signed-off-by: Sahil Kumar <[email protected]> * test(core): add tests for stream chat core Signed-off-by: Sahil Kumar <[email protected]> * test(core): add tests for message list core Signed-off-by: Sahil Kumar <[email protected]> * test(core): add tests for stream channel Signed-off-by: Sahil Kumar <[email protected]> * Fix merge conflicts Signed-off-by: Sahil Kumar <[email protected]> * flutter format Signed-off-by: Sahil Kumar <[email protected]> * fixes Signed-off-by: Sahil Kumar <[email protected]> * update workflow Signed-off-by: Sahil Kumar <[email protected]> * chore(core): fix analyzer issues Signed-off-by: Sahil Kumar <[email protected]> * chore(ui-kit): fix analyzer issues Signed-off-by: Sahil Kumar <[email protected]>
This commit is contained in:
@@ -59,7 +59,7 @@ jobs:
|
|||||||
|
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 5
|
timeout-minutes: 15
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
@@ -98,7 +98,7 @@ jobs:
|
|||||||
- uses: VeryGoodOpenSource/[email protected]
|
- uses: VeryGoodOpenSource/[email protected]
|
||||||
with:
|
with:
|
||||||
path: packages/stream_chat_flutter_core/coverage/lcov.info
|
path: packages/stream_chat_flutter_core/coverage/lcov.info
|
||||||
min_coverage: 4.5
|
min_coverage: 90
|
||||||
- uses: VeryGoodOpenSource/[email protected]
|
- uses: VeryGoodOpenSource/[email protected]
|
||||||
with:
|
with:
|
||||||
path: packages/stream_chat_flutter/coverage/lcov.info
|
path: packages/stream_chat_flutter/coverage/lcov.info
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ class Channel {
|
|||||||
|
|
||||||
/// Channel extra data
|
/// Channel extra data
|
||||||
Map<String, dynamic> get extraData =>
|
Map<String, dynamic> get extraData =>
|
||||||
state?._channelState?.channel?.extraData;
|
state?._channelState?.channel?.extraData ?? _extraData;
|
||||||
|
|
||||||
/// Channel extra data as a stream
|
/// Channel extra data as a stream
|
||||||
Stream<Map<String, dynamic>> get extraDataStream =>
|
Stream<Map<String, dynamic>> get extraDataStream =>
|
||||||
|
|||||||
@@ -104,4 +104,25 @@ class PaginationParams {
|
|||||||
lessThan: lessThan ?? this.lessThan,
|
lessThan: lessThan ?? this.lessThan,
|
||||||
lessThanOrEqual: lessThanOrEqual ?? this.lessThanOrEqual,
|
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(
|
return LazyLoadScrollView(
|
||||||
onEndOfPage: () => messageSearchBloc.loadMore(
|
onEndOfPage: () => messageSearchBloc.search(
|
||||||
filter: {
|
filter: {
|
||||||
'cid': {
|
'cid': {
|
||||||
r'$in': [StreamChannel.of(context).channel.cid]
|
r'$in': [StreamChannel.of(context).channel.cid]
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ class _ChannelMediaDisplayScreenState extends State<ChannelMediaDisplayScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return LazyLoadScrollView(
|
return LazyLoadScrollView(
|
||||||
onEndOfPage: () => messageSearchBloc.loadMore(
|
onEndOfPage: () => messageSearchBloc.search(
|
||||||
filter: {
|
filter: {
|
||||||
'cid': {
|
'cid': {
|
||||||
r'$in': [StreamChannel.of(context).channel.cid]
|
r'$in': [StreamChannel.of(context).channel.cid]
|
||||||
|
|||||||
@@ -262,9 +262,7 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
|
|||||||
);
|
);
|
||||||
if (widget.pullToRefresh) {
|
if (widget.pullToRefresh) {
|
||||||
child = RefreshIndicator(
|
child = RefreshIndicator(
|
||||||
onRefresh: () async {
|
onRefresh: () => _messageSearchListController.loadData(),
|
||||||
_messageSearchListController.loadData();
|
|
||||||
},
|
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -193,9 +193,7 @@ class _UserListViewState extends State<UserListView>
|
|||||||
return child;
|
return child;
|
||||||
} else {
|
} else {
|
||||||
return RefreshIndicator(
|
return RefreshIndicator(
|
||||||
onRefresh: () async {
|
onRefresh: () => _userListController.loadData(),
|
||||||
_userListController.loadData();
|
|
||||||
},
|
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,11 @@ dependencies:
|
|||||||
path_provider: ^1.6.27
|
path_provider: ^1.6.27
|
||||||
video_thumbnail: ^0.2.5+1
|
video_thumbnail: ^0.2.5+1
|
||||||
|
|
||||||
|
|
||||||
|
dependency_overrides:
|
||||||
|
stream_chat_flutter_core:
|
||||||
|
path: ../stream_chat_flutter_core
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
assets:
|
assets:
|
||||||
- images/
|
- images/
|
||||||
|
|||||||
@@ -130,10 +130,11 @@ class ChannelListCore extends StatefulWidget {
|
|||||||
final PaginationParams pagination;
|
final PaginationParams pagination;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_ChannelListCoreState createState() => _ChannelListCoreState();
|
ChannelListCoreState createState() => ChannelListCoreState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ChannelListCoreState extends State<ChannelListCore> {
|
/// The current state of the [ChannelListCore].
|
||||||
|
class ChannelListCoreState extends State<ChannelListCore> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final channelsBloc = ChannelsBloc.of(context);
|
final channelsBloc = ChannelsBloc.of(context);
|
||||||
@@ -148,7 +149,7 @@ class _ChannelListCoreState extends State<ChannelListCore> {
|
|||||||
stream: channelsBlocState.channelsStream,
|
stream: channelsBlocState.channelsStream,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.hasError) {
|
if (snapshot.hasError) {
|
||||||
return _buildErrorWidget(snapshot, context, channelsBlocState);
|
return widget.errorBuilder(context, snapshot.error);
|
||||||
}
|
}
|
||||||
if (!snapshot.hasData) {
|
if (!snapshot.hasData) {
|
||||||
return widget.loadingBuilder(context);
|
return widget.loadingBuilder(context);
|
||||||
@@ -161,18 +162,7 @@ class _ChannelListCoreState extends State<ChannelListCore> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildErrorWidget(
|
/// Fetches initial channels and updates the widget
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> loadData() {
|
Future<void> loadData() {
|
||||||
final channelsBloc = ChannelsBloc.of(context);
|
final channelsBloc = ChannelsBloc.of(context);
|
||||||
return channelsBloc.queryChannels(
|
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() {
|
Future<void> paginateData() {
|
||||||
final channelsBloc = ChannelsBloc.of(context);
|
final channelsBloc = ChannelsBloc.of(context);
|
||||||
return channelsBloc.queryChannels(
|
return channelsBloc.queryChannels(
|
||||||
@@ -223,9 +214,9 @@ class _ChannelListCoreState extends State<ChannelListCore> {
|
|||||||
|
|
||||||
if (widget.filter?.toString() != oldWidget.filter?.toString() ||
|
if (widget.filter?.toString() != oldWidget.filter?.toString() ||
|
||||||
jsonEncode(widget.sort) != jsonEncode(oldWidget.sort) ||
|
jsonEncode(widget.sort) != jsonEncode(oldWidget.sort) ||
|
||||||
|
widget.options?.toString() != oldWidget.options?.toString() ||
|
||||||
widget.pagination?.toJson()?.toString() !=
|
widget.pagination?.toJson()?.toString() !=
|
||||||
oldWidget.pagination?.toJson()?.toString() ||
|
oldWidget.pagination?.toJson()?.toString()) {
|
||||||
widget.options?.toString() != oldWidget.options?.toString()) {
|
|
||||||
loadData();
|
loadData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,10 +74,9 @@ class ChannelsBlocState extends State<ChannelsBloc>
|
|||||||
/// The current channel list as a stream
|
/// The current channel list as a stream
|
||||||
Stream<List<Channel>> get channelsStream => _channelsController.stream;
|
Stream<List<Channel>> get channelsStream => _channelsController.stream;
|
||||||
|
|
||||||
final BehaviorSubject<bool> _queryChannelsLoadingController =
|
final _queryChannelsLoadingController = BehaviorSubject.seeded(false);
|
||||||
BehaviorSubject.seeded(false);
|
|
||||||
|
|
||||||
final BehaviorSubject<List<Channel>> _channelsController = BehaviorSubject();
|
final _channelsController = BehaviorSubject<List<Channel>>();
|
||||||
|
|
||||||
/// The stream notifying the state of queryChannel call
|
/// The stream notifying the state of queryChannel call
|
||||||
Stream<bool> get queryChannelsLoading =>
|
Stream<bool> get queryChannelsLoading =>
|
||||||
@@ -91,15 +90,14 @@ class ChannelsBlocState extends State<ChannelsBloc>
|
|||||||
List<SortOption<ChannelModel>> sortOptions,
|
List<SortOption<ChannelModel>> sortOptions,
|
||||||
PaginationParams paginationParams,
|
PaginationParams paginationParams,
|
||||||
Map<String, dynamic> options,
|
Map<String, dynamic> options,
|
||||||
bool onlyOffline = false,
|
|
||||||
}) async {
|
}) async {
|
||||||
final client = StreamChatCore.of(context).client;
|
final client = StreamChatCore.of(context).client;
|
||||||
|
|
||||||
if (client.state?.user == null ||
|
if (_queryChannelsLoadingController.value == true) return;
|
||||||
_queryChannelsLoadingController.value == true) {
|
|
||||||
return;
|
if (_channelsController.hasValue) {
|
||||||
|
_queryChannelsLoadingController.add(true);
|
||||||
}
|
}
|
||||||
_queryChannelsLoadingController.sink.add(true);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final clear = paginationParams == null ||
|
final clear = paginationParams == null ||
|
||||||
@@ -115,15 +113,20 @@ class ChannelsBlocState extends State<ChannelsBloc>
|
|||||||
if (clear) {
|
if (clear) {
|
||||||
_channelsController.add(channels);
|
_channelsController.add(channels);
|
||||||
} else {
|
} else {
|
||||||
final l = oldChannels + channels;
|
final temp = oldChannels + channels;
|
||||||
_channelsController.add(l);
|
_channelsController.add(temp);
|
||||||
|
}
|
||||||
|
if (_channelsController.hasValue &&
|
||||||
|
_queryChannelsLoadingController.value) {
|
||||||
|
_queryChannelsLoadingController.sink.add(false);
|
||||||
}
|
}
|
||||||
_queryChannelsLoadingController.sink.add(false);
|
|
||||||
}
|
}
|
||||||
} catch (err, stackTrace) {
|
} catch (e, stk) {
|
||||||
print(err);
|
if (_channelsController.hasValue) {
|
||||||
print(stackTrace);
|
_queryChannelsLoadingController.addError(e, stk);
|
||||||
_queryChannelsLoadingController.addError(err, stackTrace);
|
} else {
|
||||||
|
_channelsController.addError(e, stk);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,15 +142,14 @@ class ChannelsBlocState extends State<ChannelsBloc>
|
|||||||
_subscriptions.add(client.on(EventType.messageNew).listen((e) {
|
_subscriptions.add(client.on(EventType.messageNew).listen((e) {
|
||||||
final newChannels = List<Channel>.from(channels ?? []);
|
final newChannels = List<Channel>.from(channels ?? []);
|
||||||
final index = newChannels.indexWhere((c) => c.cid == e.cid);
|
final index = newChannels.indexWhere((c) => c.cid == e.cid);
|
||||||
if (index > -1) {
|
if (index != -1) {
|
||||||
if (index > 0) {
|
if (index > 0) {
|
||||||
final channel = newChannels.removeAt(index);
|
final channel = newChannels.removeAt(index);
|
||||||
newChannels.insert(0, channel);
|
newChannels.insert(0, channel);
|
||||||
}
|
}
|
||||||
} else if (widget.shouldAddChannel != null &&
|
} else if (widget.shouldAddChannel?.call(e) == true) {
|
||||||
widget.shouldAddChannel(e)) {
|
|
||||||
final hiddenIndex = _hiddenChannels.indexWhere((c) => c.cid == e.cid);
|
final hiddenIndex = _hiddenChannels.indexWhere((c) => c.cid == e.cid);
|
||||||
if (hiddenIndex > -1) {
|
if (hiddenIndex != -1) {
|
||||||
newChannels.insert(0, _hiddenChannels[hiddenIndex]);
|
newChannels.insert(0, _hiddenChannels[hiddenIndex]);
|
||||||
_hiddenChannels.removeAt(hiddenIndex);
|
_hiddenChannels.removeAt(hiddenIndex);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/widgets.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
|
/// Wrapper around a [Scrollable] which triggers [onEndOfPage]/[onStartOfPage] the Scrollable
|
||||||
/// reaches to the start or end of the view extent.
|
/// reaches to the start or end of the view extent.
|
||||||
@@ -47,16 +46,16 @@ class LazyLoadScrollView extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _LazyLoadScrollViewState extends State<LazyLoadScrollView> {
|
class _LazyLoadScrollViewState extends State<LazyLoadScrollView> {
|
||||||
_LoadingStatus _loadMoreStatus = _LoadingStatus.STABLE;
|
_LoadingStatus _loadMoreStatus = _LoadingStatus.stable;
|
||||||
double _scrollPosition = 0;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => NotificationListener(
|
Widget build(BuildContext context) =>
|
||||||
|
NotificationListener<ScrollNotification>(
|
||||||
onNotification: _onNotification,
|
onNotification: _onNotification,
|
||||||
child: widget.child,
|
child: widget.child,
|
||||||
);
|
);
|
||||||
|
|
||||||
bool _onNotification(Notification notification) {
|
bool _onNotification(ScrollNotification notification) {
|
||||||
if (notification is ScrollStartNotification) {
|
if (notification is ScrollStartNotification) {
|
||||||
if (widget.onPageScrollStart != null) {
|
if (widget.onPageScrollStart != null) {
|
||||||
widget.onPageScrollStart();
|
widget.onPageScrollStart();
|
||||||
@@ -73,7 +72,7 @@ class _LazyLoadScrollViewState extends State<LazyLoadScrollView> {
|
|||||||
final pixels = notification.metrics.pixels;
|
final pixels = notification.metrics.pixels;
|
||||||
final maxScrollExtent = notification.metrics.maxScrollExtent;
|
final maxScrollExtent = notification.metrics.maxScrollExtent;
|
||||||
final minScrollExtent = notification.metrics.minScrollExtent;
|
final minScrollExtent = notification.metrics.minScrollExtent;
|
||||||
final scrollOffset = widget.scrollOffset;
|
final scrollOffset = widget.scrollOffset ?? 0;
|
||||||
|
|
||||||
if (pixels > (minScrollExtent + scrollOffset) &&
|
if (pixels > (minScrollExtent + scrollOffset) &&
|
||||||
pixels < (maxScrollExtent - scrollOffset)) {
|
pixels < (maxScrollExtent - scrollOffset)) {
|
||||||
@@ -85,28 +84,15 @@ class _LazyLoadScrollViewState extends State<LazyLoadScrollView> {
|
|||||||
|
|
||||||
final extentBefore = notification.metrics.extentBefore;
|
final extentBefore = notification.metrics.extentBefore;
|
||||||
final extentAfter = notification.metrics.extentAfter;
|
final extentAfter = notification.metrics.extentAfter;
|
||||||
final scrollingDown = _scrollPosition < pixels;
|
|
||||||
|
|
||||||
if (scrollOffset == null || scrollOffset == 0) {
|
if (extentAfter <= scrollOffset) {
|
||||||
if (extentAfter == 0) {
|
_onEndOfPage();
|
||||||
_onEndOfPage();
|
return true;
|
||||||
}
|
}
|
||||||
if (extentBefore == 0) {
|
if (extentBefore <= scrollOffset) {
|
||||||
_onStartOfPage();
|
_onStartOfPage();
|
||||||
}
|
return true;
|
||||||
} else {
|
|
||||||
if (scrollingDown) {
|
|
||||||
if (extentAfter <= scrollOffset) {
|
|
||||||
_onEndOfPage();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (extentBefore <= scrollOffset) {
|
|
||||||
_onStartOfPage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_scrollPosition = pixels;
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
if (notification is OverscrollNotification) {
|
if (notification is OverscrollNotification) {
|
||||||
if (notification.overscroll > 0) {
|
if (notification.overscroll > 0) {
|
||||||
@@ -121,22 +107,22 @@ class _LazyLoadScrollViewState extends State<LazyLoadScrollView> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _onEndOfPage() {
|
void _onEndOfPage() {
|
||||||
if (_loadMoreStatus != null && _loadMoreStatus == _LoadingStatus.STABLE) {
|
if (_loadMoreStatus != null && _loadMoreStatus == _LoadingStatus.stable) {
|
||||||
_loadMoreStatus = _LoadingStatus.LOADING;
|
_loadMoreStatus = _LoadingStatus.loading;
|
||||||
if (widget.onEndOfPage != null) {
|
if (widget.onEndOfPage != null) {
|
||||||
widget.onEndOfPage().whenComplete(() {
|
widget.onEndOfPage().whenComplete(() {
|
||||||
_loadMoreStatus = _LoadingStatus.STABLE;
|
_loadMoreStatus = _LoadingStatus.stable;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onStartOfPage() {
|
void _onStartOfPage() {
|
||||||
if (_loadMoreStatus != null && _loadMoreStatus == _LoadingStatus.STABLE) {
|
if (_loadMoreStatus != null && _loadMoreStatus == _LoadingStatus.stable) {
|
||||||
_loadMoreStatus = _LoadingStatus.LOADING;
|
_loadMoreStatus = _LoadingStatus.loading;
|
||||||
if (widget.onStartOfPage != null) {
|
if (widget.onStartOfPage != null) {
|
||||||
widget.onStartOfPage().whenComplete(() {
|
widget.onStartOfPage().whenComplete(() {
|
||||||
_loadMoreStatus = _LoadingStatus.STABLE;
|
_loadMoreStatus = _LoadingStatus.stable;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,28 +114,28 @@ class MessageListCore extends StatefulWidget {
|
|||||||
final bool Function(Message) messageFilter;
|
final bool Function(Message) messageFilter;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_MessageListCoreState createState() => _MessageListCoreState();
|
MessageListCoreState createState() => MessageListCoreState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MessageListCoreState extends State<MessageListCore> {
|
/// The current state of the [MessageListCore].
|
||||||
StreamChannelState streamChannel;
|
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;
|
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>[];
|
var _messages = <Message>[];
|
||||||
bool initialMessageHighlightComplete = false;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final messagesStream = _isThreadConversation
|
final messagesStream = _isThreadConversation
|
||||||
? streamChannel.channel.state.threadsStream
|
? _streamChannel.channel.state.threadsStream
|
||||||
.where((threads) => threads.containsKey(widget.parentMessage.id))
|
.where((threads) => threads.containsKey(widget.parentMessage.id))
|
||||||
.map((threads) => threads[widget.parentMessage.id])
|
.map((threads) => threads[widget.parentMessage.id])
|
||||||
: streamChannel.channel.state?.messagesStream;
|
: _streamChannel.channel.state?.messagesStream;
|
||||||
|
|
||||||
bool defaultFilter(Message m) {
|
bool defaultFilter(Message m) {
|
||||||
final isMyMessage = m.user.id == _currentUser.id;
|
final isMyMessage = m.user.id == _currentUser.id;
|
||||||
@@ -148,10 +148,10 @@ class _MessageListCoreState extends State<MessageListCore> {
|
|||||||
stream: messagesStream?.map((messages) =>
|
stream: messagesStream?.map((messages) =>
|
||||||
messages?.where(widget.messageFilter ?? defaultFilter)?.toList()),
|
messages?.where(widget.messageFilter ?? defaultFilter)?.toList()),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (!snapshot.hasData) {
|
if (snapshot.hasError) {
|
||||||
return widget.loadingBuilder(context);
|
|
||||||
} else if (snapshot.hasError) {
|
|
||||||
return widget.errorWidgetBuilder(context, snapshot.error);
|
return widget.errorWidgetBuilder(context, snapshot.error);
|
||||||
|
} else if (!snapshot.hasData) {
|
||||||
|
return widget.loadingBuilder(context);
|
||||||
} else {
|
} else {
|
||||||
final messageList = snapshot.data?.reversed?.toList() ?? [];
|
final messageList = snapshot.data?.reversed?.toList() ?? [];
|
||||||
if (messageList.isEmpty && !_isThreadConversation) {
|
if (messageList.isEmpty && !_isThreadConversation) {
|
||||||
@@ -159,29 +159,32 @@ class _MessageListCoreState extends State<MessageListCore> {
|
|||||||
return widget.emptyBuilder(context);
|
return widget.emptyBuilder(context);
|
||||||
}
|
}
|
||||||
} else {
|
} 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(
|
Future<void> paginateData(
|
||||||
{QueryDirection direction = QueryDirection.bottom}) {
|
{QueryDirection direction = QueryDirection.bottom}) {
|
||||||
if (!_isThreadConversation) {
|
if (!_isThreadConversation) {
|
||||||
return streamChannel.queryMessages(direction: direction);
|
return _streamChannel.queryMessages(direction: direction);
|
||||||
} else {
|
} else {
|
||||||
return streamChannel.getReplies(widget.parentMessage.id);
|
return _streamChannel.getReplies(widget.parentMessage.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
streamChannel = StreamChannel.of(context);
|
_streamChannel = StreamChannel.of(context);
|
||||||
|
|
||||||
if (_isThreadConversation) {
|
if (_isThreadConversation) {
|
||||||
streamChannel.getReplies(widget.parentMessage.id);
|
_streamChannel.getReplies(widget.parentMessage.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (widget.messageListController != null) {
|
if (widget.messageListController != null) {
|
||||||
@@ -194,7 +197,7 @@ class _MessageListCoreState extends State<MessageListCore> {
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
if (!_upToDate) {
|
if (!_upToDate) {
|
||||||
streamChannel.reloadChannel();
|
_streamChannel.reloadChannel();
|
||||||
}
|
}
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
@@ -203,5 +206,5 @@ class _MessageListCoreState extends State<MessageListCore> {
|
|||||||
/// Controller used for paginating data in [ChannelListView]
|
/// Controller used for paginating data in [ChannelListView]
|
||||||
class MessageListController {
|
class MessageListController {
|
||||||
/// Call this function to load further data
|
/// 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 =>
|
Stream<List<GetMessageResponse>> get messagesStream =>
|
||||||
_messageResponses.stream;
|
_messageResponses.stream;
|
||||||
|
|
||||||
final BehaviorSubject<List<GetMessageResponse>> _messageResponses =
|
final _messageResponses = BehaviorSubject<List<GetMessageResponse>>();
|
||||||
BehaviorSubject();
|
|
||||||
|
|
||||||
final BehaviorSubject<bool> _queryMessagesLoadingController =
|
final _queryMessagesLoadingController = BehaviorSubject.seeded(false);
|
||||||
BehaviorSubject.seeded(false);
|
|
||||||
|
|
||||||
/// The stream notifying the state of queryUsers call
|
/// The stream notifying the state of queryUsers call
|
||||||
Stream<bool> get queryMessagesLoading =>
|
Stream<bool> get queryMessagesLoading =>
|
||||||
_queryMessagesLoadingController.stream;
|
_queryMessagesLoadingController.stream;
|
||||||
|
|
||||||
/// Calls [StreamChatClient.search] updating [messageResponses] stream
|
/// Calls [StreamChatClient.search] updating
|
||||||
|
/// [messagesStream] and [queryMessagesLoading] stream
|
||||||
Future<void> search({
|
Future<void> search({
|
||||||
Map<String, dynamic> filter,
|
Map<String, dynamic> filter,
|
||||||
Map<String, dynamic> messageFilter,
|
Map<String, dynamic> messageFilter,
|
||||||
@@ -66,33 +65,13 @@ class MessageSearchBlocState extends State<MessageSearchBloc>
|
|||||||
String query,
|
String query,
|
||||||
PaginationParams pagination,
|
PaginationParams pagination,
|
||||||
}) async {
|
}) async {
|
||||||
_messageResponses.add(null);
|
final client = StreamChatCore.of(context).client;
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calls [StreamChatClient.search] updating [queryMessagesLoading] stream
|
if (_queryMessagesLoadingController.value == true) return;
|
||||||
Future<void> loadMore({
|
|
||||||
Map<String, dynamic> filter,
|
if (_messageResponses.hasValue) {
|
||||||
Map<String, dynamic> messageFilter,
|
_queryMessagesLoadingController.add(true);
|
||||||
List<SortOption> sort,
|
|
||||||
String query,
|
|
||||||
PaginationParams pagination,
|
|
||||||
}) async {
|
|
||||||
if (_queryMessagesLoadingController.value == true) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
_queryMessagesLoadingController.add(true);
|
|
||||||
try {
|
try {
|
||||||
final clear = pagination == null ||
|
final clear = pagination == null ||
|
||||||
pagination.offset == null ||
|
pagination.offset == null ||
|
||||||
@@ -100,12 +79,12 @@ class MessageSearchBlocState extends State<MessageSearchBloc>
|
|||||||
|
|
||||||
final oldMessages = List<GetMessageResponse>.from(messageResponses ?? []);
|
final oldMessages = List<GetMessageResponse>.from(messageResponses ?? []);
|
||||||
|
|
||||||
final messages = await _search(
|
final messages = await client.search(
|
||||||
filter: filter,
|
filter,
|
||||||
messageFilter: messageFilter,
|
|
||||||
sort: sort,
|
sort: sort,
|
||||||
query: query,
|
query: query,
|
||||||
pagination: pagination,
|
paginationParams: pagination,
|
||||||
|
messageFilters: messageFilter,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (clear) {
|
if (clear) {
|
||||||
@@ -114,30 +93,18 @@ class MessageSearchBlocState extends State<MessageSearchBloc>
|
|||||||
final temp = oldMessages + messages.results;
|
final temp = oldMessages + messages.results;
|
||||||
_messageResponses.add(temp);
|
_messageResponses.add(temp);
|
||||||
}
|
}
|
||||||
|
if (_messageResponses.hasValue && _queryMessagesLoadingController.value) {
|
||||||
_queryMessagesLoadingController.add(false);
|
_queryMessagesLoadingController.add(false);
|
||||||
} catch (err, stackTrace) {
|
}
|
||||||
_queryMessagesLoadingController.addError(err, stackTrace);
|
} 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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
super.build(context);
|
super.build(context);
|
||||||
|
|||||||
@@ -104,21 +104,15 @@ class MessageSearchListCore extends StatefulWidget {
|
|||||||
final WidgetBuilder loadingBuilder;
|
final WidgetBuilder loadingBuilder;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_MessageSearchListCoreState createState() => _MessageSearchListCoreState();
|
MessageSearchListCoreState createState() => MessageSearchListCoreState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MessageSearchListCoreState extends State<MessageSearchListCore> {
|
/// The current state of the [MessageSearchListCore].
|
||||||
|
class MessageSearchListCoreState extends State<MessageSearchListCore> {
|
||||||
@override
|
@override
|
||||||
void didChangeDependencies() {
|
void didChangeDependencies() {
|
||||||
super.didChangeDependencies();
|
super.didChangeDependencies();
|
||||||
MessageSearchBloc.of(context).search(
|
loadData();
|
||||||
filter: widget.filters,
|
|
||||||
sort: widget.sortOptions,
|
|
||||||
query: widget.messageQuery,
|
|
||||||
pagination: widget.paginationParams,
|
|
||||||
messageFilter: widget.messageFilters,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (widget.messageSearchListController != null) {
|
if (widget.messageSearchListController != null) {
|
||||||
widget.messageSearchListController.loadData = loadData;
|
widget.messageSearchListController.loadData = loadData;
|
||||||
widget.messageSearchListController.paginateData = paginateData;
|
widget.messageSearchListController.paginateData = paginateData;
|
||||||
@@ -136,29 +130,23 @@ class _MessageSearchListCoreState extends State<MessageSearchListCore> {
|
|||||||
stream: messageSearchBloc.messagesStream,
|
stream: messageSearchBloc.messagesStream,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.hasError) {
|
if (snapshot.hasError) {
|
||||||
if (snapshot.error is Error) {
|
|
||||||
print((snapshot.error as Error).stackTrace);
|
|
||||||
}
|
|
||||||
|
|
||||||
return widget.errorBuilder(context, snapshot.error);
|
return widget.errorBuilder(context, snapshot.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!snapshot.hasData) {
|
if (!snapshot.hasData) {
|
||||||
return widget.loadingBuilder(context);
|
return widget.loadingBuilder(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
final items = snapshot.data;
|
final items = snapshot.data;
|
||||||
|
|
||||||
if (items.isEmpty) {
|
if (items.isEmpty) {
|
||||||
return widget.emptyBuilder(context);
|
return widget.emptyBuilder(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
return widget.childBuilder(snapshot.data);
|
return widget.childBuilder(snapshot.data);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
void loadData() {
|
/// Fetches initial messages and updates the widget
|
||||||
MessageSearchBloc.of(context).search(
|
Future<void> loadData() {
|
||||||
|
final messageSearchBloc = MessageSearchBloc.of(context);
|
||||||
|
return messageSearchBloc.search(
|
||||||
filter: widget.filters,
|
filter: widget.filters,
|
||||||
sort: widget.sortOptions,
|
sort: widget.sortOptions,
|
||||||
query: widget.messageQuery,
|
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);
|
final messageSearchBloc = MessageSearchBloc.of(context);
|
||||||
|
return messageSearchBloc.search(
|
||||||
messageSearchBloc.loadMore(
|
|
||||||
filter: widget.filters,
|
filter: widget.filters,
|
||||||
sort: widget.sortOptions,
|
sort: widget.sortOptions,
|
||||||
pagination: widget.paginationParams.copyWith(
|
pagination: widget.paginationParams.copyWith(
|
||||||
@@ -186,18 +174,12 @@ class _MessageSearchListCoreState extends State<MessageSearchListCore> {
|
|||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
if (widget.filters?.toString() != oldWidget.filters?.toString() ||
|
if (widget.filters?.toString() != oldWidget.filters?.toString() ||
|
||||||
jsonEncode(widget.sortOptions) != jsonEncode(oldWidget.sortOptions) ||
|
jsonEncode(widget.sortOptions) != jsonEncode(oldWidget.sortOptions) ||
|
||||||
widget.paginationParams?.toJson()?.toString() !=
|
|
||||||
oldWidget.paginationParams?.toJson()?.toString() ||
|
|
||||||
widget.messageQuery?.toString() != oldWidget.messageQuery?.toString() ||
|
widget.messageQuery?.toString() != oldWidget.messageQuery?.toString() ||
|
||||||
widget.messageFilters?.toString() !=
|
widget.messageFilters?.toString() !=
|
||||||
oldWidget.messageFilters?.toString()) {
|
oldWidget.messageFilters?.toString() ||
|
||||||
MessageSearchBloc.of(context).search(
|
widget.paginationParams?.toJson()?.toString() !=
|
||||||
filter: widget.filters,
|
oldWidget.paginationParams?.toJson()?.toString()) {
|
||||||
sort: widget.sortOptions,
|
loadData();
|
||||||
query: widget.messageQuery,
|
|
||||||
pagination: widget.paginationParams,
|
|
||||||
messageFilter: widget.messageFilters,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -205,8 +187,8 @@ class _MessageSearchListCoreState extends State<MessageSearchListCore> {
|
|||||||
/// Controller used for paginating data in [ChannelListView]
|
/// Controller used for paginating data in [ChannelListView]
|
||||||
class MessageSearchListController {
|
class MessageSearchListController {
|
||||||
/// Call this function to reload data
|
/// Call this function to reload data
|
||||||
VoidCallback loadData;
|
AsyncCallback loadData;
|
||||||
|
|
||||||
/// Call this function to load further data
|
/// Call this function to load further data
|
||||||
VoidCallback paginateData;
|
AsyncCallback paginateData;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -207,15 +207,14 @@ class StreamChannelState extends State<StreamChannel> {
|
|||||||
int after = 20,
|
int after = 20,
|
||||||
bool preferOffline = false,
|
bool preferOffline = false,
|
||||||
}) =>
|
}) =>
|
||||||
queryAtMessage(
|
_queryAtMessage(
|
||||||
messageId: messageId,
|
messageId: messageId,
|
||||||
before: before,
|
before: before,
|
||||||
after: after,
|
after: after,
|
||||||
preferOffline: preferOffline,
|
preferOffline: preferOffline,
|
||||||
);
|
);
|
||||||
|
|
||||||
///
|
Future<void> _queryAtMessage({
|
||||||
Future<void> queryAtMessage({
|
|
||||||
String messageId,
|
String messageId,
|
||||||
int before = 20,
|
int before = 20,
|
||||||
int after = 20,
|
int after = 20,
|
||||||
@@ -297,7 +296,7 @@ class StreamChannelState extends State<StreamChannel> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Reloads the channel with latest message
|
/// Reloads the channel with latest message
|
||||||
Future<void> reloadChannel() => queryAtMessage(before: 30);
|
Future<void> reloadChannel() => _queryAtMessage(before: 30);
|
||||||
|
|
||||||
List<Future<bool>> _futures;
|
List<Future<bool>> _futures;
|
||||||
|
|
||||||
@@ -305,8 +304,7 @@ class StreamChannelState extends State<StreamChannel> {
|
|||||||
try {
|
try {
|
||||||
await loadChannelAtMessage(initialMessageId);
|
await loadChannelAtMessage(initialMessageId);
|
||||||
return true;
|
return true;
|
||||||
} catch (e, stk) {
|
} catch (_) {
|
||||||
print('Error: $e\nStack: $stk');
|
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -349,9 +347,6 @@ class StreamChannelState extends State<StreamChannel> {
|
|||||||
],
|
],
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.hasError) {
|
if (snapshot.hasError) {
|
||||||
if (snapshot.error is Error) {
|
|
||||||
print((snapshot.error as Error).stackTrace);
|
|
||||||
}
|
|
||||||
var message = snapshot.error.toString();
|
var message = snapshot.error.toString();
|
||||||
if (snapshot.error is DioError) {
|
if (snapshot.error is DioError) {
|
||||||
final dioError = snapshot.error as DioError;
|
final dioError = snapshot.error as DioError;
|
||||||
@@ -361,9 +356,7 @@ class StreamChannelState extends State<StreamChannel> {
|
|||||||
message = 'Check your connection and retry';
|
message = 'Check your connection and retry';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Center(
|
return Center(child: Text(message));
|
||||||
child: Text(message),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
final initialized = snapshot.data[0];
|
final initialized = snapshot.data[0];
|
||||||
// ignore: avoid_bool_literals_in_conditional_expressions
|
// ignore: avoid_bool_literals_in_conditional_expressions
|
||||||
|
|||||||
@@ -93,10 +93,10 @@ class StreamChatCoreState extends State<StreamChatCore>
|
|||||||
Widget build(BuildContext context) => widget.child;
|
Widget build(BuildContext context) => widget.child;
|
||||||
|
|
||||||
/// The current user
|
/// The current user
|
||||||
User get user => widget.client.state.user;
|
User get user => client.state?.user;
|
||||||
|
|
||||||
/// The current user as a stream
|
/// The current user as a stream
|
||||||
Stream<User> get userStream => widget.client.state.userStream;
|
Stream<User> get userStream => client.state?.userStream;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -108,21 +108,25 @@ class StreamChatCoreState extends State<StreamChatCore>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
if (client.state?.user != null) {
|
if (user != null) {
|
||||||
if (state == AppLifecycleState.paused) {
|
if (state == AppLifecycleState.paused) {
|
||||||
if (widget.onBackgroundEventReceived != null) {
|
if (widget.onBackgroundEventReceived == null) {
|
||||||
_eventSubscription =
|
client.disconnect();
|
||||||
client.on().listen(widget.onBackgroundEventReceived);
|
return;
|
||||||
_disconnectTimer = Timer(
|
}
|
||||||
widget.backgroundKeepAlive,
|
_eventSubscription = client.on().listen(
|
||||||
client.disconnect,
|
widget.onBackgroundEventReceived,
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
|
void onTimerComplete() {
|
||||||
|
_eventSubscription.cancel();
|
||||||
client.disconnect();
|
client.disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_disconnectTimer = Timer(widget.backgroundKeepAlive, onTimerComplete);
|
||||||
} else if (state == AppLifecycleState.resumed) {
|
} else if (state == AppLifecycleState.resumed) {
|
||||||
_eventSubscription?.cancel();
|
|
||||||
if (_disconnectTimer?.isActive == true) {
|
if (_disconnectTimer?.isActive == true) {
|
||||||
|
_eventSubscription.cancel();
|
||||||
_disconnectTimer.cancel();
|
_disconnectTimer.cancel();
|
||||||
} else {
|
} else {
|
||||||
if (client.wsConnectionStatus == ConnectionStatus.disconnected) {
|
if (client.wsConnectionStatus == ConnectionStatus.disconnected) {
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ class UserListCore extends StatefulWidget {
|
|||||||
final UserListController userListController;
|
final UserListController userListController;
|
||||||
|
|
||||||
/// The builder that will be used in case of error
|
/// 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
|
/// The builder that will be used to build the list
|
||||||
final Widget Function(BuildContext context, List<ListItem> users) listBuilder;
|
final Widget Function(BuildContext context, List<ListItem> users) listBuilder;
|
||||||
@@ -120,21 +120,16 @@ class UserListCore extends StatefulWidget {
|
|||||||
final bool groupAlphabetically;
|
final bool groupAlphabetically;
|
||||||
|
|
||||||
@override
|
@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 {
|
with WidgetsBindingObserver {
|
||||||
@override
|
@override
|
||||||
void didChangeDependencies() {
|
void didChangeDependencies() {
|
||||||
super.didChangeDependencies();
|
super.didChangeDependencies();
|
||||||
UsersBloc.of(context).queryUsers(
|
loadData();
|
||||||
filter: widget.filter,
|
|
||||||
sort: widget.sort,
|
|
||||||
pagination: widget.pagination,
|
|
||||||
options: widget.options,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (widget.userListController != null) {
|
if (widget.userListController != null) {
|
||||||
widget.userListController.loadData = loadData;
|
widget.userListController.loadData = loadData;
|
||||||
widget.userListController.paginateData = paginateData;
|
widget.userListController.paginateData = paginateData;
|
||||||
@@ -144,11 +139,10 @@ class _UserListCoreState extends State<UserListCore>
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final _usersBloc = UsersBloc.of(context);
|
final _usersBloc = UsersBloc.of(context);
|
||||||
|
|
||||||
return _buildListView(_usersBloc);
|
return _buildListView(_usersBloc);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get isListAlreadySorted =>
|
bool get _isListAlreadySorted =>
|
||||||
widget.sort?.any((e) => e.field == 'name' && e.direction == 1) ?? false;
|
widget.sort?.any((e) => e.field == 'name' && e.direction == 1) ?? false;
|
||||||
|
|
||||||
Stream<List<ListItem>> _buildUserStream(
|
Stream<List<ListItem>> _buildUserStream(
|
||||||
@@ -158,7 +152,7 @@ class _UserListCoreState extends State<UserListCore>
|
|||||||
(users) {
|
(users) {
|
||||||
if (widget.groupAlphabetically) {
|
if (widget.groupAlphabetically) {
|
||||||
var temp = users;
|
var temp = users;
|
||||||
if (!isListAlreadySorted) {
|
if (!_isListAlreadySorted) {
|
||||||
temp = users
|
temp = users
|
||||||
..sort((curr, next) => curr.name.compareTo(next.name));
|
..sort((curr, next) => curr.name.compareTo(next.name));
|
||||||
}
|
}
|
||||||
@@ -186,33 +180,23 @@ class _UserListCoreState extends State<UserListCore>
|
|||||||
stream: _buildUserStream(usersBlocState),
|
stream: _buildUserStream(usersBlocState),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.hasError) {
|
if (snapshot.hasError) {
|
||||||
if (snapshot.error is Error) {
|
|
||||||
print((snapshot.error as Error).stackTrace);
|
|
||||||
}
|
|
||||||
|
|
||||||
return widget.errorBuilder(snapshot.error);
|
return widget.errorBuilder(snapshot.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!snapshot.hasData) {
|
if (!snapshot.hasData) {
|
||||||
return widget.loadingBuilder(context);
|
return widget.loadingBuilder(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
final items = snapshot.data;
|
final items = snapshot.data;
|
||||||
|
|
||||||
if (items.isEmpty) {
|
if (items.isEmpty) {
|
||||||
return widget.emptyBuilder(context);
|
return widget.emptyBuilder(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (items.isEmpty) {
|
|
||||||
return widget.emptyBuilder(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
return widget.listBuilder(context, items);
|
return widget.listBuilder(context, items);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
void loadData() {
|
// ignore: public_member_api_docs
|
||||||
UsersBloc.of(context).queryUsers(
|
Future<void> loadData() {
|
||||||
|
final _usersBloc = UsersBloc.of(context);
|
||||||
|
return _usersBloc.queryUsers(
|
||||||
filter: widget.filter,
|
filter: widget.filter,
|
||||||
sort: widget.sort,
|
sort: widget.sort,
|
||||||
pagination: widget.pagination,
|
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);
|
final _usersBloc = UsersBloc.of(context);
|
||||||
|
return _usersBloc.queryUsers(
|
||||||
_usersBloc.queryUsers(
|
|
||||||
filter: widget.filter,
|
filter: widget.filter,
|
||||||
sort: widget.sort,
|
sort: widget.sort,
|
||||||
pagination: widget.pagination.copyWith(
|
pagination: widget.pagination.copyWith(
|
||||||
@@ -238,15 +222,10 @@ class _UserListCoreState extends State<UserListCore>
|
|||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
if (widget.filter?.toString() != oldWidget.filter?.toString() ||
|
if (widget.filter?.toString() != oldWidget.filter?.toString() ||
|
||||||
jsonEncode(widget.sort) != jsonEncode(oldWidget.sort) ||
|
jsonEncode(widget.sort) != jsonEncode(oldWidget.sort) ||
|
||||||
|
widget.options?.toString() != oldWidget.options?.toString() ||
|
||||||
widget.pagination?.toJson()?.toString() !=
|
widget.pagination?.toJson()?.toString() !=
|
||||||
oldWidget.pagination?.toJson()?.toString() ||
|
oldWidget.pagination?.toJson()?.toString()) {
|
||||||
widget.options?.toString() != oldWidget.options?.toString()) {
|
loadData();
|
||||||
UsersBloc.of(context).queryUsers(
|
|
||||||
filter: widget.filter,
|
|
||||||
sort: widget.sort,
|
|
||||||
pagination: widget.pagination,
|
|
||||||
options: widget.options,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -255,11 +234,11 @@ class _UserListCoreState extends State<UserListCore>
|
|||||||
/// Header items are prefixed with the key `HEADER` While users are prefixed
|
/// Header items are prefixed with the key `HEADER` While users are prefixed
|
||||||
/// with `USER`.
|
/// with `USER`.
|
||||||
abstract class ListItem {
|
abstract class ListItem {
|
||||||
// ignore: public_member_api_docs
|
/// Unique key per list item
|
||||||
String get key {
|
String get key {
|
||||||
if (this is ListHeaderItem) {
|
if (this is ListHeaderItem) {
|
||||||
final header = (this as ListHeaderItem).heading;
|
final header = (this as ListHeaderItem).heading;
|
||||||
return 'HEADER-$header';
|
return 'HEADER-${header.toLowerCase()}';
|
||||||
}
|
}
|
||||||
if (this is ListUserItem) {
|
if (this is ListUserItem) {
|
||||||
final user = (this as ListUserItem).user;
|
final user = (this as ListUserItem).user;
|
||||||
@@ -268,7 +247,8 @@ abstract class ListItem {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ignore: public_member_api_docs
|
/// Helper function to build widget based on ListItem type
|
||||||
|
// ignore: missing_return
|
||||||
Widget when({
|
Widget when({
|
||||||
@required Widget Function(String heading) headerItem,
|
@required Widget Function(String heading) headerItem,
|
||||||
@required Widget Function(User user) userItem,
|
@required Widget Function(User user) userItem,
|
||||||
@@ -279,33 +259,32 @@ abstract class ListItem {
|
|||||||
if (this is ListUserItem) {
|
if (this is ListUserItem) {
|
||||||
return userItem((this as ListUserItem).user);
|
return userItem((this as ListUserItem).user);
|
||||||
}
|
}
|
||||||
return const SizedBox();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ignore: public_member_api_docs
|
/// Header Item
|
||||||
class ListHeaderItem extends ListItem {
|
class ListHeaderItem extends ListItem {
|
||||||
// ignore: public_member_api_docs
|
/// Constructs a new [ListHeaderItem]
|
||||||
ListHeaderItem(this.heading);
|
ListHeaderItem(this.heading);
|
||||||
|
|
||||||
// ignore: public_member_api_docs
|
/// Heading used to build the item.
|
||||||
final String heading;
|
final String heading;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ignore: public_member_api_docs
|
/// User Item
|
||||||
class ListUserItem extends ListItem {
|
class ListUserItem extends ListItem {
|
||||||
// ignore: public_member_api_docs
|
/// Constructs a new [ListUserItem]
|
||||||
ListUserItem(this.user);
|
ListUserItem(this.user);
|
||||||
|
|
||||||
// ignore: public_member_api_docs
|
/// [User] used to build the item.
|
||||||
final User user;
|
final User user;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Controller used for paginating data in [ChannelListView]
|
/// Controller used for paginating data in [ChannelListView]
|
||||||
class UserListController {
|
class UserListController {
|
||||||
/// Call this function to reload data
|
/// Call this function to reload data
|
||||||
VoidCallback loadData;
|
AsyncCallback loadData;
|
||||||
|
|
||||||
/// Call this function to load further data
|
/// 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
|
/// The current users list as a stream
|
||||||
Stream<List<User>> get usersStream => _usersController.stream;
|
Stream<List<User>> get usersStream => _usersController.stream;
|
||||||
|
|
||||||
final BehaviorSubject<List<User>> _usersController = BehaviorSubject();
|
final _usersController = BehaviorSubject<List<User>>();
|
||||||
|
|
||||||
final BehaviorSubject<bool> _queryUsersLoadingController =
|
final _queryUsersLoadingController = BehaviorSubject.seeded(false);
|
||||||
BehaviorSubject.seeded(false);
|
|
||||||
|
|
||||||
/// The stream notifying the state of queryUsers call
|
/// The stream notifying the state of queryUsers call
|
||||||
Stream<bool> get queryUsersLoading => _queryUsersLoadingController.stream;
|
Stream<bool> get queryUsersLoading => _queryUsersLoadingController.stream;
|
||||||
@@ -70,11 +69,12 @@ class UsersBlocState extends State<UsersBloc>
|
|||||||
}) async {
|
}) async {
|
||||||
final client = StreamChatCore.of(context).client;
|
final client = StreamChatCore.of(context).client;
|
||||||
|
|
||||||
if (client.state?.user == null ||
|
if (_queryUsersLoadingController.value == true) return;
|
||||||
_queryUsersLoadingController.value == true) {
|
|
||||||
return;
|
if (_usersController.hasValue) {
|
||||||
|
_queryUsersLoadingController.add(true);
|
||||||
}
|
}
|
||||||
_queryUsersLoadingController.add(true);
|
|
||||||
try {
|
try {
|
||||||
final clear = pagination == null ||
|
final clear = pagination == null ||
|
||||||
pagination.offset == null ||
|
pagination.offset == null ||
|
||||||
@@ -95,10 +95,15 @@ class UsersBlocState extends State<UsersBloc>
|
|||||||
final temp = oldUsers + usersResponse.users;
|
final temp = oldUsers + usersResponse.users;
|
||||||
_usersController.add(temp);
|
_usersController.add(temp);
|
||||||
}
|
}
|
||||||
|
if (_usersController.hasValue && _queryUsersLoadingController.value) {
|
||||||
_queryUsersLoadingController.add(false);
|
_queryUsersLoadingController.add(false);
|
||||||
} catch (err, stackTrace) {
|
}
|
||||||
_queryUsersLoadingController.addError(err, stackTrace);
|
} 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 '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/channels_bloc.dart';
|
||||||
export 'src/lazy_load_scroll_view.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_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_channel.dart';
|
||||||
export 'src/stream_chat_core.dart';
|
export 'src/stream_chat_core.dart';
|
||||||
export 'src/typedef.dart';
|
export 'src/typedef.dart';
|
||||||
export 'src/user_list_core.dart';
|
export 'src/user_list_core.dart' hide UserListCoreState;
|
||||||
export 'src/users_bloc.dart';
|
export 'src/users_bloc.dart';
|
||||||
|
|||||||
@@ -15,8 +15,11 @@ dependencies:
|
|||||||
rxdart: ^0.25.0
|
rxdart: ^0.25.0
|
||||||
stream_chat: ^1.5.0
|
stream_chat: ^1.5.0
|
||||||
|
|
||||||
|
dependency_overrides:
|
||||||
|
stream_chat:
|
||||||
|
path: ../stream_chat
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
fake_async: ^1.1.0
|
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
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:mockito/mockito.dart';
|
||||||
import 'package:stream_chat/stream_chat.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 {}
|
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 'dart:async';
|
||||||
|
|
||||||
import 'package:fake_async/fake_async.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:mockito/mockito.dart';
|
import 'package:mockito/mockito.dart';
|
||||||
@@ -8,169 +7,283 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
|||||||
|
|
||||||
import 'mocks.dart';
|
import 'mocks.dart';
|
||||||
|
|
||||||
class MockShowLocalNotifications extends Mock {
|
class MockOnBackgroundEventReceived extends Mock {
|
||||||
void call(Event event);
|
void call(Event event);
|
||||||
}
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
testWidgets(
|
test(
|
||||||
'StreamChatCore.of(context) should throw an exception',
|
'should throw assertion error in case client is null',
|
||||||
(WidgetTester tester) async {
|
() {
|
||||||
await tester.pumpWidget(
|
final streamChatCore = () => StreamChatCore(
|
||||||
Builder(
|
client: null,
|
||||||
builder: (context) {
|
child: Offstage(),
|
||||||
expect(() => StreamChatCore.of(context), throwsException);
|
);
|
||||||
return Container();
|
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(
|
testWidgets(
|
||||||
'StreamChatCore.of(context) should return the StreamChatCore ancestor',
|
'should render StreamChatCore if both client and child is provided',
|
||||||
(WidgetTester tester) async {
|
(tester) async {
|
||||||
final client = MockClient();
|
final mockClient = MockClient();
|
||||||
await tester.pumpWidget(
|
const streamChatCoreKey = Key('streamChatCore');
|
||||||
StreamChatCore(
|
const childKey = Key('child');
|
||||||
client: client,
|
final streamChatCore = StreamChatCore(
|
||||||
child: Builder(
|
key: streamChatCoreKey,
|
||||||
builder: (context) {
|
client: mockClient,
|
||||||
final sc = StreamChatCore.of(context);
|
child: Offstage(key: childKey),
|
||||||
expect(sc, isNotNull);
|
|
||||||
return Container();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(streamChatCore);
|
||||||
|
|
||||||
|
expect(find.byKey(streamChatCoreKey), findsOneWidget);
|
||||||
|
expect(find.byKey(childKey), findsOneWidget);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
testWidgets(
|
testWidgets(
|
||||||
'StreamChatCore.of(context).client should return the client',
|
'should render StreamChatCore if both client and child is provided',
|
||||||
(WidgetTester tester) async {
|
(tester) async {
|
||||||
final client = MockClient();
|
final mockClient = MockClient();
|
||||||
final clientState = MockClientState();
|
const streamChatCoreKey = Key('streamChatCore');
|
||||||
when(client.state).thenReturn(clientState);
|
const childKey = Key('child');
|
||||||
when(clientState.user).thenReturn(
|
final streamChatCore = StreamChatCore(
|
||||||
OwnUser(
|
key: streamChatCoreKey,
|
||||||
id: 'test',
|
client: mockClient,
|
||||||
),
|
child: Offstage(key: childKey),
|
||||||
);
|
|
||||||
final userStream = Stream<OwnUser>.value(
|
|
||||||
OwnUser(
|
|
||||||
id: 'test',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
when(clientState.userStream).thenAnswer(
|
|
||||||
(_) => userStream,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(streamChatCore);
|
||||||
StreamChatCore(
|
|
||||||
client: client,
|
expect(find.byKey(streamChatCoreKey), findsOneWidget);
|
||||||
child: Builder(
|
expect(find.byKey(childKey), findsOneWidget);
|
||||||
builder: (context) {
|
|
||||||
final sc = StreamChatCore.of(context);
|
|
||||||
expect(sc.client, client);
|
|
||||||
expect(sc.user, client.state.user);
|
|
||||||
expect(sc.userStream, userStream);
|
|
||||||
return Container();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
testWidgets(
|
testWidgets(
|
||||||
'StreamChatCore should disconnect on background',
|
'didChangeAppLifecycleState should call client.disconnect() and return '
|
||||||
(WidgetTester tester) async {
|
'if onBackgroundEventReceived is null and the widget lifestyle changes to '
|
||||||
fakeAsync((_async) {
|
'AppLifecycleState.paused',
|
||||||
final client = MockClient();
|
(tester) async {
|
||||||
final clientState = MockClientState();
|
final mockClient = MockClient();
|
||||||
final channel = MockChannel();
|
const streamChatCoreKey = Key('streamChatCore');
|
||||||
when(client.state).thenReturn(clientState);
|
const childKey = Key('child');
|
||||||
when(clientState.user).thenReturn(
|
final streamChatCore = StreamChatCore(
|
||||||
OwnUser(
|
key: streamChatCoreKey,
|
||||||
id: 'test',
|
client: mockClient,
|
||||||
),
|
child: Offstage(key: childKey),
|
||||||
);
|
);
|
||||||
final showLocalNotificationMock = MockShowLocalNotifications().call;
|
|
||||||
final eventStreamController = StreamController<Event>();
|
|
||||||
when(client.on()).thenAnswer((_) => eventStreamController.stream);
|
|
||||||
|
|
||||||
when(client.channel('test', id: 'testid')).thenReturn(channel);
|
await tester.pumpWidget(streamChatCore);
|
||||||
|
|
||||||
final scKey = GlobalKey<StreamChatCoreState>();
|
expect(find.byKey(streamChatCoreKey), findsOneWidget);
|
||||||
tester.pumpWidget(
|
expect(find.byKey(childKey), findsOneWidget);
|
||||||
StreamChatCore(
|
|
||||||
key: scKey,
|
when(mockClient.disconnect()).thenAnswer((_) async {
|
||||||
client: client,
|
return;
|
||||||
onBackgroundEventReceived: showLocalNotificationMock,
|
});
|
||||||
backgroundKeepAlive: const Duration(seconds: 4),
|
|
||||||
child: Builder(
|
final streamChatCoreState = tester.state<StreamChatCoreState>(
|
||||||
builder: (context) => Container(),
|
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);
|
final event = Event();
|
||||||
eventStreamController.close();
|
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(
|
testWidgets(
|
||||||
'StreamChatCore should handle notifications when on background and '
|
'didChangeAppLifecycleState should cancel the backgroundKeepAlive timer '
|
||||||
'connected',
|
'if it is currently running in case the widget lifestyle changes to '
|
||||||
(WidgetTester tester) async {
|
'AppLifecycleState.resume',
|
||||||
final client = MockClient();
|
(tester) async {
|
||||||
final clientState = MockClientState();
|
await tester.runAsync(() async {
|
||||||
final channel = MockChannel();
|
final mockClient = MockClient();
|
||||||
when(client.state).thenReturn(clientState);
|
final mockOnBackgroundEventReceived = MockOnBackgroundEventReceived();
|
||||||
when(clientState.user).thenReturn(
|
const backgroundKeepAlive = const Duration(seconds: 3);
|
||||||
OwnUser(
|
const streamChatCoreKey = Key('streamChatCore');
|
||||||
id: 'test',
|
const childKey = Key('child');
|
||||||
),
|
final streamChatCore = StreamChatCore(
|
||||||
);
|
key: streamChatCoreKey,
|
||||||
final showLocalNotificationMock = MockShowLocalNotifications().call;
|
client: mockClient,
|
||||||
final eventStreamController = StreamController<Event>();
|
child: Offstage(key: childKey),
|
||||||
when(client.on()).thenAnswer((_) => eventStreamController.stream);
|
onBackgroundEventReceived: mockOnBackgroundEventReceived,
|
||||||
|
backgroundKeepAlive: backgroundKeepAlive,
|
||||||
|
);
|
||||||
|
|
||||||
when(client.channel('test', id: 'testid')).thenReturn(channel);
|
await tester.pumpWidget(streamChatCore);
|
||||||
|
|
||||||
final scKey = GlobalKey<StreamChatCoreState>();
|
expect(find.byKey(streamChatCoreKey), findsOneWidget);
|
||||||
await tester.pumpWidget(
|
expect(find.byKey(childKey), findsOneWidget);
|
||||||
StreamChatCore(
|
|
||||||
key: scKey,
|
|
||||||
client: client,
|
|
||||||
onBackgroundEventReceived: showLocalNotificationMock,
|
|
||||||
backgroundKeepAlive: const Duration(seconds: 4),
|
|
||||||
child: Builder(
|
|
||||||
builder: (context) => Container(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
scKey.currentState.didChangeAppLifecycleState(AppLifecycleState.paused);
|
final event = Event();
|
||||||
final event = Event(
|
when(mockClient.on()).thenAnswer((_) => Stream.value(event));
|
||||||
type: EventType.messageNew,
|
|
||||||
message: Message(text: 'hey'),
|
|
||||||
channelType: 'test',
|
|
||||||
channelId: 'testid',
|
|
||||||
user: User(id: 'other user'),
|
|
||||||
);
|
|
||||||
eventStreamController.add(event);
|
|
||||||
|
|
||||||
await untilCalled(showLocalNotificationMock(event));
|
final streamChatCoreState = tester.state<StreamChatCoreState>(
|
||||||
|
find.byKey(streamChatCoreKey),
|
||||||
|
);
|
||||||
|
|
||||||
verify(showLocalNotificationMock(event)).called(1);
|
streamChatCoreState
|
||||||
eventStreamController.close();
|
.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);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user