chore: 1.5.1 (#387)

* lint(persistence): Apply team linter

Signed-off-by: Sahil Kumar <[email protected]>

* style: flutter format

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): Add tests for channel dao

Signed-off-by: Sahil Kumar <[email protected]>

* refactor(persistence): remove dummy comments

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): Add test for channel query dao

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add tests for connection event dao

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add test for member dao

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add tests for message dao

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add test for pinned message dao

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add test for reaction dao

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add test for read dao

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): minor improvement in update function tests

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add tests for user dao

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add test for list converter

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add test for map converter

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add tests for message sending status converter

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add tests for channel mapper

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add test for event mapper

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add tests for member mapper

Signed-off-by: Sahil Kumar <[email protected]>

* remove useless dependencies

* test(persistence): add tests for message mapper

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add tests for pinned message mapper

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add tests for reaction mapper

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add tests for read mapper

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add tests for user mapper

Signed-off-by: Sahil Kumar <[email protected]>

* refactor(persistence): remove redundant UsersTable getter from ConnectionEventDao

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add tests for stream chat persistence client

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add tests moor chat database

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add moor schemas for future migration tests

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add clear queryCache test in channel query dao

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add getThreadMessagesByParentId along with pagination test in message, pinned message dao.

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add getMessagesByCid along with pagination test in message, pinned message dao.

Signed-off-by: Sahil Kumar <[email protected]>

* update ci

* apply locale to jiffy

* fix test

* test(persistence): add getMessagesByCid along with quotedMessage test in message, pinned message dao.

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add extra checks in getMessagesByCid along with pagination test in message, pinned message dao.

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add return channel with pagination applies test in channel query dao

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): improve message mapper, pinned message mapper test

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add client constructor test in stream chat persistence client

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add coverage:ignore-file for entities and db/shared directory

Signed-off-by: Sahil Kumar <[email protected]>

* skip pub publish --dry-run for develop branch

Signed-off-by: Sahil Kumar <[email protected]>

* add remove from coverage script

Signed-off-by: Sahil Kumar <[email protected]>

* fix

Signed-off-by: Sahil Kumar <[email protected]>

* fix

Signed-off-by: Sahil Kumar <[email protected]>

* fix

Signed-off-by: Sahil Kumar <[email protected]>

* fix

Signed-off-by: Sahil Kumar <[email protected]>

* fix locale

* fix message input _parseExistingMessage

* fix(UI-Kit): Show selected attachment file as file in message input widget

Signed-off-by: Sahil Kumar <[email protected]>

* fix: ws disconnection (#345)

* update ws removing custom implementation; fixed disconnection while connecting

* add test

* feat: Apply team lints to core package (#334)

* Add analyzer file to core package

* Apply team lint rules to core package

* clean up analyzer warnings in stream chat flutter

* remove unused line

* add ignore

* sync analysis options

* [Core -> MessageWidget] Fix variable dynamic types

Signed-off-by: Sahil Kumar <[email protected]>

Co-authored-by: Salvatore Giordano <[email protected]>
Co-authored-by: Sahil Kumar <[email protected]>

* 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]>

* refactor(ui-kit): update giphy interaction (#361)

* refactor(ui-kit): update giphy interaction

Signed-off-by: Sahil Kumar <[email protected]>

* fix overflow

Co-authored-by: Salvatore Giordano <[email protected]>

* fix: Fixed sendingbutton.outside case (#355)

* fix: Fixed sendingbutton.outside case

* fix: Fixed all actionbutton cases

* fix review

Co-authored-by: Salvatore Giordano <[email protected]>

* fix(ui-kit): fix accessibility text size scaling in message widget

Signed-off-by: Sahil Kumar <[email protected]>

* fix(ui-kit): fix message widget long username overflow

Signed-off-by: Sahil Kumar <[email protected]>

* fix(ui-kit): fix message input dm checkbox accessibility scaling issue

Signed-off-by: Sahil Kumar <[email protected]>

* feat: exposed textbuilder callback (#367)

* exposed textbuilder callback

* exposed textbuilder callback

* fixes

Signed-off-by: Sahil Kumar <[email protected]>

* check if user.mutes is null

* check if user.mutes is null

* fixes #370: remove all controls on full screen media if message is of type ephemeral (#373)

* fix: fix channel list pagination spinner (#375)

Signed-off-by: Sahil Kumar <[email protected]>

* fix: attachments upload when size exceeds (#377)

* fix: Fixed file upload error

* fix: Fixed file upload error

* fix coverage script

Signed-off-by: Sahil Kumar <[email protected]>

* fix: message input (#382)

* feat: Added mentions suffix builder

* feat: Added builders for mentions and theme element for message input expanded button

* refactor: refactored PR to suit other style

* fix: fixes trailing commas and changes names

* doc: added documentation to new elements

* doc: added documentation to new elements

* bump version

* add meta dep to core

* sort deps

Co-authored-by: Sahil Kumar <[email protected]>
Co-authored-by: Neevash Ramdial (Nash) <[email protected]>
Co-authored-by: Deven Joshi <[email protected]>
This commit is contained in:
Salvatore Giordano
2021-04-07 12:24:29 +02:00
committed by GitHub
co-authored by Sahil Kumar Neevash Ramdial Deven Joshi
parent aeb6da8d38
commit 87ca818a9a
119 changed files with 11223 additions and 1240 deletions
@@ -1,3 +1,8 @@
## 1.5.1
* Improved test coverage to > 90%
* Minor fixes and improvements
## 1.5.0
* Minor fixes and improvements
@@ -0,0 +1,145 @@
analyzer:
exclude:
- lib/**/*.g.dart
- lib/**/*.freezed.dart
- example/*
- test/*
linter:
rules:
- always_use_package_imports
- avoid_empty_else
- avoid_relative_lib_imports
- avoid_slow_async_io
- avoid_types_as_parameter_names
- cancel_subscriptions
- close_sinks
- control_flow_in_finally
- empty_statements
- hash_and_equals
- invariant_booleans
- iterable_contains_unrelated_type
- list_remove_unrelated_type
- literal_only_boolean_expressions
- no_adjacent_strings_in_list
- no_duplicate_case_values
- no_logic_in_create_state
- prefer_void_to_null
- test_types_in_equals
- throw_in_finally
- unnecessary_statements
- unrelated_type_equality_checks
- omit_local_variable_types
- use_key_in_widget_constructors
- valid_regexps
- always_declare_return_types
- always_require_non_null_named_parameters
- annotate_overrides
- avoid_bool_literals_in_conditional_expressions
- avoid_catching_errors
- avoid_init_to_null
- avoid_null_checks_in_equality_operators
- avoid_positional_boolean_parameters
- avoid_private_typedef_functions
- avoid_redundant_argument_values
- avoid_return_types_on_setters
- avoid_returning_null
- avoid_returning_null_for_void
- avoid_shadowing_type_parameters
- avoid_single_cascade_in_expression_statements
- avoid_unnecessary_containers
- avoid_unused_constructor_parameters
- await_only_futures
- camel_case_extensions
- camel_case_types
- cascade_invocations
- constant_identifier_names
- curly_braces_in_flow_control_structures
- directives_ordering
- empty_catches
- empty_constructor_bodies
- exhaustive_cases
- file_names
- implementation_imports
- join_return_with_assignment
- leading_newlines_in_multiline_strings
- library_names
- library_prefixes
- lines_longer_than_80_chars
- missing_whitespace_between_adjacent_strings
- non_constant_identifier_names
- null_closures
- one_member_abstracts
- only_throw_errors
- package_api_docs
- package_prefixed_library_names
- parameter_assignments
- prefer_adjacent_string_concatenation
- prefer_asserts_in_initializer_lists
- prefer_asserts_with_message
- prefer_collection_literals
- prefer_conditional_assignment
- prefer_const_constructors
- prefer_const_constructors_in_immutables
- prefer_const_declarations
- prefer_const_literals_to_create_immutables
- prefer_constructors_over_static_methods
- prefer_contains
- prefer_equal_for_default_values
- prefer_expression_function_bodies
- prefer_final_fields
- prefer_final_in_for_each
- prefer_final_locals
- prefer_function_declarations_over_variables
- prefer_generic_function_type_aliases
- prefer_if_elements_to_conditional_expressions
- prefer_if_null_operators
- prefer_initializing_formals
- prefer_inlined_adds
- prefer_int_literals
- prefer_interpolation_to_compose_strings
- prefer_is_empty
- prefer_is_not_empty
- prefer_is_not_operator
- prefer_null_aware_operators
- prefer_single_quotes
- prefer_spread_collections
- prefer_typing_uninitialized_variables
- provide_deprecation_message
- public_member_api_docs
- recursive_getters
- sized_box_for_whitespace
- slash_for_doc_comments
- sort_child_properties_last
- sort_constructors_first
- sort_unnamed_constructors_first
- type_annotate_public_apis
- type_init_formals
- unnecessary_await_in_return
- unnecessary_brace_in_string_interps
- unnecessary_const
- unnecessary_getters_setters
- unnecessary_lambdas
- unnecessary_new
- unnecessary_null_aware_assignments
- unnecessary_null_in_if_null_operators
- unnecessary_nullable_for_final_variable_declarations
- unnecessary_parenthesis
- unnecessary_raw_strings
- unnecessary_string_escapes
- unnecessary_string_interpolations
- unnecessary_this
- use_is_even_rather_than_modulo
- use_late_for_private_fields_and_variables
- use_rethrow_when_possible
- use_setters_to_change_properties
- use_to_and_as_if_applicable
- package_names
- sort_pub_dependencies
# To be added when null-safe:
# - cast_nullable_to_non_nullable
#- unnecessary_null_checks
# - tighten_type_of_initializing_formals
# - null_check_on_nullable_type_parameter
@@ -5,11 +5,11 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter_core/src/channels_bloc.dart';
import 'package:stream_chat_flutter_core/src/stream_chat_core.dart';
import 'package:stream_chat_flutter_core/src/typedef.dart';
import 'stream_chat_core.dart';
/// [ChannelListCore] is a simplified class that allows fetching a list of channels while exposing UI builders.
/// [ChannelListCore] is a simplified class that allows fetching a list of
/// channels while exposing UI builders.
/// A [ChannelListController] is used to reload and paginate data.
///
///
@@ -52,10 +52,11 @@ import 'stream_chat_core.dart';
/// }
/// ```
///
/// Make sure to have a [StreamChatCore] ancestor in order to provide the information about the channels.
/// Make sure to have a [StreamChatCore] ancestor in order to provide the
/// information about the channels.
class ChannelListCore extends StatefulWidget {
/// Instantiate a new ChannelListView
ChannelListCore({
const ChannelListCore({
Key key,
@required this.errorBuilder,
@required this.emptyBuilder,
@@ -65,18 +66,31 @@ class ChannelListCore extends StatefulWidget {
this.options,
this.sort,
this.pagination = const PaginationParams(
offset: 0,
limit: 25,
),
this.channelListController,
}) : assert(errorBuilder != null),
assert(emptyBuilder != null),
assert(loadingBuilder != null),
assert(listBuilder != null),
}) : assert(
errorBuilder != null,
'Parameter errorBuilder should not be null',
),
assert(
emptyBuilder != null,
'Parameter emptyBuilder should not be null',
),
assert(
loadingBuilder != null,
'Parameter loadingBuilder should not be null',
),
assert(
listBuilder != null,
'Parameter listBuilder should not be null',
),
super(key: key);
/// A [ChannelListController] allows reloading and pagination.
/// Use [ChannelListController.loadData] and [ChannelListController.paginateData] respectively for reloading and pagination.
/// Use [ChannelListController.loadData] and
/// [ChannelListController.paginateData] respectively for reloading and
/// pagination.
final ChannelListController channelListController;
/// The builder that will be used in case of error
@@ -103,9 +117,10 @@ class ChannelListCore extends StatefulWidget {
final Map<String, dynamic> options;
/// The sorting used for the channels matching the filters.
/// Sorting is based on field and direction, multiple sorting options can be provided.
/// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count.
/// Direction can be ascending or descending.
/// Sorting is based on field and direction, multiple sorting options can be
/// provided.
/// You can sort based on last_updated, last_message_at, updated_at, created
/// _at or member_count. Direction can be ascending or descending.
final List<SortOption<ChannelModel>> sort;
/// Pagination parameters
@@ -115,10 +130,11 @@ class ChannelListCore extends StatefulWidget {
final PaginationParams pagination;
@override
_ChannelListCoreState createState() => _ChannelListCoreState();
ChannelListCoreState createState() => ChannelListCoreState();
}
class _ChannelListCoreState extends State<ChannelListCore> {
/// The current state of the [ChannelListCore].
class ChannelListCoreState extends State<ChannelListCore> {
@override
Widget build(BuildContext context) {
final channelsBloc = ChannelsBloc.of(context);
@@ -128,37 +144,25 @@ class _ChannelListCoreState extends State<ChannelListCore> {
StreamBuilder<List<Channel>> _buildListView(
ChannelsBlocState channelsBlocState,
) {
return StreamBuilder<List<Channel>>(
stream: channelsBlocState.channelsStream,
builder: (context, snapshot) {
if (snapshot.hasError) {
return _buildErrorWidget(snapshot, context, channelsBlocState);
}
if (!snapshot.hasData) {
return widget.loadingBuilder(context);
}
final channels = snapshot.data;
if (channels.isEmpty) {
return widget.emptyBuilder(context);
}
return widget.listBuilder(context, channels);
},
);
}
Widget _buildErrorWidget(
AsyncSnapshot<List<Channel>> snapshot,
BuildContext context,
ChannelsBlocState channelsBlocState,
) {
if (snapshot.error is Error) {
print((snapshot.error as Error).stackTrace);
}
return widget.errorBuilder(context, snapshot.error);
}
) =>
StreamBuilder<List<Channel>>(
stream: channelsBlocState.channelsStream,
builder: (context, snapshot) {
if (snapshot.hasError) {
return widget.errorBuilder(context, snapshot.error);
}
if (!snapshot.hasData) {
return widget.loadingBuilder(context);
}
final channels = snapshot.data;
if (channels.isEmpty) {
return widget.emptyBuilder(context);
}
return widget.listBuilder(context, channels);
},
);
/// Fetches initial channels and updates the widget
Future<void> loadData() {
final channelsBloc = ChannelsBloc.of(context);
return channelsBloc.queryChannels(
@@ -169,6 +173,7 @@ class _ChannelListCoreState extends State<ChannelListCore> {
);
}
/// Fetches more channels with updated pagination and updates the widget
Future<void> paginateData() {
final channelsBloc = ChannelsBloc.of(context);
return channelsBloc.queryChannels(
@@ -209,9 +214,9 @@ class _ChannelListCoreState extends State<ChannelListCore> {
if (widget.filter?.toString() != oldWidget.filter?.toString() ||
jsonEncode(widget.sort) != jsonEncode(oldWidget.sort) ||
widget.options?.toString() != oldWidget.options?.toString() ||
widget.pagination?.toJson()?.toString() !=
oldWidget.pagination?.toJson()?.toString() ||
widget.options?.toString() != oldWidget.options?.toString()) {
oldWidget.pagination?.toJson()?.toString()) {
loadData();
}
}
@@ -223,14 +228,15 @@ class _ChannelListCoreState extends State<ChannelListCore> {
}
}
/// Controller used for loading more data and controlling pagination in [ChannelListCore].
/// Controller used for loading more data and controlling pagination in
/// [ChannelListCore].
class ChannelListController {
/// This function calls Stream's servers to load a list of channels. If there is existing data,
/// calling this function causes a reload.
/// This function calls Stream's servers to load a list of channels.
/// If there is existing data, calling this function causes a reload.
AsyncCallback loadData;
/// This function is used to load another page of data. Note, [loadData] should be
/// used to populate the initial page of data. Calling [paginateData] performs a query
/// to load subsequent pages.
/// This function is used to load another page of data. Note, [loadData]
/// should be used to populate the initial page of data. Calling
/// [paginateData] performs a query to load subsequent pages.
AsyncCallback paginateData;
}
@@ -1,5 +1,6 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:rxdart/rxdart.dart';
import 'package:stream_chat/stream_chat.dart';
@@ -16,26 +17,29 @@ import 'package:stream_chat_flutter_core/src/stream_chat_core.dart';
///
/// API docs: https://getstream.io/chat/docs/flutter-dart/query_channels/
class ChannelsBloc extends StatefulWidget {
/// Creates a new [ChannelsBloc]. The parameter [child] must be supplied and not null.
/// Creates a new [ChannelsBloc]. The parameter [child] must be supplied and
/// not null.
const ChannelsBloc({
Key key,
@required this.child,
this.lockChannelsOrder = false,
this.channelsComparator,
this.shouldAddChannel,
}) : assert(child != null),
}) : assert(child != null, 'Parameter child should not be null.'),
super(key: key);
/// The widget child
final Widget child;
/// Set this to true to prevent channels to be brought to the top of the list when a new message arrives
/// Set this to true to prevent channels to be brought to the top of the list
/// when a new message arrives
final bool lockChannelsOrder;
/// Comparator used to sort the channels when a message.new event is received
final Comparator<Channel> channelsComparator;
/// Function used to evaluate if a channel should be added to the list when a message.new event is received
/// Function used to evaluate if a channel should be added to the list when a
/// message.new event is received
final bool Function(Event) shouldAddChannel;
@override
@@ -70,10 +74,9 @@ class ChannelsBlocState extends State<ChannelsBloc>
/// The current channel list as a stream
Stream<List<Channel>> get channelsStream => _channelsController.stream;
final BehaviorSubject<bool> _queryChannelsLoadingController =
BehaviorSubject.seeded(false);
final _queryChannelsLoadingController = BehaviorSubject.seeded(false);
final BehaviorSubject<List<Channel>> _channelsController = BehaviorSubject();
final _channelsController = BehaviorSubject<List<Channel>>();
/// The stream notifying the state of queryChannel call
Stream<bool> get queryChannelsLoading =>
@@ -81,45 +84,58 @@ class ChannelsBlocState extends State<ChannelsBloc>
final List<Channel> _hiddenChannels = [];
bool _paginationEnded = false;
/// Calls [client.queryChannels] updating [queryChannelsLoading] stream
Future<void> queryChannels({
Map<String, dynamic> filter,
List<SortOption<ChannelModel>> sortOptions,
PaginationParams paginationParams,
Map<String, dynamic> options,
bool onlyOffline = false,
}) async {
final client = StreamChatCore.of(context).client;
if (client.state?.user == null ||
_queryChannelsLoadingController.value == true) {
if (_paginationEnded || _queryChannelsLoadingController.value == true) {
return;
}
_queryChannelsLoadingController.sink.add(true);
if (_channelsController.hasValue) {
_queryChannelsLoadingController.add(true);
}
try {
final clear = paginationParams == null ||
paginationParams.offset == null ||
paginationParams.offset == 0;
final oldChannels = List<Channel>.from(channels ?? []);
var newChannels = <Channel>[];
await for (final channels in client.queryChannels(
filter: filter,
sort: sortOptions,
options: options,
paginationParams: paginationParams,
)) {
newChannels = channels;
if (clear) {
_channelsController.add(channels);
} else {
final l = oldChannels + channels;
_channelsController.add(l);
final temp = oldChannels + channels;
_channelsController.add(temp);
}
if (_channelsController.hasValue &&
_queryChannelsLoadingController.value) {
_queryChannelsLoadingController.sink.add(false);
}
_queryChannelsLoadingController.sink.add(false);
}
} catch (err, stackTrace) {
print(err);
print(stackTrace);
_queryChannelsLoadingController.addError(err, stackTrace);
if (newChannels.isEmpty || newChannels.length < paginationParams.limit) {
_paginationEnded = true;
}
} catch (e, stk) {
if (_channelsController.hasValue) {
_queryChannelsLoadingController.addError(e, stk);
} else {
_channelsController.addError(e, stk);
}
}
}
@@ -135,15 +151,14 @@ class ChannelsBlocState extends State<ChannelsBloc>
_subscriptions.add(client.on(EventType.messageNew).listen((e) {
final newChannels = List<Channel>.from(channels ?? []);
final index = newChannels.indexWhere((c) => c.cid == e.cid);
if (index > -1) {
if (index != -1) {
if (index > 0) {
final channel = newChannels.removeAt(index);
newChannels.insert(0, channel);
}
} else if (widget.shouldAddChannel != null &&
widget.shouldAddChannel(e)) {
} else if (widget.shouldAddChannel?.call(e) == true) {
final hiddenIndex = _hiddenChannels.indexWhere((c) => c.cid == e.cid);
if (hiddenIndex > -1) {
if (hiddenIndex != -1) {
newChannels.insert(0, _hiddenChannels[hiddenIndex]);
_hiddenChannels.removeAt(hiddenIndex);
} else {
@@ -170,13 +185,14 @@ class ChannelsBlocState extends State<ChannelsBloc>
_channelsController.add(newChannels);
}
}));
// ignore: cascade_invocations
_subscriptions.add(client
.on(
EventType.channelDeleted,
EventType.notificationRemovedFromChannel,
)
.listen((e) {
// ignore: cascade_invocations
final channel = e.channel;
_channelsController.add(List.from(
(channels ?? [])..removeWhere((c) => c.cid == channel.cid)));
@@ -1,13 +1,13 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
enum _LoadingStatus { LOADING, STABLE }
enum _LoadingStatus { loading, stable }
/// Wrapper around a [Scrollable] which triggers [onEndOfPage]/[onStartOfPage] the Scrollable
/// reaches to the start or end of the view extent.
class LazyLoadScrollView extends StatefulWidget {
/// Creates a new instance of [LazyLoadScrollView]. The parameter [child] must be
/// supplied and not null.
/// Creates a new instance of [LazyLoadScrollView]. The parameter [child]
/// must be supplied and not null.
const LazyLoadScrollView({
Key key,
@required this.child,
@@ -17,7 +17,7 @@ class LazyLoadScrollView extends StatefulWidget {
this.onPageScrollEnd,
this.onInBetweenOfPage,
this.scrollOffset = 100,
}) : assert(child != null),
}) : assert(child != null, 'Parameter child should not be null'),
super(key: key);
/// The [Widget] that this widget watches for changes on
@@ -46,18 +46,17 @@ class LazyLoadScrollView extends StatefulWidget {
}
class _LazyLoadScrollViewState extends State<LazyLoadScrollView> {
_LoadingStatus _loadMoreStatus = _LoadingStatus.STABLE;
double _scrollPosition = 0.0;
_LoadingStatus _loadMoreStatus = _LoadingStatus.stable;
double _scrollPosition = 0;
@override
Widget build(BuildContext context) {
return NotificationListener(
child: widget.child,
onNotification: _onNotification,
);
}
Widget build(BuildContext context) =>
NotificationListener<ScrollNotification>(
onNotification: _onNotification,
child: widget.child,
);
bool _onNotification(Notification notification) {
bool _onNotification(ScrollNotification notification) {
if (notification is ScrollStartNotification) {
if (widget.onPageScrollStart != null) {
widget.onPageScrollStart();
@@ -74,7 +73,7 @@ class _LazyLoadScrollViewState extends State<LazyLoadScrollView> {
final pixels = notification.metrics.pixels;
final maxScrollExtent = notification.metrics.maxScrollExtent;
final minScrollExtent = notification.metrics.minScrollExtent;
final scrollOffset = widget.scrollOffset;
final scrollOffset = widget.scrollOffset ?? 0;
if (pixels > (minScrollExtent + scrollOffset) &&
pixels < (maxScrollExtent - scrollOffset)) {
@@ -88,26 +87,19 @@ class _LazyLoadScrollViewState extends State<LazyLoadScrollView> {
final extentAfter = notification.metrics.extentAfter;
final scrollingDown = _scrollPosition < pixels;
if (scrollOffset == null || scrollOffset == 0) {
if (extentAfter == 0) {
if (scrollingDown) {
if (extentAfter <= scrollOffset) {
_onEndOfPage();
}
if (extentBefore == 0) {
_onStartOfPage();
return true;
}
} else {
if (scrollingDown) {
if (extentAfter <= scrollOffset) {
_onEndOfPage();
}
} else {
if (extentBefore <= scrollOffset) {
_onStartOfPage();
}
if (extentBefore <= scrollOffset) {
_onStartOfPage();
return true;
}
}
_scrollPosition = pixels;
return true;
}
if (notification is OverscrollNotification) {
if (notification.overscroll > 0) {
@@ -122,22 +114,22 @@ class _LazyLoadScrollViewState extends State<LazyLoadScrollView> {
}
void _onEndOfPage() {
if (_loadMoreStatus != null && _loadMoreStatus == _LoadingStatus.STABLE) {
_loadMoreStatus = _LoadingStatus.LOADING;
if (_loadMoreStatus != null && _loadMoreStatus == _LoadingStatus.stable) {
if (widget.onEndOfPage != null) {
_loadMoreStatus = _LoadingStatus.loading;
widget.onEndOfPage().whenComplete(() {
_loadMoreStatus = _LoadingStatus.STABLE;
_loadMoreStatus = _LoadingStatus.stable;
});
}
}
}
void _onStartOfPage() {
if (_loadMoreStatus != null && _loadMoreStatus == _LoadingStatus.STABLE) {
_loadMoreStatus = _LoadingStatus.LOADING;
if (_loadMoreStatus != null && _loadMoreStatus == _LoadingStatus.stable) {
if (widget.onStartOfPage != null) {
_loadMoreStatus = _LoadingStatus.loading;
widget.onStartOfPage().whenComplete(() {
_loadMoreStatus = _LoadingStatus.STABLE;
_loadMoreStatus = _LoadingStatus.stable;
});
}
}
@@ -1,12 +1,15 @@
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter_core/src/stream_channel.dart';
import 'package:stream_chat_flutter_core/src/typedef.dart';
import 'stream_channel.dart';
/// [MessageListCore] is a simplified class that allows fetching a list of messages while exposing UI builders.
/// [MessageListCore] is a simplified class that allows fetching a list of
/// messages while exposing UI builders.
///
/// A [MessageListController] is used to paginate data.
///
/// ```dart
@@ -50,12 +53,14 @@ import 'stream_channel.dart';
/// ```
///
///
/// Make sure to have a [StreamChannel] ancestor in order to provide the information about the channels.
/// Make sure to have a [StreamChannel] ancestor in order to provide the
/// information about the channels.
///
/// The widget uses a [ListView.custom] to render the list of channels.
///
class MessageListCore extends StatefulWidget {
/// Instantiate a new [MessageListView].
MessageListCore({
const MessageListCore({
Key key,
@required this.loadingBuilder,
@required this.emptyBuilder,
@@ -65,10 +70,16 @@ class MessageListCore extends StatefulWidget {
this.parentMessage,
this.messageListController,
this.messageFilter,
}) : assert(loadingBuilder != null),
assert(emptyBuilder != null),
assert(messageListBuilder != null),
assert(errorWidgetBuilder != null),
}) : assert(loadingBuilder != null, 'loadingBuilder should not be null'),
assert(emptyBuilder != null, 'emptyBuilder should not be null'),
assert(
messageListBuilder != null,
'messageListBuilder should not be null',
),
assert(
errorWidgetBuilder != null,
'errorWidgetBuilder should not be null',
),
super(key: key);
/// A [MessageListController] allows pagination.
@@ -84,12 +95,15 @@ class MessageListCore extends StatefulWidget {
/// Function used to build an empty widget
final WidgetBuilder emptyBuilder;
/// Callback triggered when an error occurs while performing the given request.
/// This parameter can be used to display an error message to users in the event
/// of a connection failure.
/// Callback triggered when an error occurs while performing the given
/// request.
///
/// This parameter can be used to display an error message to users in the
/// event of a connection failure.
final ErrorBuilder errorWidgetBuilder;
/// If true will show a scroll to bottom message when there are new messages and the scroll offset is not zero
/// If true will show a scroll to bottom message when there are new messages
/// and the scroll offset is not zero.
final bool showScrollToBottom;
/// If the current message belongs to a `thread`, this property represents the
@@ -100,32 +114,28 @@ class MessageListCore extends StatefulWidget {
final bool Function(Message) messageFilter;
@override
_MessageListCoreState createState() => _MessageListCoreState();
MessageListCoreState createState() => MessageListCoreState();
}
class _MessageListCoreState extends State<MessageListCore> {
StreamChannelState streamChannel;
/// The current state of the [MessageListCore].
class MessageListCoreState extends State<MessageListCore> {
StreamChannelState _streamChannel;
bool get _upToDate => streamChannel.channel.state.isUpToDate;
bool get _upToDate => _streamChannel.channel.state.isUpToDate;
bool get _isThreadConversation => widget.parentMessage != null;
OwnUser get _currentUser => streamChannel.channel.client.state.user;
OwnUser get _currentUser => _streamChannel.channel.client.state.user;
int initialIndex;
double initialAlignment;
List<Message> messages = <Message>[];
bool initialMessageHighlightComplete = false;
var _messages = <Message>[];
@override
Widget build(BuildContext context) {
final messagesStream = _isThreadConversation
? streamChannel.channel.state.threadsStream
? _streamChannel.channel.state.threadsStream
.where((threads) => threads.containsKey(widget.parentMessage.id))
.map((threads) => threads[widget.parentMessage.id])
: streamChannel.channel.state?.messagesStream;
: _streamChannel.channel.state?.messagesStream;
bool defaultFilter(Message m) {
final isMyMessage = m.user.id == _currentUser.id;
@@ -138,10 +148,10 @@ class _MessageListCoreState extends State<MessageListCore> {
stream: messagesStream?.map((messages) =>
messages?.where(widget.messageFilter ?? defaultFilter)?.toList()),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return widget.loadingBuilder(context);
} else if (snapshot.hasError) {
if (snapshot.hasError) {
return widget.errorWidgetBuilder(context, snapshot.error);
} else if (!snapshot.hasData) {
return widget.loadingBuilder(context);
} else {
final messageList = snapshot.data?.reversed?.toList() ?? [];
if (messageList.isEmpty && !_isThreadConversation) {
@@ -149,29 +159,32 @@ class _MessageListCoreState extends State<MessageListCore> {
return widget.emptyBuilder(context);
}
} else {
messages = messageList;
_messages = messageList;
}
return widget.messageListBuilder(context, messages);
return widget.messageListBuilder(context, _messages);
}
},
);
}
/// Fetches more messages with updated pagination and updates the widget.
///
/// Optionally pass the fetch direction, defaults to [QueryDirection.bottom]
Future<void> paginateData(
{QueryDirection direction = QueryDirection.bottom}) {
if (!_isThreadConversation) {
return streamChannel.queryMessages(direction: direction);
return _streamChannel.queryMessages(direction: direction);
} else {
return streamChannel.getReplies(widget.parentMessage.id);
return _streamChannel.getReplies(widget.parentMessage.id);
}
}
@override
void initState() {
streamChannel = StreamChannel.of(context);
_streamChannel = StreamChannel.of(context);
if (_isThreadConversation) {
streamChannel.getReplies(widget.parentMessage.id);
_streamChannel.getReplies(widget.parentMessage.id);
}
if (widget.messageListController != null) {
@@ -184,7 +197,7 @@ class _MessageListCoreState extends State<MessageListCore> {
@override
void dispose() {
if (!_upToDate) {
streamChannel.reloadChannel();
_streamChannel.reloadChannel();
}
super.dispose();
}
@@ -193,5 +206,5 @@ class _MessageListCoreState extends State<MessageListCore> {
/// Controller used for paginating data in [ChannelListView]
class MessageListController {
/// Call this function to load further data
Function({QueryDirection direction}) paginateData;
Future<void> Function({QueryDirection direction}) paginateData;
}
@@ -1,8 +1,7 @@
import 'package:flutter/material.dart';
import 'package:rxdart/rxdart.dart';
import 'package:stream_chat/stream_chat.dart';
import 'stream_chat_core.dart';
import 'package:stream_chat_flutter_core/src/stream_chat_core.dart';
/// [MessageSearchBloc] is used to manage a list of messages with pagination.
/// This class can be used to load messages, perform queries, etc.
@@ -16,7 +15,7 @@ class MessageSearchBloc extends StatefulWidget {
const MessageSearchBloc({
Key key,
@required this.child,
}) : assert(child != null),
}) : assert(child != null, 'Parameter child should not be null.'),
super(key: key);
/// The widget child
@@ -49,17 +48,16 @@ class MessageSearchBlocState extends State<MessageSearchBloc>
Stream<List<GetMessageResponse>> get messagesStream =>
_messageResponses.stream;
final BehaviorSubject<List<GetMessageResponse>> _messageResponses =
BehaviorSubject();
final _messageResponses = BehaviorSubject<List<GetMessageResponse>>();
final BehaviorSubject<bool> _queryMessagesLoadingController =
BehaviorSubject.seeded(false);
final _queryMessagesLoadingController = BehaviorSubject.seeded(false);
/// The stream notifying the state of queryUsers call
Stream<bool> get queryMessagesLoading =>
_queryMessagesLoadingController.stream;
/// Calls [StreamChatClient.search] updating [messageResponses] stream
/// Calls [StreamChatClient.search] updating
/// [messagesStream] and [queryMessagesLoading] stream
Future<void> search({
Map<String, dynamic> filter,
Map<String, dynamic> messageFilter,
@@ -67,33 +65,13 @@ class MessageSearchBlocState extends State<MessageSearchBloc>
String query,
PaginationParams pagination,
}) async {
_messageResponses.add(null);
try {
final messages = await _search(
filter: filter,
messageFilter: messageFilter,
sort: sort,
query: query,
pagination: pagination,
);
_messageResponses.add(messages.results);
} catch (err, stk) {
_messageResponses.addError(err, stk);
}
}
final client = StreamChatCore.of(context).client;
/// Calls [StreamChatClient.search] updating [queryMessagesLoading] stream
Future<void> loadMore({
Map<String, dynamic> filter,
Map<String, dynamic> messageFilter,
List<SortOption> sort,
String query,
PaginationParams pagination,
}) async {
if (_queryMessagesLoadingController.value == true) {
return;
if (_queryMessagesLoadingController.value == true) return;
if (_messageResponses.hasValue) {
_queryMessagesLoadingController.add(true);
}
_queryMessagesLoadingController.add(true);
try {
final clear = pagination == null ||
pagination.offset == null ||
@@ -101,12 +79,12 @@ class MessageSearchBlocState extends State<MessageSearchBloc>
final oldMessages = List<GetMessageResponse>.from(messageResponses ?? []);
final messages = await _search(
filter: filter,
messageFilter: messageFilter,
final messages = await client.search(
filter,
sort: sort,
query: query,
pagination: pagination,
paginationParams: pagination,
messageFilters: messageFilter,
);
if (clear) {
@@ -115,30 +93,18 @@ class MessageSearchBlocState extends State<MessageSearchBloc>
final temp = oldMessages + messages.results;
_messageResponses.add(temp);
}
_queryMessagesLoadingController.add(false);
} catch (err, stackTrace) {
_queryMessagesLoadingController.addError(err, stackTrace);
if (_messageResponses.hasValue && _queryMessagesLoadingController.value) {
_queryMessagesLoadingController.add(false);
}
} catch (e, stk) {
if (_messageResponses.hasValue) {
_queryMessagesLoadingController.addError(e, stk);
} else {
_messageResponses.addError(e, stk);
}
}
}
Future<SearchMessagesResponse> _search({
Map<String, dynamic> filter,
Map<String, dynamic> messageFilter,
List<SortOption> sort,
String query,
PaginationParams pagination,
}) {
final client = StreamChatCore.of(context).client;
return client.search(
filter,
sort: sort,
query: query,
paginationParams: pagination,
messageFilters: messageFilter,
);
}
@override
Widget build(BuildContext context) {
super.build(context);
@@ -1,13 +1,15 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.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/src/typedef.dart';
import 'message_search_bloc.dart';
///
/// [MessageSearchListCore] is a simplified class that allows searching for messages across channels while exposing UI builders.
/// A [MessageSearchListController] is used to load and paginate data.
/// [MessageSearchListCore] is a simplified class that allows searching for
/// messages across channels while exposing UI builders.
/// A [MessageSearchListController] is used to load and paginate data.
///
/// ```dart
/// class MessageSearchPage extends StatelessWidget {
@@ -28,7 +30,8 @@ import 'message_search_bloc.dart';
/// }
/// ```
///
/// Make sure to have a [MessageSearchBloc] ancestor in order to provide the information about the messages.
/// Make sure to have a [MessageSearchBloc] ancestor in order to provide the
/// information about the messages.
/// The widget uses a [ListView.separated] to render the list of messages.
///
class MessageSearchListCore extends StatefulWidget {
@@ -50,14 +53,16 @@ class MessageSearchListCore extends StatefulWidget {
this.paginationParams,
this.messageFilters,
this.messageSearchListController,
}) : assert(emptyBuilder != null),
assert(errorBuilder != null),
assert(loadingBuilder != null),
assert(childBuilder != null),
}) : assert(emptyBuilder != null, 'emptyBuilder should not be null'),
assert(errorBuilder != null, 'errorBuilder should not be null'),
assert(loadingBuilder != null, 'loadingBuilder should not be null'),
assert(childBuilder != null, 'childBuilder should not be null'),
super(key: key);
/// A [MessageSearchListController] allows reloading and pagination.
/// Use [MessageSearchListController.loadData] and [MessageSearchListController.paginateData] respectively for reloading and pagination.
/// Use [MessageSearchListController.loadData] and
/// [MessageSearchListController.paginateData] respectively for reloading and
/// pagination.
final MessageSearchListController messageSearchListController;
/// Message String to search on
@@ -69,9 +74,10 @@ class MessageSearchListCore extends StatefulWidget {
final Map<String, dynamic> filters;
/// The sorting used for the channels matching the filters.
/// Sorting is based on field and direction, multiple sorting options can be provided.
/// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count.
/// Direction can be ascending or descending.
/// Sorting is based on field and direction, multiple sorting options can be
/// provided.
/// You can sort based on last_updated, last_message_at, updated_at, created_
/// at or member_count. Direction can be ascending or descending.
final List<SortOption> sortOptions;
/// Pagination parameters
@@ -98,22 +104,15 @@ class MessageSearchListCore extends StatefulWidget {
final WidgetBuilder loadingBuilder;
@override
_MessageSearchListCoreState createState() => _MessageSearchListCoreState();
MessageSearchListCoreState createState() => MessageSearchListCoreState();
}
class _MessageSearchListCoreState extends State<MessageSearchListCore> {
/// The current state of the [MessageSearchListCore].
class MessageSearchListCoreState extends State<MessageSearchListCore> {
@override
void initState() {
super.initState();
final messageSearchBloc = MessageSearchBloc.of(context);
messageSearchBloc.search(
filter: widget.filters,
sort: widget.sortOptions,
query: widget.messageQuery,
pagination: widget.paginationParams,
messageFilter: widget.messageFilters,
);
void didChangeDependencies() {
super.didChangeDependencies();
loadData();
if (widget.messageSearchListController != null) {
widget.messageSearchListController.loadData = loadData;
widget.messageSearchListController.paginateData = paginateData;
@@ -126,37 +125,28 @@ class _MessageSearchListCoreState extends State<MessageSearchListCore> {
return _buildListView(messageSearchBloc);
}
Widget _buildListView(MessageSearchBlocState messageSearchBloc) {
return StreamBuilder<List<GetMessageResponse>>(
stream: messageSearchBloc.messagesStream,
builder: (context, snapshot) {
if (snapshot.hasError) {
if (snapshot.error is Error) {
print((snapshot.error as Error).stackTrace);
Widget _buildListView(MessageSearchBlocState messageSearchBloc) =>
StreamBuilder<List<GetMessageResponse>>(
stream: messageSearchBloc.messagesStream,
builder: (context, snapshot) {
if (snapshot.hasError) {
return widget.errorBuilder(context, snapshot.error);
}
if (!snapshot.hasData) {
return widget.loadingBuilder(context);
}
final items = snapshot.data;
if (items.isEmpty) {
return widget.emptyBuilder(context);
}
return widget.childBuilder(snapshot.data);
},
);
return widget.errorBuilder(context, snapshot.error);
}
if (!snapshot.hasData) {
return widget.loadingBuilder(context);
}
final items = snapshot.data;
if (items.isEmpty) {
return widget.emptyBuilder(context);
}
return widget.childBuilder(snapshot.data);
},
);
}
void loadData() {
/// Fetches initial messages and updates the widget
Future<void> loadData() {
final messageSearchBloc = MessageSearchBloc.of(context);
messageSearchBloc.search(
return messageSearchBloc.search(
filter: widget.filters,
sort: widget.sortOptions,
query: widget.messageQuery,
@@ -165,10 +155,10 @@ class _MessageSearchListCoreState extends State<MessageSearchListCore> {
);
}
void paginateData() {
/// Fetches more messages with updated pagination and updates the widget
Future<void> paginateData() {
final messageSearchBloc = MessageSearchBloc.of(context);
messageSearchBloc.loadMore(
return messageSearchBloc.search(
filter: widget.filters,
sort: widget.sortOptions,
pagination: widget.paginationParams.copyWith(
@@ -184,19 +174,12 @@ class _MessageSearchListCoreState extends State<MessageSearchListCore> {
super.didUpdateWidget(oldWidget);
if (widget.filters?.toString() != oldWidget.filters?.toString() ||
jsonEncode(widget.sortOptions) != jsonEncode(oldWidget.sortOptions) ||
widget.paginationParams?.toJson()?.toString() !=
oldWidget.paginationParams?.toJson()?.toString() ||
widget.messageQuery?.toString() != oldWidget.messageQuery?.toString() ||
widget.messageFilters?.toString() !=
oldWidget.messageFilters?.toString()) {
final messageSearchBloc = MessageSearchBloc.of(context);
messageSearchBloc.search(
filter: widget.filters,
sort: widget.sortOptions,
query: widget.messageQuery,
pagination: widget.paginationParams,
messageFilter: widget.messageFilters,
);
oldWidget.messageFilters?.toString() ||
widget.paginationParams?.toJson()?.toString() !=
oldWidget.paginationParams?.toJson()?.toString()) {
loadData();
}
}
}
@@ -204,8 +187,8 @@ class _MessageSearchListCoreState extends State<MessageSearchListCore> {
/// Controller used for paginating data in [ChannelListView]
class MessageSearchListController {
/// Call this function to reload data
VoidCallback loadData;
AsyncCallback loadData;
/// Call this function to load further data
VoidCallback paginateData;
AsyncCallback paginateData;
}
@@ -1,5 +1,6 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:rxdart/rxdart.dart';
import 'package:stream_chat/stream_chat.dart';
@@ -25,8 +26,8 @@ class StreamChannel extends StatefulWidget {
@required this.channel,
this.showLoading = true,
this.initialMessageId,
}) : assert(child != null),
assert(channel != null),
}) : assert(child != null, 'Child should not be null'),
assert(channel != null, 'Channel should not be null'),
super(key: key);
// ignore: public_member_api_docs
@@ -49,7 +50,8 @@ class StreamChannel extends StatefulWidget {
if (streamChannelState == null) {
throw Exception(
'You must have a StreamChannel widget at the top of your widget tree');
'You must have a StreamChannel widget at the top of your widget tree',
);
}
return streamChannelState;
@@ -204,17 +206,15 @@ class StreamChannelState extends State<StreamChannel> {
int before = 20,
int after = 20,
bool preferOffline = false,
}) {
return queryAtMessage(
messageId: messageId,
before: before,
after: after,
preferOffline: preferOffline,
);
}
}) =>
_queryAtMessage(
messageId: messageId,
before: before,
after: after,
preferOffline: preferOffline,
);
///
Future<void> queryAtMessage({
Future<void> _queryAtMessage({
String messageId,
int before = 20,
int after = 20,
@@ -254,15 +254,14 @@ class StreamChannelState extends State<StreamChannel> {
String messageId, {
int limit = 20,
bool preferOffline = false,
}) {
return channel.query(
messagesPagination: PaginationParams(
lessThan: messageId,
limit: limit,
),
preferOffline: preferOffline,
);
}
}) =>
channel.query(
messagesPagination: PaginationParams(
lessThan: messageId,
limit: limit,
),
preferOffline: preferOffline,
);
///
Future<ChannelState> queryAfterMessage(
@@ -297,7 +296,7 @@ class StreamChannelState extends State<StreamChannel> {
}
/// Reloads the channel with latest message
Future<void> reloadChannel() => queryAtMessage(before: 30);
Future<void> reloadChannel() => _queryAtMessage(before: 30);
List<Future<bool>> _futures;
@@ -305,8 +304,7 @@ class StreamChannelState extends State<StreamChannel> {
try {
await loadChannelAtMessage(initialMessageId);
return true;
} catch (e, stk) {
print('Error: $e\nStack: $stk');
} catch (_) {
rethrow;
}
}
@@ -349,9 +347,6 @@ class StreamChannelState extends State<StreamChannel> {
],
builder: (context, snapshot) {
if (snapshot.hasError) {
if (snapshot.error is Error) {
print((snapshot.error as Error).stackTrace);
}
var message = snapshot.error.toString();
if (snapshot.error is DioError) {
final dioError = snapshot.error as DioError;
@@ -361,14 +356,13 @@ class StreamChannelState extends State<StreamChannel> {
message = 'Check your connection and retry';
}
}
return Center(
child: Text(message),
);
return Center(child: Text(message));
}
final initialized = snapshot.data[0];
// ignore: avoid_bool_literals_in_conditional_expressions
final dataLoaded = initialMessageId == null ? true : snapshot.data[1];
if (widget.showLoading && (!initialized || !dataLoaded)) {
return Center(
return const Center(
child: CircularProgressIndicator(),
);
}
@@ -3,13 +3,12 @@ import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'typedef.dart';
import 'package:stream_chat_flutter_core/src/typedef.dart';
/// Widget used to provide information about the chat to the widget tree.
/// This Widget is used to react to life cycle changes and system updates.
/// When the app goes into the background, the websocket connection is kept alive
/// for two minutes before being terminated.
/// When the app goes into the background, the websocket connection is kept
/// alive for two minutes before being terminated.
///
/// Conversely, when app is resumed or restarted, a new connection is initiated.
///
@@ -36,16 +35,16 @@ import 'typedef.dart';
class StreamChatCore extends StatefulWidget {
/// Constructor used for creating a new instance of [StreamChatCore].
///
/// [StreamChatCore] is a stateful widget which reacts to system events and updates
/// Stream's connection status accordingly.
StreamChatCore({
/// [StreamChatCore] is a stateful widget which reacts to system events and
/// updates Stream's connection status accordingly.
const StreamChatCore({
Key key,
@required this.client,
@required this.child,
this.onBackgroundEventReceived,
this.backgroundKeepAlive = const Duration(minutes: 1),
}) : assert(client != null),
assert(child != null),
}) : assert(client != null, 'Stream Chat Client should not be null'),
assert(child != null, 'Child should not be null'),
super(key: key);
/// Instance of Stream Chat Client containing information about the current
@@ -55,7 +54,8 @@ class StreamChatCore extends StatefulWidget {
/// Widget descendant.
final Widget child;
/// The amount of time that will pass before disconnecting the client in the background
/// The amount of time that will pass before disconnecting the client in
/// the background
final Duration backgroundKeepAlive;
/// Handler called whenever the [client] receives a new [Event] while the app
@@ -90,15 +90,13 @@ class StreamChatCoreState extends State<StreamChatCore>
Timer _disconnectTimer;
@override
Widget build(BuildContext context) {
return widget.child;
}
Widget build(BuildContext context) => widget.child;
/// The current user
User get user => widget.client.state.user;
User get user => client.state?.user;
/// The current user as a stream
Stream<User> get userStream => widget.client.state.userStream;
Stream<User> get userStream => client.state?.userStream;
@override
void initState() {
@@ -110,21 +108,25 @@ class StreamChatCoreState extends State<StreamChatCore>
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (client.state?.user != null) {
if (user != null) {
if (state == AppLifecycleState.paused) {
if (widget.onBackgroundEventReceived != null) {
_eventSubscription =
client.on().listen(widget.onBackgroundEventReceived);
_disconnectTimer = Timer(
widget.backgroundKeepAlive,
client.disconnect,
);
} else {
if (widget.onBackgroundEventReceived == null) {
client.disconnect();
return;
}
_eventSubscription = client.on().listen(
widget.onBackgroundEventReceived,
);
void onTimerComplete() {
_eventSubscription.cancel();
client.disconnect();
}
_disconnectTimer = Timer(widget.backgroundKeepAlive, onTimerComplete);
} else if (state == AppLifecycleState.resumed) {
_eventSubscription?.cancel();
if (_disconnectTimer?.isActive == true) {
_eventSubscription.cancel();
_disconnectTimer.cancel();
} else {
if (client.wsConnectionStatus == ConnectionStatus.disconnected) {
@@ -2,8 +2,8 @@ import 'package:flutter/widgets.dart';
import 'package:stream_chat/stream_chat.dart';
/// A signature for a callback which exposes an error and returns a function.
/// This Callback can be used in cases where an API failure occurs and the widget
/// is unable to render data.
/// This Callback can be used in cases where an API failure occurs and the
/// widget is unable to render data.
typedef ErrorBuilder = Widget Function(BuildContext context, Object error);
/// A Signature for a handler function which will expose a [event].
@@ -1,11 +1,13 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter_core/src/users_bloc.dart';
///
/// [UserListCore] is a simplified class that allows fetching users while exposing UI builders.
/// [UserListCore] is a simplified class that allows fetching users while
/// exposing UI builders.
/// A [UserListController] is used to load and paginate data.
///
/// ```dart
@@ -50,34 +52,35 @@ import 'package:stream_chat_flutter_core/src/users_bloc.dart';
/// [UsersBloc] must be the ancestor of this widget. This is necessary since
/// [UserListCore] depends on functionality contained within [UsersBloc].
///
/// The parameters [listBuilder], [loadingBuilder], [emptyBuilder] and [errorBuilder] must all be supplied
/// and not null.
/// The parameters [listBuilder], [loadingBuilder], [emptyBuilder] and
/// [errorBuilder] must all be supplied and not null.
class UserListCore extends StatefulWidget {
/// Instantiate a new [UserListCore]
const UserListCore({
Key key,
@required this.errorBuilder,
@required this.emptyBuilder,
@required this.loadingBuilder,
@required this.listBuilder,
Key key,
this.filter,
this.options,
this.sort,
this.pagination,
this.groupAlphabetically = false,
this.userListController,
}) : assert(errorBuilder != null),
assert(emptyBuilder != null),
assert(loadingBuilder != null),
assert(listBuilder != null),
}) : assert(errorBuilder != null, ''),
assert(emptyBuilder != null, ''),
assert(loadingBuilder != null, ''),
assert(listBuilder != null, ''),
super(key: key);
/// A [UserListController] allows reloading and pagination.
/// Use [UserListController.loadData] and [UserListController.paginateData] respectively for reloading and pagination.
/// Use [UserListController.loadData] and [UserListController.paginateData]
/// respectively for reloading and pagination.
final UserListController userListController;
/// The builder that will be used in case of error
final Widget Function(Error error) errorBuilder;
final Widget Function(Object error) errorBuilder;
/// The builder that will be used to build the list
final Widget Function(BuildContext context, List<ListItem> users) listBuilder;
@@ -100,9 +103,9 @@ class UserListCore extends StatefulWidget {
final Map<String, dynamic> options;
/// The sorting used for the channels matching the filters.
/// Sorting is based on field and direction, multiple sorting options can be provided.
/// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count.
/// Direction can be ascending or descending.
/// Sorting is based on field and direction, multiple sorting options can be
/// provided. You can sort based on last_updated, last_message_at, updated_at,
/// created_at or member_count. Direction can be ascending or descending.
final List<SortOption> sort;
/// Pagination parameters
@@ -117,22 +120,16 @@ class UserListCore extends StatefulWidget {
final bool groupAlphabetically;
@override
_UserListCoreState createState() => _UserListCoreState();
UserListCoreState createState() => UserListCoreState();
}
class _UserListCoreState extends State<UserListCore>
/// The current state of the [UserListCore].
class UserListCoreState extends State<UserListCore>
with WidgetsBindingObserver {
@override
void initState() {
super.initState();
final usersBloc = UsersBloc.of(context);
usersBloc.queryUsers(
filter: widget.filter,
sort: widget.sort,
pagination: widget.pagination,
options: widget.options,
);
void didChangeDependencies() {
super.didChangeDependencies();
loadData();
if (widget.userListController != null) {
widget.userListController.loadData = loadData;
widget.userListController.paginateData = paginateData;
@@ -142,77 +139,64 @@ class _UserListCoreState extends State<UserListCore>
@override
Widget build(BuildContext context) {
final _usersBloc = UsersBloc.of(context);
return _buildListView(_usersBloc);
}
bool get isListAlreadySorted =>
bool get _isListAlreadySorted =>
widget.sort?.any((e) => e.field == 'name' && e.direction == 1) ?? false;
Stream<List<ListItem>> _buildUserStream(
UsersBlocState usersBlocState,
) {
return usersBlocState.usersStream.map(
(users) {
if (widget.groupAlphabetically) {
var temp = users;
if (!isListAlreadySorted) {
temp = users..sort((curr, next) => curr.name.compareTo(next.name));
) =>
usersBlocState.usersStream.map(
(users) {
if (widget.groupAlphabetically) {
var temp = users;
if (!_isListAlreadySorted) {
temp = users
..sort((curr, next) => curr.name.compareTo(next.name));
}
final groupedUsers = <String, List<User>>{};
for (final e in temp) {
final alphabet = e.name[0]?.toUpperCase();
groupedUsers[alphabet] = [...groupedUsers[alphabet] ?? [], e];
}
final items = <ListItem>[];
for (final key in groupedUsers.keys) {
items
..add(ListHeaderItem(key))
..addAll(groupedUsers[key].map((e) => ListUserItem(e)));
}
return items;
}
final groupedUsers = <String, List<User>>{};
for (final e in temp) {
final alphabet = e.name[0]?.toUpperCase();
groupedUsers[alphabet] = [...groupedUsers[alphabet] ?? [], e];
}
final items = <ListItem>[];
for (final key in groupedUsers.keys) {
items.add(ListHeaderItem(key));
items.addAll(groupedUsers[key].map((e) => ListUserItem(e)));
}
return items;
}
return users.map((e) => ListUserItem(e)).toList();
},
);
}
return users.map((e) => ListUserItem(e)).toList();
},
);
StreamBuilder<List<ListItem>> _buildListView(
UsersBlocState usersBlocState,
) {
return StreamBuilder(
stream: _buildUserStream(usersBlocState),
builder: (context, snapshot) {
if (snapshot.hasError) {
if (snapshot.error is Error) {
print((snapshot.error as Error).stackTrace);
) =>
StreamBuilder(
stream: _buildUserStream(usersBlocState),
builder: (context, snapshot) {
if (snapshot.hasError) {
return widget.errorBuilder(snapshot.error);
}
if (!snapshot.hasData) {
return widget.loadingBuilder(context);
}
final items = snapshot.data;
if (items.isEmpty) {
return widget.emptyBuilder(context);
}
return widget.listBuilder(context, items);
},
);
return widget.errorBuilder(snapshot.error);
}
if (!snapshot.hasData) {
return widget.loadingBuilder(context);
}
final items = snapshot.data;
if (items.isEmpty) {
return widget.emptyBuilder(context);
}
if (items.isEmpty) {
return widget.emptyBuilder(context);
}
return widget.listBuilder(context, items);
},
);
}
void loadData() {
// ignore: public_member_api_docs
Future<void> loadData() {
final _usersBloc = UsersBloc.of(context);
_usersBloc.queryUsers(
return _usersBloc.queryUsers(
filter: widget.filter,
sort: widget.sort,
pagination: widget.pagination,
@@ -220,10 +204,10 @@ class _UserListCoreState extends State<UserListCore>
);
}
void paginateData() {
// ignore: public_member_api_docs
Future<void> paginateData() {
final _usersBloc = UsersBloc.of(context);
_usersBloc.queryUsers(
return _usersBloc.queryUsers(
filter: widget.filter,
sort: widget.sort,
pagination: widget.pagination.copyWith(
@@ -238,29 +222,23 @@ class _UserListCoreState extends State<UserListCore>
super.didUpdateWidget(oldWidget);
if (widget.filter?.toString() != oldWidget.filter?.toString() ||
jsonEncode(widget.sort) != jsonEncode(oldWidget.sort) ||
widget.options?.toString() != oldWidget.options?.toString() ||
widget.pagination?.toJson()?.toString() !=
oldWidget.pagination?.toJson()?.toString() ||
widget.options?.toString() != oldWidget.options?.toString()) {
final usersBloc = UsersBloc.of(context);
usersBloc.queryUsers(
filter: widget.filter,
sort: widget.sort,
pagination: widget.pagination,
options: widget.options,
);
oldWidget.pagination?.toJson()?.toString()) {
loadData();
}
}
}
/// Represents an item in a the user stream list.
/// Header items are prefixed with the key `HEADER` While users are prefixed with
/// `USER`.
/// Header items are prefixed with the key `HEADER` While users are prefixed
/// with `USER`.
abstract class ListItem {
// ignore: public_member_api_docs
/// Unique key per list item
String get key {
if (this is ListHeaderItem) {
final header = (this as ListHeaderItem).heading;
return 'HEADER-$header';
return 'HEADER-${header.toLowerCase()}';
}
if (this is ListUserItem) {
final user = (this as ListUserItem).user;
@@ -269,7 +247,8 @@ abstract class ListItem {
return null;
}
// ignore: public_member_api_docs
/// Helper function to build widget based on ListItem type
// ignore: missing_return
Widget when({
@required Widget Function(String heading) headerItem,
@required Widget Function(User user) userItem,
@@ -280,33 +259,32 @@ abstract class ListItem {
if (this is ListUserItem) {
return userItem((this as ListUserItem).user);
}
return SizedBox();
}
}
// ignore: public_member_api_docs
/// Header Item
class ListHeaderItem extends ListItem {
// ignore: public_member_api_docs
final String heading;
// ignore: public_member_api_docs
/// Constructs a new [ListHeaderItem]
ListHeaderItem(this.heading);
/// Heading used to build the item.
final String heading;
}
// ignore: public_member_api_docs
/// User Item
class ListUserItem extends ListItem {
// ignore: public_member_api_docs
final User user;
// ignore: public_member_api_docs
/// Constructs a new [ListUserItem]
ListUserItem(this.user);
/// [User] used to build the item.
final User user;
}
/// Controller used for paginating data in [ChannelListView]
class UserListController {
/// Call this function to reload data
VoidCallback loadData;
AsyncCallback loadData;
/// Call this function to load further data
VoidCallback paginateData;
AsyncCallback paginateData;
}
@@ -1,8 +1,8 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:rxdart/rxdart.dart';
import 'package:stream_chat/stream_chat.dart';
import 'stream_chat_core.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
/// Widget dedicated to the management of a users list with pagination.
///
@@ -14,9 +14,12 @@ class UsersBloc extends StatefulWidget {
/// Instantiate a new [UsersBloc]. The parameter [child] must be supplied and
/// not null.
const UsersBloc({
Key key,
@required this.child,
}) : assert(child != null),
Key key,
}) : assert(
child != null,
'When constructing a UsersBloc, the parameter '
'child should not be null.'),
super(key: key);
/// The widget child
@@ -48,10 +51,9 @@ class UsersBlocState extends State<UsersBloc>
/// The current users list as a stream
Stream<List<User>> get usersStream => _usersController.stream;
final BehaviorSubject<List<User>> _usersController = BehaviorSubject();
final _usersController = BehaviorSubject<List<User>>();
final BehaviorSubject<bool> _queryUsersLoadingController =
BehaviorSubject.seeded(false);
final _queryUsersLoadingController = BehaviorSubject.seeded(false);
/// The stream notifying the state of queryUsers call
Stream<bool> get queryUsersLoading => _queryUsersLoadingController.stream;
@@ -67,11 +69,12 @@ class UsersBlocState extends State<UsersBloc>
}) async {
final client = StreamChatCore.of(context).client;
if (client.state?.user == null ||
_queryUsersLoadingController.value == true) {
return;
if (_queryUsersLoadingController.value == true) return;
if (_usersController.hasValue) {
_queryUsersLoadingController.add(true);
}
_queryUsersLoadingController.add(true);
try {
final clear = pagination == null ||
pagination.offset == null ||
@@ -92,10 +95,15 @@ class UsersBlocState extends State<UsersBloc>
final temp = oldUsers + usersResponse.users;
_usersController.add(temp);
}
_queryUsersLoadingController.add(false);
} catch (err, stackTrace) {
_queryUsersLoadingController.addError(err, stackTrace);
if (_usersController.hasValue && _queryUsersLoadingController.value) {
_queryUsersLoadingController.add(false);
}
} catch (e, stk) {
if (_usersController.hasValue) {
_queryUsersLoadingController.addError(e, stk);
} else {
_usersController.addError(e, stk);
}
}
}
@@ -1,14 +1,15 @@
library stream_chat_flutter_core;
export 'src/channel_list_core.dart';
export 'package:stream_chat/stream_chat.dart';
export 'src/channel_list_core.dart' hide ChannelListCoreState;
export 'src/channels_bloc.dart';
export 'src/lazy_load_scroll_view.dart';
export 'src/message_list_core.dart';
export 'src/message_list_core.dart' hide MessageListCoreState;
export 'src/message_search_bloc.dart';
export 'src/message_search_list_core.dart';
export 'src/message_search_list_core.dart' hide MessageSearchListCoreState;
export 'src/stream_channel.dart';
export 'src/stream_chat_core.dart';
export 'src/user_list_core.dart';
export 'src/users_bloc.dart';
export 'src/typedef.dart';
export 'package:stream_chat/stream_chat.dart';
export 'src/user_list_core.dart' hide UserListCoreState;
export 'src/users_bloc.dart';
@@ -1,7 +1,7 @@
name: stream_chat_flutter_core
homepage: https://github.com/GetStream/stream-chat-flutter
description: Stream Chat official Flutter SDK Core. Build your own chat experience using Dart and Flutter.
version: 1.5.0
version: 1.5.1
repository: https://github.com/GetStream/stream-chat-flutter
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
@@ -10,15 +10,13 @@ environment:
flutter: ">=1.17.0"
dependencies:
stream_chat: ^1.5.0
flutter:
sdk: flutter
meta: ^1.2.4
rxdart: ^0.25.0
stream_chat: ^1.5.1
dev_dependencies:
mockito: ^4.1.4
flutter_test:
sdk: flutter
fake_async: ^1.1.0
pedantic: ^1.9.2
mockito: ^4.1.3
@@ -0,0 +1,504 @@
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: 3);
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),
pagination: pagination,
);
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(),
pagination: pagination,
);
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(),
pagination: pagination,
);
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(),
pagination: pagination,
);
final mockClient = MockClient();
when(mockClient.on(any, any, any, any)).thenAnswer((_) => Stream.empty());
final channels = _generateChannels(mockClient);
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: pagination,
)).thenAnswer((_) => Stream.value(channels));
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StreamChatCore(
client: mockClient,
child: ChannelsBloc(
child: channelListCore,
),
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(listWidgetKey), findsOneWidget);
expect(find.text(channels.map((e) => e.cid).join(',')), findsOneWidget);
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: pagination,
)).called(1);
final channelListCoreState = tester.state<ChannelListCoreState>(
find.byKey(channelListCoreKey),
);
final offset = channels.length;
final paginatedChannels = _generateChannels(mockClient, offset: offset);
final updatedPagination = pagination.copyWith(offset: offset);
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: updatedPagination,
)).thenAnswer((_) => Stream.value(paginatedChannels));
await channelListCoreState.paginateData();
await tester.pumpAndSettle();
expect(find.byKey(listWidgetKey), findsOneWidget);
expect(
find.text([
...channels,
...paginatedChannels,
].map((e) => e.cid).join(',')),
findsOneWidget,
);
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: updatedPagination,
)).called(1);
},
);
testWidgets(
'should rebuild ChannelListCore with updated widget data '
'on calling setState()',
(tester) async {
StateSetter _stateSetter;
int limit = pagination.limit;
const channelListCoreKey = Key('channelListCore');
const listWidgetKey = Key('listWidget');
ChannelListCore channelListCoreBuilder(int limit) => ChannelListCore(
key: channelListCoreKey,
listBuilder: (_, channels) {
return Container(
key: listWidgetKey,
child: Text(
channels.map((e) => e.cid).join(','),
),
);
},
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (BuildContext context, Object error) => Offstage(),
pagination: pagination.copyWith(limit: limit),
);
final mockClient = MockClient();
when(mockClient.on(any, any, any, any)).thenAnswer((_) => Stream.empty());
final channels = _generateChannels(mockClient);
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: pagination,
)).thenAnswer((_) => Stream.value(channels));
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StreamChatCore(
client: mockClient,
child: ChannelsBloc(
child: StatefulBuilder(builder: (context, stateSetter) {
// Assigning stateSetter for rebuilding ChannelListCore
_stateSetter = stateSetter;
return channelListCoreBuilder(limit);
}),
),
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(listWidgetKey), findsOneWidget);
expect(find.text(channels.map((e) => e.cid).join(',')), findsOneWidget);
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: pagination,
)).called(1);
// Rebuilding ChannelListCore with new pagination limit
_stateSetter(() => limit = 6);
final updatedChannels = _generateChannels(mockClient, count: limit);
final updatedPagination = pagination.copyWith(limit: limit);
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: updatedPagination,
)).thenAnswer((_) => Stream.value(updatedChannels));
await tester.pumpAndSettle();
expect(find.byKey(listWidgetKey), findsOneWidget);
expect(
find.text(updatedChannels.map((e) => e.cid).join(',')),
findsOneWidget,
);
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: updatedPagination,
)).called(1);
},
);
}
@@ -0,0 +1,847 @@
import 'dart:async';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'matchers/channel_matcher.dart';
import 'mocks.dart';
void main() {
List<Channel> _generateChannels(
StreamChatClient client, {
int count = 3,
int offset = 0,
}) {
return List.generate(
count,
(index) {
index = index + offset;
return Channel(
client,
'testType$index',
'testId$index',
{'extra_data_key': 'extra_data_value_$index'},
);
},
);
}
test(
'should throw assertion error if child is null',
() async {
const channelsBlocKey = Key('channelsBloc');
final channelsBloc = () => ChannelsBloc(
key: channelsBlocKey,
child: null,
);
expect(channelsBloc, throwsA(isA<AssertionError>()));
},
);
testWidgets(
'should throw if ChannelsBloc is used where StreamChat is not present in the widget tree',
(tester) async {
const channelsBlocKey = Key('channelsBloc');
const childKey = Key('child');
final channelsBloc = ChannelsBloc(
key: channelsBlocKey,
child: Offstage(key: childKey),
);
await tester.pumpWidget(channelsBloc);
expect(find.byKey(channelsBlocKey), findsNothing);
expect(find.byKey(childKey), findsNothing);
expect(tester.takeException(), isInstanceOf<Exception>());
},
);
testWidgets(
'should render ChannelsBloc if used with StreamChatCore as an ancestor',
(tester) async {
const channelsBlocKey = Key('channelsBloc');
const childKey = Key('child');
final channelsBloc = ChannelsBloc(
key: channelsBlocKey,
child: Offstage(key: childKey),
);
final mockClient = MockClient();
when(mockClient.on(any, any, any, any)).thenAnswer((_) => Stream.empty());
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: channelsBloc,
),
);
expect(find.byKey(channelsBlocKey), findsOneWidget);
expect(find.byKey(childKey), findsOneWidget);
},
);
testWidgets(
'channelsBlocState.queryChannels() should emit data through channelsStream',
(tester) async {
const channelsBlocKey = Key('channelsBloc');
const childKey = Key('child');
final channelsBloc = ChannelsBloc(
key: channelsBlocKey,
child: Builder(
key: childKey,
builder: (context) {
return Offstage();
},
),
);
final mockClient = MockClient();
when(mockClient.on(any, any, any, any)).thenAnswer((_) => Stream.empty());
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: channelsBloc,
),
);
final channelsBlocState = tester.state<ChannelsBlocState>(
find.byKey(channelsBlocKey),
);
final offlineChannels = _generateChannels(mockClient);
final onlineChannels = _generateChannels(mockClient, offset: 3);
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).thenAnswer(
(_) => Stream.fromIterable([offlineChannels, onlineChannels]),
);
channelsBlocState.queryChannels();
await expectLater(
channelsBlocState.channelsStream,
emitsInOrder([
isSameChannelListAs(offlineChannels),
isSameChannelListAs(onlineChannels),
]),
);
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).called(1);
},
);
testWidgets(
'channelsBlocState.channelsStream should emit error '
'if client.queryChannels() throws',
(tester) async {
const channelsBlocKey = Key('channelsBloc');
const childKey = Key('child');
final channelsBloc = ChannelsBloc(
key: channelsBlocKey,
child: Builder(
key: childKey,
builder: (context) {
return Offstage();
},
),
);
final mockClient = MockClient();
when(mockClient.on(any, any, any, any)).thenAnswer((_) => Stream.empty());
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: channelsBloc,
),
);
final channelsBlocState = tester.state<ChannelsBlocState>(
find.byKey(channelsBlocKey),
);
final error = 'Error! Error! Error!';
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).thenThrow(error);
channelsBlocState.queryChannels();
await expectLater(
channelsBlocState.channelsStream,
emitsError(error),
);
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).called(1);
},
);
testWidgets(
'calling channelsBlocState.queryChannels() again with an offset '
'should emit new data through channelsStream and also emit loading state '
'through queryChannelsLoading',
(tester) async {
const channelsBlocKey = Key('channelsBloc');
final channelsBloc = ChannelsBloc(
key: channelsBlocKey,
child: Offstage(),
);
final mockClient = MockClient();
when(mockClient.on(any, any, any, any)).thenAnswer((_) => Stream.empty());
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: channelsBloc,
),
);
final channelsBlocState = tester.state<ChannelsBlocState>(
find.byKey(channelsBlocKey),
);
final channels = _generateChannels(mockClient);
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).thenAnswer((_) => Stream.value(channels));
channelsBlocState.queryChannels();
await expectLater(
channelsBlocState.channelsStream,
emits(isSameChannelListAs(channels)),
);
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).called(1);
final offset = channels.length;
final paginationParams = PaginationParams(offset: offset);
final newChannels = _generateChannels(mockClient, offset: offset);
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: paginationParams,
)).thenAnswer(
(_) => Stream.value(newChannels),
);
channelsBlocState.queryChannels(paginationParams: paginationParams);
await Future.wait([
expectLater(
channelsBlocState.queryChannelsLoading,
emitsInOrder([true, false]),
),
expectLater(
channelsBlocState.channelsStream,
emits(isSameChannelListAs(channels + newChannels)),
),
]);
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: paginationParams,
)).called(1);
},
);
testWidgets(
'calling channelsBlocState.queryChannels() again with an offset '
'should emit error through queryChannelsLoading if '
'client.queryChannels() throws',
(tester) async {
const channelsBlocKey = Key('channelsBloc');
final channelsBloc = ChannelsBloc(
key: channelsBlocKey,
child: Offstage(),
);
final mockClient = MockClient();
when(mockClient.on(any, any, any, any)).thenAnswer((_) => Stream.empty());
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: channelsBloc,
),
);
final channelsBlocState = tester.state<ChannelsBlocState>(
find.byKey(channelsBlocKey),
);
final channels = _generateChannels(mockClient);
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).thenAnswer((_) => Stream.value(channels));
channelsBlocState.queryChannels();
await expectLater(
channelsBlocState.channelsStream,
emits(isSameChannelListAs(channels)),
);
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).called(1);
final offset = channels.length;
final paginationParams = PaginationParams(offset: offset);
final error = 'Error! Error! Error!';
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: paginationParams,
)).thenThrow(error);
channelsBlocState.queryChannels(paginationParams: paginationParams);
await expectLater(
channelsBlocState.queryChannelsLoading,
emitsError(error),
);
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: paginationParams,
)).called(1);
},
);
group('event controller test', () {
StreamController<Event> eventController;
setUp(() {
eventController = StreamController<Event>.broadcast();
});
testWidgets(
'channel should get hide when EventType.channelHidden event is received',
(tester) async {
final mockClient = MockClient();
const channelsBlocKey = Key('channelsBloc');
final channelsBloc = ChannelsBloc(
key: channelsBlocKey,
child: Offstage(),
);
when(mockClient.on(any, any, any, any))
.thenAnswer((_) => Stream.empty());
when(mockClient.on(
EventType.channelHidden,
)).thenAnswer((_) => eventController.stream);
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: channelsBloc,
),
);
final channelsBlocState = tester.state<ChannelsBlocState>(
find.byKey(channelsBlocKey),
);
final channels = _generateChannels(mockClient);
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).thenAnswer(
(_) => Stream.value(channels),
);
await channelsBlocState.queryChannels();
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).called(1);
final channelHiddenEvent = Event(
type: EventType.channelHidden,
cid: channels.first.cid,
);
eventController.add(channelHiddenEvent);
final newChannels = [...channels]
..removeWhere((it) => it.cid == channelHiddenEvent.cid);
await expectLater(
channelsBlocState.channelsStream,
emitsInOrder([
isSameChannelListAs(channels),
isSameChannelListAs(newChannels),
]),
);
verify(mockClient.on(EventType.channelHidden)).called(1);
},
);
testWidgets(
'channel should get removed when EventType.channelDeleted or '
'EventType.notificationRemovedFromChannel, event is received',
(tester) async {
final mockClient = MockClient();
const channelsBlocKey = Key('channelsBloc');
final channelsBloc = ChannelsBloc(
key: channelsBlocKey,
child: Offstage(),
);
when(mockClient.on(any, any, any, any))
.thenAnswer((_) => Stream.empty());
when(mockClient.on(
EventType.channelDeleted,
EventType.notificationRemovedFromChannel,
)).thenAnswer((_) => eventController.stream);
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: channelsBloc,
),
);
final channelsBlocState = tester.state<ChannelsBlocState>(
find.byKey(channelsBlocKey),
);
final channels = _generateChannels(mockClient);
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).thenAnswer(
(_) => Stream.value(channels),
);
await channelsBlocState.queryChannels();
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).called(1);
final channelDeletedOrNotificationRemovedEvent = Event(
channel: EventChannel(cid: channels.first.cid),
);
eventController.add(channelDeletedOrNotificationRemovedEvent);
final channelCid = channelDeletedOrNotificationRemovedEvent.channel.cid;
final newChannels = [...channels]
..removeWhere((it) => it.cid == channelCid);
await expectLater(
channelsBlocState.channelsStream,
emitsInOrder([
isSameChannelListAs(channels),
isSameChannelListAs(newChannels),
]),
);
verify(mockClient.on(
EventType.channelDeleted,
EventType.notificationRemovedFromChannel,
)).called(1);
},
);
testWidgets(
'event channel should be moved to top of the list if present when'
'EventType.messageNew event is received',
(tester) async {
final mockClient = MockClient();
const channelsBlocKey = Key('channelsBloc');
final channelsBloc = ChannelsBloc(
key: channelsBlocKey,
child: Offstage(),
);
when(mockClient.on(any, any, any, any))
.thenAnswer((_) => Stream.empty());
when(mockClient.on(
EventType.messageNew,
)).thenAnswer((_) => eventController.stream);
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: channelsBloc,
),
);
final channelsBlocState = tester.state<ChannelsBlocState>(
find.byKey(channelsBlocKey),
);
final channels = _generateChannels(mockClient);
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).thenAnswer(
(_) => Stream.value(channels),
);
await channelsBlocState.queryChannels();
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).called(1);
final messageNewEvent = Event(
type: EventType.messageNew,
cid: channels.last.cid,
);
eventController.add(messageNewEvent);
final channelCid = messageNewEvent.cid;
final index = channels.indexWhere((it) => it.cid == channelCid);
final updatedChannel = channels[index];
final newChannels = [...channels]
..removeAt(index)
..insert(0, updatedChannel);
await expectLater(
channelsBlocState.channelsStream,
emitsInOrder([
isSameChannelListAs(channels),
isSameChannelListAs(newChannels),
]),
);
verify(mockClient.on(EventType.messageNew)).called(1);
},
);
testWidgets(
'event channel should be moved to top of the list if present inside '
'hiddenChannels list and shouldAddChannel is true when '
'EventType.messageNew event is received',
(tester) async {
final hiddenChannelEventController = StreamController<Event>();
addTearDown(() {
hiddenChannelEventController.close();
});
final mockClient = MockClient();
final channels = _generateChannels(mockClient);
const channelsBlocKey = Key('channelsBloc');
final channelsBloc = ChannelsBloc(
key: channelsBlocKey,
child: Offstage(),
shouldAddChannel: (e) => channels.map((it) => it.cid).contains(e.cid),
);
when(mockClient.on(any, any, any, any))
.thenAnswer((_) => Stream.empty());
when(mockClient.on(
EventType.channelHidden,
)).thenAnswer((_) => hiddenChannelEventController.stream);
when(mockClient.on(
EventType.messageNew,
)).thenAnswer((_) => eventController.stream);
final messageNewEvent = Event(
type: EventType.messageNew,
cid: channels.last.cid,
);
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: channelsBloc,
),
);
final channelsBlocState = tester.state<ChannelsBlocState>(
find.byKey(channelsBlocKey),
);
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).thenAnswer(
(_) => Stream.value(channels),
);
await channelsBlocState.queryChannels();
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).called(1);
final channelHiddenEvent = Event(
type: EventType.channelHidden,
cid: channels.last.cid,
);
// Hiding the channel before passing messageNew event
hiddenChannelEventController.add(channelHiddenEvent);
final channelsAfterHiddenEvent = [...channels]..removeLast();
eventController.add(messageNewEvent);
final channelCid = messageNewEvent.cid;
final index = channels.indexWhere((it) => it.cid == channelCid);
final newChannels = [...channels]
..removeAt(index)
..insert(0, channels[index]);
await expectLater(
channelsBlocState.channelsStream,
emitsInOrder([
isSameChannelListAs(channels),
isSameChannelListAs(channelsAfterHiddenEvent),
isSameChannelListAs(newChannels),
]),
);
verify(mockClient.on(EventType.channelHidden)).called(1);
verify(mockClient.on(EventType.messageNew)).called(1);
},
);
testWidgets(
'event channel should be moved to top of the list if present inside '
'channel state and shouldAddChannel is true when '
'EventType.messageNew event is received',
(tester) async {
final mockClient = MockClient();
final channels = _generateChannels(mockClient);
final stateChannels = {
for (var c in _generateChannels(mockClient, offset: 5)) c.cid: c
};
const channelsBlocKey = Key('channelsBloc');
final channelsBloc = ChannelsBloc(
key: channelsBlocKey,
child: Offstage(),
shouldAddChannel: (_) => true,
);
when(mockClient.state.channels).thenReturn(stateChannels);
when(mockClient.on(any, any, any, any))
.thenAnswer((_) => Stream.empty());
when(mockClient.on(
EventType.messageNew,
)).thenAnswer((_) => eventController.stream);
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: channelsBloc,
),
);
final channelsBlocState = tester.state<ChannelsBlocState>(
find.byKey(channelsBlocKey),
);
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).thenAnswer(
(_) => Stream.value(channels),
);
await channelsBlocState.queryChannels();
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).called(1);
final messageNewEvent = Event(
type: EventType.messageNew,
cid: stateChannels.keys.first,
);
eventController.add(messageNewEvent);
final newChannels = [...channels]
..insert(0, stateChannels[stateChannels.keys.first]);
await expectLater(
channelsBlocState.channelsStream,
emitsInOrder([
isSameChannelListAs(channels),
isSameChannelListAs(newChannels),
]),
);
verify(mockClient.on(EventType.messageNew)).called(1);
},
);
testWidgets(
'channels should get sorted according to channelsComparator when '
'EventType.messageNew event is received',
(tester) async {
final mockClient = MockClient();
final channels = _generateChannels(mockClient);
int channelComparator(Channel a, Channel b) {
final aData = a.extraData['extra_data_key'] as String;
final bData = b.extraData['extra_data_key'] as String;
return bData.compareTo(aData);
}
const channelsBlocKey = Key('channelsBloc');
final channelsBloc = ChannelsBloc(
key: channelsBlocKey,
child: Offstage(),
shouldAddChannel: (_) => true,
channelsComparator: channelComparator,
);
when(mockClient.on(any, any, any, any))
.thenAnswer((_) => Stream.empty());
when(mockClient.on(
EventType.messageNew,
)).thenAnswer((_) => eventController.stream);
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: channelsBloc,
),
);
final channelsBlocState = tester.state<ChannelsBlocState>(
find.byKey(channelsBlocKey),
);
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).thenAnswer(
(_) => Stream.value(channels),
);
await channelsBlocState.queryChannels();
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).called(1);
final messageNewEvent = Event(
type: EventType.messageNew,
cid: channels.first.cid,
);
eventController.add(messageNewEvent);
final newChannels = [...channels]..sort(channelComparator);
await expectLater(
channelsBlocState.channelsStream,
emitsInOrder([
isSameChannelListAs(channels),
isSameChannelListAs(newChannels),
]),
);
verify(mockClient.on(EventType.messageNew)).called(1);
},
);
tearDown(() {
eventController.close();
});
});
}
@@ -0,0 +1,294 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:stream_chat_flutter_core/src/lazy_load_scroll_view.dart';
void main() {
test(
'should throw assertion error if child is null',
() async {
const lazyLoadScrollViewKey = Key('lazyLoadScrollView');
final lazyLoadScrollView = () => LazyLoadScrollView(
key: lazyLoadScrollViewKey,
child: null,
);
expect(lazyLoadScrollView, throwsA(isA<AssertionError>()));
},
);
testWidgets(
'should render LazyLoadScrollView if child is provided',
(tester) async {
const lazyLoadScrollViewKey = Key('lazyLoadScrollView');
final lazyLoadScrollView = LazyLoadScrollView(
key: lazyLoadScrollViewKey,
child: Offstage(),
);
await tester.pumpWidget(lazyLoadScrollView);
expect(find.byKey(lazyLoadScrollViewKey), findsOneWidget);
},
);
testWidgets(
'should render LazyLoadScrollView if child is provided',
(tester) async {
const lazyLoadScrollViewKey = Key('lazyLoadScrollView');
const childKey = Key('childKey');
final lazyLoadScrollView = LazyLoadScrollView(
key: lazyLoadScrollViewKey,
child: Offstage(key: childKey),
);
await tester.pumpWidget(lazyLoadScrollView);
expect(find.byKey(lazyLoadScrollViewKey), findsOneWidget);
expect(find.byKey(childKey), findsOneWidget);
},
);
testWidgets(
'should call onPageScrollStart if child scrollView scrolls',
(tester) async {
const lazyLoadScrollViewKey = Key('lazyLoadScrollView');
const childListViewKey = Key('childListView');
bool onPageScrollStartCalled = false;
final lazyLoadScrollView = LazyLoadScrollView(
key: lazyLoadScrollViewKey,
onPageScrollStart: () {
onPageScrollStartCalled = true;
},
child: ListView(
key: childListViewKey,
children: List.generate(
12,
(index) => Container(
height: 100,
child: Text('Item #$index'),
),
),
),
);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: lazyLoadScrollView,
),
);
expect(find.byKey(lazyLoadScrollViewKey), findsOneWidget);
expect(find.byKey(childListViewKey), findsOneWidget);
expect(onPageScrollStartCalled, isFalse);
await tester.startGesture(const Offset(100.0, 100.0));
await tester.pump(const Duration(seconds: 1));
expect(onPageScrollStartCalled, isTrue);
},
);
testWidgets(
'should call onPageScrollStart, onPageScrollEnd '
'if child scrollView starts scrolling and ends',
(tester) async {
const lazyLoadScrollViewKey = Key('lazyLoadScrollView');
const childListViewKey = Key('childListView');
bool onPageScrollStartCalled = false;
bool onPageScrollEndCalled = false;
final lazyLoadScrollView = LazyLoadScrollView(
key: lazyLoadScrollViewKey,
onPageScrollStart: () {
onPageScrollStartCalled = true;
},
onPageScrollEnd: () {
onPageScrollEndCalled = true;
},
child: ListView(
key: childListViewKey,
children: List.generate(
12,
(index) => Container(
height: 100,
child: Text('Item #$index'),
),
),
),
);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: lazyLoadScrollView,
),
);
expect(find.byKey(lazyLoadScrollViewKey), findsOneWidget);
expect(find.byKey(childListViewKey), findsOneWidget);
expect(onPageScrollStartCalled, isFalse);
expect(onPageScrollEndCalled, isFalse);
final gesture = await tester.createGesture();
await gesture.down(const Offset(100.0, 100.0));
await tester.pump(const Duration(seconds: 1));
expect(onPageScrollStartCalled, isTrue);
await gesture.up();
await tester.pump(const Duration(seconds: 1));
expect(onPageScrollEndCalled, isTrue);
},
);
testWidgets(
'should call onInBetweenOfPage if child scrollView '
'scroll position is in-between of view',
(tester) async {
const lazyLoadScrollViewKey = Key('lazyLoadScrollView');
const childListViewKey = Key('childListView');
bool onInBetweenOfPageCalled = false;
final lazyLoadScrollView = LazyLoadScrollView(
key: lazyLoadScrollViewKey,
onInBetweenOfPage: () async {
onInBetweenOfPageCalled = true;
},
child: ListView(
key: childListViewKey,
children: List.generate(
12,
(index) => Container(
height: 100,
child: Text('Item #$index'),
),
),
),
);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: lazyLoadScrollView,
),
);
expect(find.byKey(lazyLoadScrollViewKey), findsOneWidget);
expect(find.byKey(childListViewKey), findsOneWidget);
expect(onInBetweenOfPageCalled, isFalse);
final gesture = await tester.createGesture();
await gesture.down(const Offset(100.0, 100.0));
await tester.pump(const Duration(seconds: 1));
await gesture.moveBy(const Offset(-200.0, -200.0));
await tester.pump(const Duration(seconds: 1));
expect(onInBetweenOfPageCalled, isTrue);
},
);
testWidgets(
'should call onStartOfPage if child scrollView '
'tends to reach the start',
(tester) async {
const lazyLoadScrollViewKey = Key('lazyLoadScrollView');
const childListViewKey = Key('childListView');
bool onStartOfPageCalled = false;
final lazyLoadScrollView = LazyLoadScrollView(
key: lazyLoadScrollViewKey,
onStartOfPage: () async {
onStartOfPageCalled = true;
},
child: ListView(
key: childListViewKey,
children: List.generate(
12,
(index) => Container(
height: 100,
child: Text('Item #$index'),
),
),
),
);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: lazyLoadScrollView,
),
);
expect(find.byKey(lazyLoadScrollViewKey), findsOneWidget);
expect(find.byKey(childListViewKey), findsOneWidget);
expect(onStartOfPageCalled, isFalse);
final gesture = await tester.createGesture();
await gesture.down(const Offset(100.0, 100.0));
await tester.pump(const Duration(seconds: 1));
await gesture.moveBy(const Offset(-200.0, -200.0));
await tester.pump(const Duration(seconds: 1));
await gesture.moveBy(const Offset(201.0, 201.0));
await tester.pump(const Duration(seconds: 1));
expect(onStartOfPageCalled, isTrue);
},
);
testWidgets(
'should call onEndOfPage if child scrollView '
'tends to reach the end',
(tester) async {
const lazyLoadScrollViewKey = Key('lazyLoadScrollView');
const childListViewKey = Key('childListView');
bool onEndOfPageCalled = false;
final lazyLoadScrollView = LazyLoadScrollView(
key: lazyLoadScrollViewKey,
onEndOfPage: () async {
onEndOfPageCalled = true;
},
child: ListView(
key: childListViewKey,
children: List.generate(
12,
(index) => Container(
height: 100,
child: Text('Item #$index'),
),
),
),
);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: lazyLoadScrollView,
),
);
expect(find.byKey(lazyLoadScrollViewKey), findsOneWidget);
expect(find.byKey(childListViewKey), findsOneWidget);
expect(onEndOfPageCalled, isFalse);
final gesture = await tester.createGesture();
await gesture.down(const Offset(100.0, 100.0));
await tester.pump(const Duration(seconds: 1));
await gesture.moveBy(const Offset(-601.0, -601.0));
await tester.pump(const Duration(seconds: 1));
expect(onEndOfPageCalled, isTrue);
},
);
}
@@ -0,0 +1,49 @@
import 'package:meta/meta.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
Matcher isSameChannelAs(Channel targetChannel) =>
_IsSameChannelAs(targetChannel: targetChannel);
class _IsSameChannelAs extends Matcher {
const _IsSameChannelAs({
@required this.targetChannel,
}) : assert(targetChannel != null, '');
final Channel targetChannel;
@override
bool matches(covariant Channel channel, Map matchState) =>
channel.cid == targetChannel.cid;
@override
Description describe(Description description) =>
description.add('is same channel as $targetChannel');
}
Matcher isSameChannelListAs(List<Channel> targetChannelList) =>
_IsSameChannelListAs(targetChannelList: targetChannelList);
class _IsSameChannelListAs extends Matcher {
const _IsSameChannelListAs({
@required this.targetChannelList,
}) : assert(targetChannelList != null, '');
final List<Channel> targetChannelList;
@override
bool matches(covariant List<Channel> channelList, Map matchState) {
bool matches = true;
for (var i = 0; i < channelList.length; i++) {
final channel = channelList[i];
final targetChannel = targetChannelList[i];
matches = isSameChannelAs(targetChannel).matches(channel, matchState);
if (!matches) break;
}
return matches;
}
@override
Description describe(Description description) =>
description.add('is same channelList as $targetChannelList');
}
@@ -0,0 +1,55 @@
import 'package:meta/meta.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
Matcher isSameMessageResponseAs(GetMessageResponse targetResponse) =>
_IsSameMessageResponseAs(targetResponse: targetResponse);
class _IsSameMessageResponseAs extends Matcher {
const _IsSameMessageResponseAs({
@required this.targetResponse,
}) : assert(targetResponse != null, '');
final GetMessageResponse targetResponse;
@override
bool matches(covariant GetMessageResponse response, Map matchState) =>
response.message.id == targetResponse.message.id &&
response.channel.cid == targetResponse.channel.cid;
@override
Description describe(Description description) =>
description.add('is same message response as $targetResponse');
}
Matcher isSameMessageResponseListAs(
List<GetMessageResponse> targetResponseList) =>
_IsSameMessageResponseListAs(targetResponseList: targetResponseList);
class _IsSameMessageResponseListAs extends Matcher {
const _IsSameMessageResponseListAs({
@required this.targetResponseList,
}) : assert(targetResponseList != null, '');
final List<GetMessageResponse> targetResponseList;
@override
bool matches(
covariant List<GetMessageResponse> responseList, Map matchState) {
bool matches = true;
for (var i = 0; i < responseList.length; i++) {
final response = responseList[i];
final targetResponse = targetResponseList[i];
matches = isSameMessageResponseAs(targetResponse).matches(
response,
matchState,
);
if (!matches) break;
}
return matches;
}
@override
Description describe(Description description) =>
description.add('is same userResponseList as $targetResponseList');
}
@@ -0,0 +1,49 @@
import 'package:meta/meta.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
Matcher isSameMessageAs(Message targetMessage) =>
_IsSameMessageAs(targetMessage: targetMessage);
class _IsSameMessageAs extends Matcher {
const _IsSameMessageAs({
@required this.targetMessage,
}) : assert(targetMessage != null, '');
final Message targetMessage;
@override
bool matches(covariant Message message, Map matchState) =>
message.id == targetMessage.id;
@override
Description describe(Description description) =>
description.add('is same message as $targetMessage');
}
Matcher isSameMessageListAs(List<Message> targetMessageList) =>
_IsSameMessageListAs(targetMessageList: targetMessageList);
class _IsSameMessageListAs extends Matcher {
const _IsSameMessageListAs({
@required this.targetMessageList,
}) : assert(targetMessageList != null, '');
final List<Message> targetMessageList;
@override
bool matches(covariant List<Message> messageList, Map matchState) {
bool matches = true;
for (var i = 0; i < messageList.length; i++) {
final message = messageList[i];
final targetMessage = targetMessageList[i];
matches = isSameMessageAs(targetMessage).matches(message, matchState);
if (!matches) break;
}
return matches;
}
@override
Description describe(Description description) =>
description.add('is same messageList as $targetMessageList');
}
@@ -0,0 +1,47 @@
import 'package:meta/meta.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
Matcher isSameUserAs(User targetUser) => _IsSameUserAs(targetUser: targetUser);
class _IsSameUserAs extends Matcher {
const _IsSameUserAs({
@required this.targetUser,
}) : assert(targetUser != null, '');
final User targetUser;
@override
bool matches(covariant User user, Map matchState) => user.id == targetUser.id;
@override
Description describe(Description description) =>
description.add('is same user as $targetUser');
}
Matcher isSameUserListAs(List<User> targetUserList) =>
_IsSameUserListAs(targetUserList: targetUserList);
class _IsSameUserListAs extends Matcher {
const _IsSameUserListAs({
@required this.targetUserList,
}) : assert(targetUserList != null, '');
final List<User> targetUserList;
@override
bool matches(covariant List<User> userList, Map matchState) {
bool matches = true;
for (var i = 0; i < userList.length; i++) {
final user = userList[i];
final targetUser = targetUserList[i];
matches = isSameUserAs(targetUser).matches(user, matchState);
if (!matches) break;
}
return matches;
}
@override
Description describe(Description description) =>
description.add('is same userList as $targetUserList');
}
@@ -0,0 +1,413 @@
import 'dart:async';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:stream_chat_flutter_core/src/message_list_core.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'mocks.dart';
void main() {
List<Message> _generateMessages({
int count = 3,
int offset = 0,
bool threads = false,
}) {
final users = List.generate(count, (index) {
index = count + offset;
return User(id: 'testUserId$index');
});
final messages = List.generate(
count,
(index) {
index = index + offset;
return Message(
id: 'testMessageId$index',
type: 'testType',
user: users[index],
createdAt: DateTime.now(),
shadowed: false,
replyCount: index,
updatedAt: DateTime.now(),
extraData: {'extra_test_field': 'extraTestData'},
text: 'Dummy text #$index',
pinned: true,
pinnedAt: DateTime.now(),
pinnedBy: User(id: 'testUserId$index'),
);
},
);
final threadMessages = List.generate(
count,
(index) {
index = index + offset;
return Message(
id: 'testThreadMessageId$index',
type: 'testType',
user: users[index],
parentId: messages[0].id,
createdAt: DateTime.now(),
shadowed: false,
replyCount: index,
updatedAt: DateTime.now(),
extraData: {'extra_test_field': 'extraTestData'},
text: 'Dummy text #$index',
pinned: true,
pinnedAt: DateTime.now(),
pinnedBy: User(id: 'testUserId$index'),
);
},
);
return threads ? threadMessages : messages;
}
test(
'should throw assertion error in case messageListBuilder is null',
() {
final messageListCore = () => MessageListCore(
messageListBuilder: null,
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorWidgetBuilder: (BuildContext context, Object error) =>
Offstage(),
);
expect(messageListCore, throwsA(isA<AssertionError>()));
},
);
test(
'should throw assertion error in case loadingBuilder is null',
() {
final messageListCore = () => MessageListCore(
messageListBuilder: (_, __) => Offstage(),
loadingBuilder: null,
emptyBuilder: (BuildContext context) => Offstage(),
errorWidgetBuilder: (BuildContext context, Object error) =>
Offstage(),
);
expect(messageListCore, throwsA(isA<AssertionError>()));
},
);
test(
'should throw assertion error in case emptyBuilder is null',
() {
final messageListCore = () => MessageListCore(
messageListBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: null,
errorWidgetBuilder: (BuildContext context, Object error) =>
Offstage(),
);
expect(messageListCore, throwsA(isA<AssertionError>()));
},
);
test(
'should throw assertion error in case errorWidgetBuilder is null',
() {
final messageListCore = () => MessageListCore(
messageListBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorWidgetBuilder: null,
);
expect(messageListCore, throwsA(isA<AssertionError>()));
},
);
testWidgets(
'should throw if MessageListCore is used where StreamChannel is not present '
'in the widget tree',
(tester) async {
const messageListCoreKey = Key('messageListCore');
final messageListCore = MessageListCore(
key: messageListCoreKey,
messageListBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorWidgetBuilder: (BuildContext context, Object error) => Offstage(),
);
await tester.pumpWidget(messageListCore);
expect(find.byKey(messageListCoreKey), findsNothing);
expect(tester.takeException(), isInstanceOf<Exception>());
},
);
testWidgets(
'should render MessageListCore if used with StreamChannel as an ancestor',
(tester) async {
const messageListCoreKey = Key('messageListCore');
final messageListCore = MessageListCore(
key: messageListCoreKey,
messageListBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorWidgetBuilder: (BuildContext context, Object error) => Offstage(),
);
final mockChannel = MockChannel();
when(mockChannel.state.isUpToDate).thenReturn(true);
await tester.pumpWidget(
StreamChannel(
channel: mockChannel,
child: messageListCore,
),
);
expect(find.byKey(messageListCoreKey), findsOneWidget);
},
);
testWidgets(
'should assign paginateData callback to MessageListController if passed',
(tester) async {
const messageListCoreKey = Key('messageListCore');
final controller = MessageListController();
final messageListCore = MessageListCore(
key: messageListCoreKey,
messageListBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorWidgetBuilder: (BuildContext context, Object error) => Offstage(),
messageListController: controller,
);
expect(controller.paginateData, isNull);
final mockChannel = MockChannel();
when(mockChannel.state.isUpToDate).thenReturn(true);
await tester.pumpWidget(
StreamChannel(
channel: mockChannel,
child: messageListCore,
),
);
expect(find.byKey(messageListCoreKey), findsOneWidget);
expect(controller.paginateData, isNotNull);
},
);
testWidgets(
'should build error widget if messagesStream emits error',
(tester) async {
const messageListCoreKey = Key('messageListCore');
const errorWidgetKey = Key('errorWidget');
final messageListCore = MessageListCore(
key: messageListCoreKey,
messageListBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorWidgetBuilder: (BuildContext context, Object error) => Offstage(
key: errorWidgetKey,
),
);
final mockChannel = MockChannel();
when(mockChannel.state.isUpToDate).thenReturn(true);
when(mockChannel.initialized).thenAnswer((_) async => true);
const error = 'Error! Error! Error!';
when(mockChannel.state.messagesStream)
.thenAnswer((_) => Stream.error(error));
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StreamChannel(
channel: mockChannel,
child: messageListCore,
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(errorWidgetKey), findsOneWidget);
},
);
testWidgets(
'should build empty widget if the channel is upToDate and '
'messagesStream emits empty data',
(tester) async {
const messageListCoreKey = Key('messageListCore');
const emptyWidgetKey = Key('emptyWidget');
final messageListCore = MessageListCore(
key: messageListCoreKey,
messageListBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(key: emptyWidgetKey),
errorWidgetBuilder: (BuildContext context, Object error) => Offstage(),
);
final mockChannel = MockChannel();
when(mockChannel.state.isUpToDate).thenReturn(true);
when(mockChannel.initialized).thenAnswer((_) async => true);
const messages = <Message>[];
when(mockChannel.state.messagesStream)
.thenAnswer((_) => Stream.value(messages));
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StreamChannel(
channel: mockChannel,
child: messageListCore,
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(emptyWidgetKey), findsOneWidget);
},
);
testWidgets(
'should build list widget if the channel is not upToDate and '
'messagesStream emits empty data',
(tester) async {
const messageListCoreKey = Key('messageListCore');
const listWidgetKey = Key('listWidget');
final messageListCore = MessageListCore(
key: messageListCoreKey,
messageListBuilder: (_, __) => Offstage(key: listWidgetKey),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorWidgetBuilder: (BuildContext context, Object error) => Offstage(),
);
final mockChannel = MockChannel();
when(mockChannel.state.isUpToDate).thenReturn(false);
when(mockChannel.initialized).thenAnswer((_) async => true);
const messages = <Message>[];
when(mockChannel.state.messagesStream)
.thenAnswer((_) => Stream.value(messages));
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StreamChannel(
channel: mockChannel,
child: messageListCore,
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(listWidgetKey), findsOneWidget);
},
);
testWidgets(
'should build list widget and save it for future rebuilds '
'if messagesStream emits some data',
(tester) async {
const messageListCoreKey = Key('messageListCore');
const listWidgetKey = Key('listWidget');
final messageListCore = MessageListCore(
key: messageListCoreKey,
messageListBuilder: (_, messages) => Container(
key: listWidgetKey,
child: Text(
messages.reversed.map((it) => it.id).join(','),
),
),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorWidgetBuilder: (BuildContext context, Object error) => Offstage(),
);
final mockChannel = MockChannel();
when(mockChannel.state.isUpToDate).thenReturn(true);
when(mockChannel.initialized).thenAnswer((_) async => true);
final messages = _generateMessages();
when(mockChannel.state.messagesStream)
.thenAnswer((_) => Stream.value(messages));
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StreamChannel(
channel: mockChannel,
child: messageListCore,
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(listWidgetKey), findsOneWidget);
expect(find.text(messages.map((it) => it.id).join(',')), findsOneWidget);
},
);
testWidgets(
'should build list widget with thread messages if parentMessage is passed',
(tester) async {
final messages = _generateMessages(threads: true);
final parentMessage = Message(id: messages.first.parentId);
const messageListCoreKey = Key('messageListCore');
const listWidgetKey = Key('listWidget');
final messageListCore = MessageListCore(
key: messageListCoreKey,
messageListBuilder: (_, messages) => Container(
key: listWidgetKey,
child: Text(
messages.reversed.map((it) => '${it.parentId}-${it.id}').join(','),
),
),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorWidgetBuilder: (BuildContext context, Object error) => Offstage(),
parentMessage: parentMessage,
);
final mockChannel = MockChannel();
when(mockChannel.state.isUpToDate).thenReturn(true);
when(mockChannel.initialized).thenAnswer((_) async => true);
final threads = {parentMessage.id: messages};
when(mockChannel.state.threads).thenReturn(threads);
when(mockChannel.state.threadsStream)
.thenAnswer((_) => Stream.value(threads));
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StreamChannel(
channel: mockChannel,
child: messageListCore,
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(listWidgetKey), findsOneWidget);
expect(
find.text(messages.map((it) => '${it.parentId}-${it.id}').join(',')),
findsOneWidget,
);
},
);
}
@@ -0,0 +1,344 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter_core/src/message_search_bloc.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'matchers/get_message_response_matcher.dart';
import 'mocks.dart';
void main() {
List<GetMessageResponse> _generateMessages({
int count = 3,
int offset = 0,
}) {
return List.generate(
count,
(index) {
index = index + offset;
return GetMessageResponse()
..message = Message(
id: 'testId$index',
text: 'testTextData$index',
)
..channel = ChannelModel(
cid: 'testCid',
);
},
);
}
test(
'should throw assertion error if child is null',
() async {
const messageSearchBlocKey = Key('messageSearchBloc');
final messageSearchBloc = () => MessageSearchBloc(
key: messageSearchBlocKey,
child: null,
);
expect(messageSearchBloc, throwsA(isA<AssertionError>()));
},
);
testWidgets(
'messageSearchBlocState.search() should throw if used where '
'StreamChat is not present in the widget tree',
(tester) async {
const messageSearchBlocKey = Key('messageSearchBloc');
const childKey = Key('child');
final messageSearchBloc = MessageSearchBloc(
key: messageSearchBlocKey,
child: Offstage(key: childKey),
);
await tester.pumpWidget(messageSearchBloc);
expect(find.byKey(messageSearchBlocKey), findsOneWidget);
expect(find.byKey(childKey), findsOneWidget);
final usersBlocState = tester.state<MessageSearchBlocState>(
find.byKey(messageSearchBlocKey),
);
try {
await usersBlocState.search();
} catch (e) {
expect(e, isInstanceOf<Exception>());
}
},
);
testWidgets(
'messageSearchBlocState.search() should emit data through usersStream',
(tester) async {
const messageSearchBlocKey = Key('messageSearchBloc');
const childKey = Key('child');
final messageSearchBloc = MessageSearchBloc(
key: messageSearchBlocKey,
child: Offstage(key: childKey),
);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: messageSearchBloc,
),
);
final messageSearchBlocState = tester.state<MessageSearchBlocState>(
find.byKey(messageSearchBlocKey),
);
final messageResponseList = _generateMessages();
when(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: anyNamed('paginationParams'),
)).thenAnswer(
(_) async => SearchMessagesResponse()..results = messageResponseList,
);
messageSearchBlocState.search();
await expectLater(
messageSearchBlocState.messagesStream,
emits(isSameMessageResponseListAs(messageResponseList)),
);
verify(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: anyNamed('paginationParams'),
)).called(1);
},
);
testWidgets(
'messageSearchBlocState.messagesStream should emit error '
'if client.search() throws',
(tester) async {
const messageSearchBlocKey = Key('messageSearchBloc');
const childKey = Key('child');
final messageSearchBloc = MessageSearchBloc(
key: messageSearchBlocKey,
child: Offstage(key: childKey),
);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: messageSearchBloc,
),
);
final messageSearchBlocState = tester.state<MessageSearchBlocState>(
find.byKey(messageSearchBlocKey),
);
const error = 'Error! Error! Error!';
when(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: anyNamed('paginationParams'),
)).thenThrow(error);
messageSearchBlocState.search();
await expectLater(
messageSearchBlocState.messagesStream,
emitsError(error),
);
verify(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: anyNamed('paginationParams'),
)).called(1);
},
);
testWidgets(
'calling messageSearchBlocState.search() again with an offset '
'should emit new data through messagesStream and also emit loading state '
'through queryMessagesLoading',
(tester) async {
const messageSearchBlocKey = Key('messageSearchBloc');
const childKey = Key('child');
final messageSearchBloc = MessageSearchBloc(
key: messageSearchBlocKey,
child: Offstage(key: childKey),
);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: messageSearchBloc,
),
);
final messageSearchBlocState = tester.state<MessageSearchBlocState>(
find.byKey(messageSearchBlocKey),
);
final messageResponseList = _generateMessages();
when(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: anyNamed('paginationParams'),
)).thenAnswer(
(_) async => SearchMessagesResponse()..results = messageResponseList,
);
messageSearchBlocState.search();
await expectLater(
messageSearchBlocState.messagesStream,
emits(isSameMessageResponseListAs(messageResponseList)),
);
verify(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: anyNamed('paginationParams'),
)).called(1);
final offset = messageResponseList.length;
final paginatedMessageResponseList = _generateMessages(offset: offset);
final pagination = PaginationParams(offset: offset);
when(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: pagination,
)).thenAnswer(
(_) async =>
SearchMessagesResponse()..results = paginatedMessageResponseList,
);
messageSearchBlocState.search(pagination: pagination);
await Future.wait([
expectLater(
messageSearchBlocState.queryMessagesLoading,
emitsInOrder([true, false]),
),
expectLater(
messageSearchBlocState.messagesStream,
emits(isSameMessageResponseListAs(
messageResponseList + paginatedMessageResponseList,
)),
),
]);
verify(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: pagination,
)).called(1);
},
);
testWidgets(
'calling messageSearchBlocState.search() again with an offset '
'should emit error through queryUsersLoading if '
'client.search() throws',
(tester) async {
const messageSearchBlocKey = Key('messageSearchBloc');
const childKey = Key('child');
final messageSearchBloc = MessageSearchBloc(
key: messageSearchBlocKey,
child: Offstage(key: childKey),
);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: messageSearchBloc,
),
);
final messageSearchBlocState = tester.state<MessageSearchBlocState>(
find.byKey(messageSearchBlocKey),
);
final messageResponseList = _generateMessages();
when(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: anyNamed('paginationParams'),
)).thenAnswer(
(_) async => SearchMessagesResponse()..results = messageResponseList,
);
messageSearchBlocState.search();
await expectLater(
messageSearchBlocState.messagesStream,
emits(isSameMessageResponseListAs(messageResponseList)),
);
verify(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: anyNamed('paginationParams'),
)).called(1);
final offset = messageResponseList.length;
final pagination = PaginationParams(offset: offset);
const error = 'Error! Error! Error!';
when(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: pagination,
)).thenThrow(error);
messageSearchBlocState.search(pagination: pagination);
await expectLater(
messageSearchBlocState.queryMessagesLoading,
emitsError(error),
);
verify(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: pagination,
)).called(1);
},
);
}
@@ -0,0 +1,535 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'package:stream_chat_flutter_core/src/message_search_list_core.dart';
import 'mocks.dart';
void main() {
List<GetMessageResponse> _generateMessages({
int count = 3,
int offset = 0,
}) {
return List.generate(
count,
(index) {
index = index + offset;
return GetMessageResponse()
..message = Message(
id: 'testId$index',
text: 'testTextData$index',
)
..channel = ChannelModel(
cid: 'testCid',
);
},
);
}
test(
'should throw assertion error in case childBuilder is null',
() {
final messageSearchListCore = () => MessageSearchListCore(
childBuilder: null,
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (BuildContext context, Object error) => Offstage(),
);
expect(messageSearchListCore, throwsA(isA<AssertionError>()));
},
);
test(
'should throw assertion error in case loadingBuilder is null',
() {
final messageSearchListCore = () => MessageSearchListCore(
childBuilder: (List<GetMessageResponse> messages) => Offstage(),
loadingBuilder: null,
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (BuildContext context, Object error) => Offstage(),
);
expect(messageSearchListCore, throwsA(isA<AssertionError>()));
},
);
test(
'should throw assertion error in case emptyBuilder is null',
() {
final messageSearchListCore = () => MessageSearchListCore(
childBuilder: (List<GetMessageResponse> messages) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: null,
errorBuilder: (BuildContext context, Object error) => Offstage(),
);
expect(messageSearchListCore, throwsA(isA<AssertionError>()));
},
);
test(
'should throw assertion error in case errorBuilder is null',
() {
final messageSearchListCore = () => MessageSearchListCore(
childBuilder: (List<GetMessageResponse> messages) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: null,
);
expect(messageSearchListCore, throwsA(isA<AssertionError>()));
},
);
testWidgets(
'should throw if MessageSearchListCore is used where MessageSearchBloc '
'is not present in the widget tree',
(tester) async {
const messageSearchListCoreKey = Key('messageSearchListCore');
final messageSearchListCore = MessageSearchListCore(
key: messageSearchListCoreKey,
childBuilder: (List<GetMessageResponse> messages) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (BuildContext context, Object error) => Offstage(),
);
await tester.pumpWidget(messageSearchListCore);
expect(find.byKey(messageSearchListCoreKey), findsNothing);
expect(tester.takeException(), isInstanceOf<Exception>());
},
);
testWidgets(
'should render MessageSearchListCore if used with '
'MessageSearchListCore as an ancestor',
(tester) async {
const messageSearchListCoreKey = Key('messageSearchListCore');
final messageSearchListCore = MessageSearchListCore(
key: messageSearchListCoreKey,
childBuilder: (List<GetMessageResponse> messages) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (BuildContext context, Object error) => Offstage(),
);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: MessageSearchBloc(
child: messageSearchListCore,
),
),
);
expect(find.byKey(messageSearchListCoreKey), findsOneWidget);
},
);
testWidgets(
'should assign loadData and paginateData callback to '
'UserListController if passed',
(tester) async {
const messageSearchListCoreKey = Key('messageSearchListCore');
final controller = MessageSearchListController();
final messageSearchListCore = MessageSearchListCore(
key: messageSearchListCoreKey,
childBuilder: (List<GetMessageResponse> messages) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (BuildContext context, Object error) => Offstage(),
messageSearchListController: controller,
);
expect(controller.loadData, isNull);
expect(controller.paginateData, isNull);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: MessageSearchBloc(
child: messageSearchListCore,
),
),
);
expect(find.byKey(messageSearchListCoreKey), findsOneWidget);
expect(controller.loadData, isNotNull);
expect(controller.paginateData, isNotNull);
},
);
testWidgets(
'should build error widget if messageSearchBloc.messagesStream emits error',
(tester) async {
const messageSearchListCoreKey = Key('messageSearchListCore');
const errorWidgetKey = Key('errorWidget');
final messageSearchListCore = MessageSearchListCore(
key: messageSearchListCoreKey,
childBuilder: (List<GetMessageResponse> messages) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (BuildContext context, Object error) => Offstage(
key: errorWidgetKey,
),
);
final mockClient = MockClient();
const error = 'Error! Error! Error!';
when(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: anyNamed('paginationParams'),
)).thenThrow(error);
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: MessageSearchBloc(
child: messageSearchListCore,
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(errorWidgetKey), findsOneWidget);
verify(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: anyNamed('paginationParams'),
)).called(1);
},
);
testWidgets(
'should build empty widget if messageSearchBloc.messagesStream '
'emits empty data',
(tester) async {
const messageSearchListCoreKey = Key('messageSearchListCore');
const emptyWidgetKey = Key('emptyWidget');
final messageSearchListCore = MessageSearchListCore(
key: messageSearchListCoreKey,
childBuilder: (List<GetMessageResponse> messages) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(key: emptyWidgetKey),
errorBuilder: (BuildContext context, Object error) => Offstage(),
);
final mockClient = MockClient();
final messageResponseList = <GetMessageResponse>[];
when(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: anyNamed('paginationParams'),
)).thenAnswer(
(_) async => SearchMessagesResponse()..results = messageResponseList,
);
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: MessageSearchBloc(
child: messageSearchListCore,
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(emptyWidgetKey), findsOneWidget);
verify(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: anyNamed('paginationParams'),
)).called(1);
},
);
testWidgets(
'should build child widget if usersBlocState.usersStream emits some data',
(tester) async {
const messageSearchListCoreKey = Key('messageSearchListCore');
const childWidgetKey = Key('childWidget');
final messageSearchListCore = MessageSearchListCore(
key: messageSearchListCoreKey,
childBuilder: (List<GetMessageResponse> messages) => Offstage(
key: childWidgetKey,
),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (BuildContext context, Object error) => Offstage(),
);
final mockClient = MockClient();
final messageResponseList = _generateMessages();
when(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: anyNamed('paginationParams'),
)).thenAnswer(
(_) async => SearchMessagesResponse()..results = messageResponseList,
);
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: MessageSearchBloc(
child: messageSearchListCore,
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(childWidgetKey), findsOneWidget);
verify(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: anyNamed('paginationParams'),
)).called(1);
},
);
testWidgets(
'should build child widget with paginated data '
'on calling channelListCoreState.paginateData',
(tester) async {
const messageSearchListCoreKey = Key('messageSearchListCore');
const childWidgetKey = Key('childWidget');
const pagination = PaginationParams();
final messageSearchListCore = MessageSearchListCore(
key: messageSearchListCoreKey,
childBuilder: (List<GetMessageResponse> messages) => Container(
key: childWidgetKey,
child: Text(
messages.map((e) => '${e.channel.cid}-${e.message.id}').join(','),
),
),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (BuildContext context, Object error) => Offstage(),
paginationParams: pagination,
);
final mockClient = MockClient();
final messageResponseList = _generateMessages();
when(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: pagination,
)).thenAnswer(
(_) async => SearchMessagesResponse()..results = messageResponseList,
);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StreamChatCore(
client: mockClient,
child: MessageSearchBloc(
child: messageSearchListCore,
),
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(childWidgetKey), findsOneWidget);
expect(
find.text(
messageResponseList
.map((e) => '${e.channel.cid}-${e.message.id}')
.join(','),
),
findsOneWidget,
);
verify(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: pagination,
)).called(1);
final messageSearchListCoreState =
tester.state<MessageSearchListCoreState>(
find.byKey(messageSearchListCoreKey),
);
final offset = messageResponseList.length;
final paginatedMessageResponseList = _generateMessages(offset: offset);
final updatedPagination = pagination.copyWith(offset: offset);
when(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: updatedPagination,
)).thenAnswer(
(_) async =>
SearchMessagesResponse()..results = paginatedMessageResponseList,
);
await messageSearchListCoreState.paginateData();
await tester.pumpAndSettle();
expect(find.byKey(childWidgetKey), findsOneWidget);
expect(
find.text([
...messageResponseList,
...paginatedMessageResponseList,
].map((e) => '${e.channel.cid}-${e.message.id}').join(',')),
findsOneWidget,
);
verify(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: updatedPagination,
)).called(1);
},
);
testWidgets(
'should rebuild MessageSearchListCore with updated widget data '
'on calling setState()',
(tester) async {
const pagination = PaginationParams();
StateSetter _stateSetter;
int limit = pagination.limit;
const messageSearchListCoreKey = Key('messageSearchListCore');
const childWidgetKey = Key('childWidget');
MessageSearchListCore messageSearchListCoreBuilder(int limit) =>
MessageSearchListCore(
key: messageSearchListCoreKey,
childBuilder: (List<GetMessageResponse> messages) => Container(
key: childWidgetKey,
child: Text(
messages
.map((e) => '${e.channel.cid}-${e.message.id}')
.join(','),
),
),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (BuildContext context, Object error) => Offstage(),
paginationParams: pagination.copyWith(limit: limit),
);
final mockClient = MockClient();
final messageResponseList = _generateMessages();
when(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: pagination,
)).thenAnswer(
(_) async => SearchMessagesResponse()..results = messageResponseList,
);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StreamChatCore(
client: mockClient,
child: MessageSearchBloc(
child: StatefulBuilder(builder: (context, stateSetter) {
// Assigning stateSetter for rebuilding UserListCore
_stateSetter = stateSetter;
return messageSearchListCoreBuilder(limit);
}),
),
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(childWidgetKey), findsOneWidget);
expect(
find.text(
messageResponseList
.map((e) => '${e.channel.cid}-${e.message.id}')
.join(','),
),
findsOneWidget,
);
verify(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: pagination,
)).called(1);
// Rebuilding MessageSearchListCore with new pagination limit
_stateSetter(() => limit = 6);
final updatedMessageResponseList = _generateMessages(count: limit);
final updatedPagination = pagination.copyWith(limit: limit);
when(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: updatedPagination,
)).thenAnswer(
(_) async =>
SearchMessagesResponse()..results = updatedMessageResponseList,
);
await tester.pumpAndSettle();
expect(find.byKey(childWidgetKey), findsOneWidget);
expect(
find.text(updatedMessageResponseList
.map((e) => '${e.channel.cid}-${e.message.id}')
.join(',')),
findsOneWidget,
);
verify(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: updatedPagination,
)).called(1);
},
);
}
@@ -1,10 +1,39 @@
import 'package:mockito/mockito.dart';
import 'package:stream_chat/stream_chat.dart';
class MockClient extends Mock implements StreamChatClient {}
class MockLogger extends Mock implements Logger {}
class MockClientState extends Mock implements ClientState {}
class MockClient extends Mock implements StreamChatClient {
final Logger logger = MockLogger();
class MockChannel extends Mock implements Channel {}
ClientState _state;
@override
ClientState get state => _state ??= MockClientState();
}
class MockClientState extends Mock implements ClientState {
OwnUser _user;
@override
OwnUser get user => _user ??= OwnUser(
id: 'testUserId',
role: 'admin',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
}
class MockChannel extends Mock implements Channel {
ChannelClientState _state;
@override
ChannelClientState get state => _state ??= MockChannelState();
StreamChatClient _client;
@override
StreamChatClient get client => _client ??= MockClient();
}
class MockChannelState extends Mock implements ChannelClientState {}
@@ -0,0 +1,343 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'mocks.dart';
void main() {
List<Message> _generateMessages({
int count = 3,
int offset = 0,
bool threads = false,
}) {
final users = List.generate(count, (index) {
index = count + offset;
return User(id: 'testUserId$index');
});
final messages = List.generate(
count,
(index) {
index = index + offset;
return Message(
id: 'testMessageId$index',
type: 'testType',
user: users[index],
createdAt: DateTime.now(),
shadowed: false,
replyCount: index,
updatedAt: DateTime.now(),
extraData: {'extra_test_field': 'extraTestData'},
text: 'Dummy text #$index',
pinned: true,
pinnedAt: DateTime.now(),
pinnedBy: User(id: 'testUserId$index'),
);
},
);
final threadMessages = List.generate(
count,
(index) {
index = index + offset;
return Message(
id: 'testThreadMessageId$index',
type: 'testType',
user: users[index],
parentId: messages[0].id,
createdAt: DateTime.now(),
shadowed: false,
replyCount: index,
updatedAt: DateTime.now(),
extraData: {'extra_test_field': 'extraTestData'},
text: 'Dummy text #$index',
pinned: true,
pinnedAt: DateTime.now(),
pinnedBy: User(id: 'testUserId$index'),
);
},
);
return threads ? threadMessages : messages;
}
test(
'should throw assertion error if child is null',
() async {
final mockChannel = MockChannel();
const streamChannelKey = Key('streamChannel');
final streamChannel = () => StreamChannel(
key: streamChannelKey,
channel: mockChannel,
child: null,
);
expect(streamChannel, throwsA(isA<AssertionError>()));
},
);
test(
'should throw assertion error if channel is null',
() async {
const streamChannelKey = Key('streamChannel');
final streamChannel = () => StreamChannel(
key: streamChannelKey,
child: Offstage(),
channel: null,
);
expect(streamChannel, throwsA(isA<AssertionError>()));
},
);
testWidgets(
'should render StreamChannel if both channel and child is provided',
(tester) async {
final mockChannel = MockChannel();
const streamChannelKey = Key('streamChannel');
const childKey = Key('childKey');
final streamChannel = StreamChannel(
key: streamChannelKey,
channel: mockChannel,
child: Offstage(key: childKey),
);
await tester.pumpWidget(streamChannel);
expect(find.byKey(streamChannelKey), findsOneWidget);
expect(find.byKey(childKey), findsOneWidget);
},
);
testWidgets(
'should build error widget if channel.initialized throws',
(tester) async {
final mockChannel = MockChannel();
const streamChannelKey = Key('streamChannel');
const childKey = Key('childKey');
final streamChannel = StreamChannel(
key: streamChannelKey,
channel: mockChannel,
child: Offstage(key: childKey),
);
final errorMessage = 'Error! Error! Error!';
final error = DioError(type: DioErrorType.RESPONSE, error: errorMessage);
when(mockChannel.initialized).thenAnswer((_) => Future.error(error));
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: streamChannel,
),
);
await tester.pumpAndSettle();
expect(find.text(errorMessage), findsOneWidget);
verify(mockChannel.initialized).called(1);
},
);
testWidgets(
'should show circular progress indicator showLoading is true '
'and channel is still not initialized',
(tester) async {
final mockChannel = MockChannel();
const streamChannelKey = Key('streamChannel');
const childKey = Key('childKey');
final streamChannel = StreamChannel(
key: streamChannelKey,
channel: mockChannel,
child: Offstage(key: childKey),
showLoading: true,
);
when(mockChannel.initialized).thenAnswer((_) async => false);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: streamChannel,
),
);
await tester.pump(const Duration(seconds: 1));
expect(find.byType(CircularProgressIndicator), findsOneWidget);
verify(mockChannel.initialized).called(1);
},
);
testWidgets(
'should preload extra messages if initialMessageId is provided',
(tester) async {
final mockChannel = MockChannel();
const streamChannelKey = Key('streamChannel');
const childKey = Key('childKey');
final streamChannel = StreamChannel(
key: streamChannelKey,
channel: mockChannel,
child: Offstage(key: childKey),
initialMessageId: 'testInitialMessageId',
);
when(mockChannel.initialized).thenAnswer((_) async => true);
final messages = _generateMessages();
when(mockChannel.query(
options: anyNamed('options'),
messagesPagination: anyNamed('messagesPagination'),
membersPagination: anyNamed('membersPagination'),
watchersPagination: anyNamed('watchersPagination'),
preferOffline: anyNamed('preferOffline'),
)).thenAnswer((_) async => ChannelState(messages: messages));
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: streamChannel,
),
);
await tester.pumpAndSettle();
verify(mockChannel.initialized).called(1);
verify(mockChannel.query(
options: anyNamed('options'),
messagesPagination: anyNamed('messagesPagination'),
membersPagination: anyNamed('membersPagination'),
watchersPagination: anyNamed('watchersPagination'),
preferOffline: anyNamed('preferOffline'),
)).called(
2, // Fetching After messages + Fetching Before messages,
);
},
);
testWidgets(
'should rebuild StreamChannel with updated widget data '
'on calling setState()',
(tester) async {
StateSetter _stateSetter;
var initialMessageId = 'testInitialMessageId';
final mockChannel = MockChannel();
const streamChannelKey = Key('streamChannel');
const childKey = Key('childKey');
StreamChannel streamChannelBuilder(String initialMessageId) =>
StreamChannel(
key: streamChannelKey,
channel: mockChannel,
child: Offstage(key: childKey),
initialMessageId: initialMessageId,
);
final beforePagination = PaginationParams(
lessThan: initialMessageId,
limit: 20,
);
final afterPagination = PaginationParams(
greaterThanOrEqual: initialMessageId,
limit: 20,
);
when(mockChannel.initialized).thenAnswer((_) async => true);
final messages = _generateMessages();
when(mockChannel.query(
options: anyNamed('options'),
messagesPagination: beforePagination,
membersPagination: anyNamed('membersPagination'),
watchersPagination: anyNamed('watchersPagination'),
preferOffline: anyNamed('preferOffline'),
)).thenAnswer((_) async => ChannelState(messages: messages));
when(mockChannel.query(
options: anyNamed('options'),
messagesPagination: afterPagination,
membersPagination: anyNamed('membersPagination'),
watchersPagination: anyNamed('watchersPagination'),
preferOffline: anyNamed('preferOffline'),
)).thenAnswer((_) async => ChannelState(messages: messages));
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (context, stateSetter) {
// Assigning stateSetter for rebuilding StreamChannel
_stateSetter = stateSetter;
return streamChannelBuilder(initialMessageId);
},
),
),
);
await tester.pumpAndSettle();
verify(mockChannel.query(
options: anyNamed('options'),
messagesPagination: beforePagination,
membersPagination: anyNamed('membersPagination'),
watchersPagination: anyNamed('watchersPagination'),
preferOffline: anyNamed('preferOffline'),
)).called(1);
verify(mockChannel.query(
options: anyNamed('options'),
messagesPagination: afterPagination,
membersPagination: anyNamed('membersPagination'),
watchersPagination: anyNamed('watchersPagination'),
preferOffline: anyNamed('preferOffline'),
)).called(1);
_stateSetter(() => initialMessageId = 'testInitialMessageId2');
final updatedBeforePagination = beforePagination.copyWith(
lessThan: initialMessageId,
);
final updatedAfterPagination = afterPagination.copyWith(
greaterThanOrEqual: initialMessageId,
);
when(mockChannel.query(
options: anyNamed('options'),
messagesPagination: updatedBeforePagination,
membersPagination: anyNamed('membersPagination'),
watchersPagination: anyNamed('watchersPagination'),
preferOffline: anyNamed('preferOffline'),
)).thenAnswer((_) async => ChannelState(messages: messages));
when(mockChannel.query(
options: anyNamed('options'),
messagesPagination: updatedAfterPagination,
membersPagination: anyNamed('membersPagination'),
watchersPagination: anyNamed('watchersPagination'),
preferOffline: anyNamed('preferOffline'),
)).thenAnswer((_) async => ChannelState(messages: messages));
await tester.pumpAndSettle();
verify(mockChannel.query(
options: anyNamed('options'),
messagesPagination: updatedBeforePagination,
membersPagination: anyNamed('membersPagination'),
watchersPagination: anyNamed('watchersPagination'),
preferOffline: anyNamed('preferOffline'),
)).called(1);
verify(mockChannel.query(
options: anyNamed('options'),
messagesPagination: updatedAfterPagination,
membersPagination: anyNamed('membersPagination'),
watchersPagination: anyNamed('watchersPagination'),
preferOffline: anyNamed('preferOffline'),
)).called(1);
},
);
}
@@ -1,181 +1,289 @@
import 'dart:async';
import 'package:fake_async/fake_async.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'package:mockito/mockito.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'mocks.dart';
class MockShowLocalNotifications extends Mock {
class MockOnBackgroundEventReceived extends Mock {
void call(Event event);
}
void main() {
testWidgets(
'StreamChatCore.of(context) should throw an exception',
(WidgetTester tester) async {
await tester.pumpWidget(
Builder(
builder: (context) {
expect(() => StreamChatCore.of(context), throwsException);
return Container();
},
),
);
test(
'should throw assertion error in case client is null',
() {
final streamChatCore = () => StreamChatCore(
client: null,
child: Offstage(),
);
expect(streamChatCore, throwsA(isA<AssertionError>()));
},
);
test(
'should throw assertion error in case child is null',
() {
final mockClient = MockClient();
final streamChatCore = () => StreamChatCore(
client: mockClient,
child: null,
);
expect(streamChatCore, throwsA(isA<AssertionError>()));
},
);
testWidgets(
'StreamChatCore.of(context) should return the StreamChatCore ancestor',
(WidgetTester tester) async {
final client = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: client,
child: Builder(
builder: (context) {
final sc = StreamChatCore.of(context);
expect(sc, isNotNull);
return Container();
},
),
),
'should render StreamChatCore if both client and child is provided',
(tester) async {
final mockClient = MockClient();
const streamChatCoreKey = Key('streamChatCore');
const childKey = Key('child');
final streamChatCore = StreamChatCore(
key: streamChatCoreKey,
client: mockClient,
child: Offstage(key: childKey),
);
await tester.pumpWidget(streamChatCore);
expect(find.byKey(streamChatCoreKey), findsOneWidget);
expect(find.byKey(childKey), findsOneWidget);
},
);
testWidgets(
'StreamChatCore.of(context).client should return the client',
(WidgetTester tester) async {
final client = MockClient();
final clientState = MockClientState();
when(client.state).thenReturn(clientState);
when(clientState.user).thenReturn(
OwnUser(
id: 'test',
),
);
final userStream = Stream<OwnUser>.value(
OwnUser(
id: 'test',
),
);
when(clientState.userStream).thenAnswer(
(_) => userStream,
'should render StreamChatCore if both client and child is provided',
(tester) async {
final mockClient = MockClient();
const streamChatCoreKey = Key('streamChatCore');
const childKey = Key('child');
final streamChatCore = StreamChatCore(
key: streamChatCoreKey,
client: mockClient,
child: Offstage(key: childKey),
);
await tester.pumpWidget(
StreamChatCore(
client: client,
child: Builder(
builder: (context) {
final sc = StreamChatCore.of(context);
expect(sc.client, client);
expect(sc.user, client.state.user);
expect(sc.userStream, userStream);
return Container();
},
),
),
);
await tester.pumpWidget(streamChatCore);
expect(find.byKey(streamChatCoreKey), findsOneWidget);
expect(find.byKey(childKey), findsOneWidget);
},
);
testWidgets(
'StreamChatCore should disconnect on background',
(WidgetTester tester) async {
fakeAsync((_async) {
final client = MockClient();
final clientState = MockClientState();
final channel = MockChannel();
when(client.state).thenReturn(clientState);
when(clientState.user).thenReturn(
OwnUser(
id: 'test',
),
);
final showLocalNotificationMock = MockShowLocalNotifications().call;
final eventStreamController = StreamController<Event>();
when(client.on()).thenAnswer((_) => eventStreamController.stream);
'didChangeAppLifecycleState should call client.disconnect() and return '
'if onBackgroundEventReceived is null and the widget lifestyle changes to '
'AppLifecycleState.paused',
(tester) async {
final mockClient = MockClient();
const streamChatCoreKey = Key('streamChatCore');
const childKey = Key('child');
final streamChatCore = StreamChatCore(
key: streamChatCoreKey,
client: mockClient,
child: Offstage(key: childKey),
);
when(client.channel('test', id: 'testid')).thenReturn(channel);
await tester.pumpWidget(streamChatCore);
final scKey = GlobalKey<StreamChatCoreState>();
tester.pumpWidget(
StreamChatCore(
key: scKey,
client: client,
onBackgroundEventReceived: showLocalNotificationMock,
backgroundKeepAlive: const Duration(seconds: 4),
child: Builder(
builder: (context) {
return Container();
},
),
),
expect(find.byKey(streamChatCoreKey), findsOneWidget);
expect(find.byKey(childKey), findsOneWidget);
when(mockClient.disconnect()).thenAnswer((_) async {
return;
});
final streamChatCoreState = tester.state<StreamChatCoreState>(
find.byKey(streamChatCoreKey),
);
streamChatCoreState.didChangeAppLifecycleState(AppLifecycleState.paused);
verify(mockClient.disconnect()).called(1);
},
);
testWidgets(
'didChangeAppLifecycleState should subscribe to client events for '
'backgroundKeepAlive duration if onBackgroundEventReceived is provided '
'and the widget lifestyle changes to AppLifecycleState.paused',
(tester) async {
await tester.runAsync(() async {
final mockClient = MockClient();
final mockOnBackgroundEventReceived = MockOnBackgroundEventReceived();
const backgroundKeepAlive = const Duration(seconds: 3);
const streamChatCoreKey = Key('streamChatCore');
const childKey = Key('child');
final streamChatCore = StreamChatCore(
key: streamChatCoreKey,
client: mockClient,
child: Offstage(key: childKey),
onBackgroundEventReceived: mockOnBackgroundEventReceived,
backgroundKeepAlive: backgroundKeepAlive,
);
final sc = scKey.currentState;
sc.didChangeAppLifecycleState(AppLifecycleState.paused);
await tester.pumpWidget(streamChatCore);
_async.elapse(Duration(seconds: 5));
expect(find.byKey(streamChatCoreKey), findsOneWidget);
expect(find.byKey(childKey), findsOneWidget);
verify(client.disconnect()).called(1);
eventStreamController.close();
final event = Event();
when(mockClient.on()).thenAnswer((_) => Stream.value(event));
when(mockClient.disconnect()).thenAnswer((_) async {
return;
});
final streamChatCoreState = tester.state<StreamChatCoreState>(
find.byKey(streamChatCoreKey),
);
streamChatCoreState
.didChangeAppLifecycleState(AppLifecycleState.paused);
await untilCalled(mockOnBackgroundEventReceived.call(event));
verify(mockOnBackgroundEventReceived.call(event)).called(1);
await Future.delayed(backgroundKeepAlive);
verify(mockClient.disconnect()).called(1);
verifyNever(mockOnBackgroundEventReceived.call(event));
});
},
);
testWidgets(
'StreamChatCore should handle notifications when on background and connected',
(WidgetTester tester) async {
final client = MockClient();
final clientState = MockClientState();
final channel = MockChannel();
when(client.state).thenReturn(clientState);
when(clientState.user).thenReturn(
OwnUser(
id: 'test',
),
);
final showLocalNotificationMock = MockShowLocalNotifications().call;
final eventStreamController = StreamController<Event>();
when(client.on()).thenAnswer((_) => eventStreamController.stream);
'didChangeAppLifecycleState should cancel the backgroundKeepAlive timer '
'if it is currently running in case the widget lifestyle changes to '
'AppLifecycleState.resume',
(tester) async {
await tester.runAsync(() async {
final mockClient = MockClient();
final mockOnBackgroundEventReceived = MockOnBackgroundEventReceived();
const backgroundKeepAlive = const Duration(seconds: 3);
const streamChatCoreKey = Key('streamChatCore');
const childKey = Key('child');
final streamChatCore = StreamChatCore(
key: streamChatCoreKey,
client: mockClient,
child: Offstage(key: childKey),
onBackgroundEventReceived: mockOnBackgroundEventReceived,
backgroundKeepAlive: backgroundKeepAlive,
);
when(client.channel('test', id: 'testid')).thenReturn(channel);
await tester.pumpWidget(streamChatCore);
final scKey = GlobalKey<StreamChatCoreState>();
await tester.pumpWidget(
StreamChatCore(
key: scKey,
client: client,
onBackgroundEventReceived: showLocalNotificationMock,
backgroundKeepAlive: const Duration(seconds: 4),
child: Builder(
builder: (context) {
return Container();
},
),
),
);
expect(find.byKey(streamChatCoreKey), findsOneWidget);
expect(find.byKey(childKey), findsOneWidget);
final sc = scKey.currentState;
sc.didChangeAppLifecycleState(AppLifecycleState.paused);
final event = Event(
type: EventType.messageNew,
message: Message(text: 'hey'),
channelType: 'test',
channelId: 'testid',
user: User(id: 'other user'),
);
eventStreamController.add(event);
final event = Event();
when(mockClient.on()).thenAnswer((_) => Stream.value(event));
await untilCalled(showLocalNotificationMock(event));
final streamChatCoreState = tester.state<StreamChatCoreState>(
find.byKey(streamChatCoreKey),
);
verify(showLocalNotificationMock(event)).called(1);
eventStreamController.close();
streamChatCoreState
.didChangeAppLifecycleState(AppLifecycleState.paused);
await untilCalled(mockOnBackgroundEventReceived.call(event));
verify(mockOnBackgroundEventReceived.call(event)).called(1);
streamChatCoreState
.didChangeAppLifecycleState(AppLifecycleState.resumed);
verifyNever(mockOnBackgroundEventReceived.call(event));
});
},
);
testWidgets(
'didChangeAppLifecycleState should call client.connect() '
'if the connectionStatus is ConnectionStatus.disconnected in case the '
'widget lifestyle changes to AppLifecycleState.resume',
(tester) async {
await tester.runAsync(() async {
final mockClient = MockClient();
const streamChatCoreKey = Key('streamChatCore');
const childKey = Key('child');
final streamChatCore = StreamChatCore(
key: streamChatCoreKey,
client: mockClient,
child: Offstage(key: childKey),
);
await tester.pumpWidget(streamChatCore);
expect(find.byKey(streamChatCoreKey), findsOneWidget);
expect(find.byKey(childKey), findsOneWidget);
final event = Event();
when(mockClient.on()).thenAnswer((_) => Stream.value(event));
when(mockClient.connect()).thenAnswer((_) async => event);
when(mockClient.wsConnectionStatus)
.thenReturn(ConnectionStatus.disconnected);
final streamChatCoreState = tester.state<StreamChatCoreState>(
find.byKey(streamChatCoreKey),
);
streamChatCoreState
.didChangeAppLifecycleState(AppLifecycleState.paused);
await Future.delayed(const Duration(seconds: 1));
streamChatCoreState
.didChangeAppLifecycleState(AppLifecycleState.resumed);
verify(mockClient.connect()).called(1);
});
},
);
testWidgets(
'streamChatCoreState.userStream should emit all the user events '
'provided by client',
(tester) async {
await tester.runAsync(() async {
final userController = StreamController<OwnUser>();
final mockClient = MockClient();
const streamChatCoreKey = Key('streamChatCore');
const childKey = Key('child');
final streamChatCore = StreamChatCore(
key: streamChatCoreKey,
client: mockClient,
child: Offstage(key: childKey),
);
await tester.pumpWidget(streamChatCore);
expect(find.byKey(streamChatCoreKey), findsOneWidget);
expect(find.byKey(childKey), findsOneWidget);
when(mockClient.state.userStream)
.thenAnswer((_) => userController.stream);
final streamChatCoreState = tester.state<StreamChatCoreState>(
find.byKey(streamChatCoreKey),
);
final ownUser = OwnUser(id: 'testUserId');
userController.add(ownUser);
await expectLater(
streamChatCoreState.userStream,
emits(ownUser),
);
addTearDown(() {
userController.close();
});
});
},
);
}
@@ -0,0 +1,572 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:stream_chat_flutter_core/src/user_list_core.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'mocks.dart';
void main() {
const alphabets = 'abcdefghijklmnopqrstuvwxyz';
List<User> _generateUsers({
int count = 3,
int offset = 0,
}) {
return List.generate(
count,
(index) {
index = index + offset;
return User(
id: 'testId$index',
role: 'testRole$index',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
lastActive: DateTime.now(),
online: true,
banned: false,
extraData: {
'name': '${alphabets[index]}-testName',
},
);
},
);
}
test(
'should throw assertion error in case listBuilder is null',
() {
final userListCore = () => UserListCore(
listBuilder: null,
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (Object error) => Offstage(),
);
expect(userListCore, throwsA(isA<AssertionError>()));
},
);
test(
'should throw assertion error in case loadingBuilder is null',
() {
final userListCore = () => UserListCore(
listBuilder: (_, __) => Offstage(),
loadingBuilder: null,
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (Object error) => Offstage(),
);
expect(userListCore, throwsA(isA<AssertionError>()));
},
);
test(
'should throw assertion error in case emptyBuilder is null',
() {
final userListCore = () => UserListCore(
listBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: null,
errorBuilder: (Object error) => Offstage(),
);
expect(userListCore, throwsA(isA<AssertionError>()));
},
);
test(
'should throw assertion error in case errorBuilder is null',
() {
final userListCore = () => UserListCore(
listBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: null,
);
expect(userListCore, throwsA(isA<AssertionError>()));
},
);
testWidgets(
'should throw if UserListCore is used where UsersBloc is not present '
'in the widget tree',
(tester) async {
const userListCoreKey = Key('userListCore');
final userListCore = UserListCore(
key: userListCoreKey,
listBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (Object error) => Offstage(),
);
await tester.pumpWidget(userListCore);
expect(find.byKey(userListCoreKey), findsNothing);
expect(tester.takeException(), isInstanceOf<Exception>());
},
);
testWidgets(
'should render UserListCore if used with UsersBloc as an ancestor',
(tester) async {
const userListCoreKey = Key('userListCore');
final userListCore = UserListCore(
key: userListCoreKey,
listBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (Object error) => Offstage(),
);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: UsersBloc(
child: userListCore,
),
),
);
expect(find.byKey(userListCoreKey), findsOneWidget);
},
);
testWidgets(
'should assign loadData and paginateData callback to '
'UserListController if passed',
(tester) async {
const userListCoreKey = Key('userListCore');
final controller = UserListController();
final userListCore = UserListCore(
key: userListCoreKey,
listBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (Object error) => Offstage(),
userListController: controller,
);
expect(controller.loadData, isNull);
expect(controller.paginateData, isNull);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: UsersBloc(
child: userListCore,
),
),
);
expect(find.byKey(userListCoreKey), findsOneWidget);
expect(controller.loadData, isNotNull);
expect(controller.paginateData, isNotNull);
},
);
testWidgets(
'should build error widget if usersBlocState.usersStream emits error',
(tester) async {
const userListCoreKey = Key('userListCore');
const errorWidgetKey = Key('errorWidget');
final userListCore = UserListCore(
key: userListCoreKey,
listBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (Object error) => Container(key: errorWidgetKey),
);
final mockClient = MockClient();
const error = 'Error! Error! Error!';
when(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).thenThrow(error);
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: UsersBloc(
child: userListCore,
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(errorWidgetKey), findsOneWidget);
verify(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).called(1);
},
);
testWidgets(
'should build empty widget if usersBlocState.usersStream emits empty data',
(tester) async {
const userListCoreKey = Key('userListCore');
const emptyWidgetKey = Key('emptyWidget');
final userListCore = UserListCore(
key: userListCoreKey,
listBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Container(key: emptyWidgetKey),
errorBuilder: (Object error) => Offstage(),
);
final mockClient = MockClient();
const users = <User>[];
when(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).thenAnswer((_) async => QueryUsersResponse()..users = users);
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: UsersBloc(
child: userListCore,
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(emptyWidgetKey), findsOneWidget);
verify(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).called(1);
},
);
testWidgets(
'should build list widget if usersBlocState.usersStream emits some data',
(tester) async {
const userListCoreKey = Key('userListCore');
const listWidgetKey = Key('listWidget');
final userListCore = UserListCore(
key: userListCoreKey,
listBuilder: (_, __) => Container(key: listWidgetKey),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (Object error) => Offstage(),
);
final mockClient = MockClient();
final users = _generateUsers();
when(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).thenAnswer((_) async => QueryUsersResponse()..users = users);
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: UsersBloc(
child: userListCore,
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(listWidgetKey), findsOneWidget);
verify(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).called(1);
},
);
testWidgets(
'should build list widget with grouped data if groupAlphabetically is true',
(tester) async {
const userListCoreKey = Key('userListCore');
const listWidgetKey = Key('listWidget');
final userListCore = UserListCore(
key: userListCoreKey,
listBuilder: (_, items) => Container(
key: listWidgetKey,
child: ListView(
children: items.map((e) {
return Container(
key: Key(e.key),
child: e.when(
headerItem: (heading) => Text(heading),
userItem: (user) => Text(user.id),
),
);
}).toList(growable: false),
),
),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (Object error) => Offstage(),
groupAlphabetically: true,
);
final mockClient = MockClient();
final users = _generateUsers();
when(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).thenAnswer((_) async => QueryUsersResponse()..users = users);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StreamChatCore(
client: mockClient,
child: UsersBloc(
child: userListCore,
),
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(listWidgetKey), findsOneWidget);
for (final user in users) {
expect(find.byKey(Key('HEADER-${user.name[0]}')), findsOneWidget);
expect(find.byKey(Key('USER-${user.id}')), findsOneWidget);
}
verify(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).called(1);
},
);
testWidgets(
'should build list widget with paginated data '
'on calling channelListCoreState.paginateData',
(tester) async {
const userListCoreKey = Key('userListCore');
const listWidgetKey = Key('listWidget');
const pagination = PaginationParams();
final userListCore = UserListCore(
key: userListCoreKey,
listBuilder: (_, items) => Container(
key: listWidgetKey,
child: ListView(
children: items.map((e) {
return Container(
key: Key(e.key),
child: e.when(
headerItem: (heading) => Text(heading),
userItem: (user) => Text(user.id),
),
);
}).toList(growable: false),
),
),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (Object error) => Offstage(),
pagination: pagination,
groupAlphabetically: true,
);
final mockClient = MockClient();
final users = _generateUsers();
when(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).thenAnswer((_) async => QueryUsersResponse()..users = users);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StreamChatCore(
client: mockClient,
child: UsersBloc(
child: userListCore,
),
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(listWidgetKey), findsOneWidget);
for (final user in users) {
expect(find.byKey(Key('HEADER-${user.name[0]}')), findsOneWidget);
expect(find.byKey(Key('USER-${user.id}')), findsOneWidget);
}
verify(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).called(1);
final userListCoreState = tester.state<UserListCoreState>(
find.byKey(userListCoreKey),
);
final offset = users.length;
final paginatedUsers = _generateUsers(offset: offset);
final updatedPagination = pagination.copyWith(offset: offset);
when(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: updatedPagination,
)).thenAnswer((_) async => QueryUsersResponse()..users = paginatedUsers);
await userListCoreState.paginateData();
await tester.pumpAndSettle();
for (final user in users + paginatedUsers) {
expect(find.byKey(Key('HEADER-${user.name[0]}')), findsOneWidget);
expect(find.byKey(Key('USER-${user.id}')), findsOneWidget);
}
verify(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: updatedPagination,
)).called(1);
},
);
testWidgets(
'should rebuild UserListCore with updated widget data '
'on calling setState()',
(tester) async {
const pagination = PaginationParams();
StateSetter _stateSetter;
int limit = pagination.limit;
const userListCoreKey = Key('userListCore');
const listWidgetKey = Key('listWidget');
UserListCore userListCoreBuilder(int limit) => UserListCore(
key: userListCoreKey,
listBuilder: (_, items) => Container(
key: listWidgetKey,
child: ListView(
children: items.map((e) {
return Container(
key: Key(e.key),
child: e.when(
headerItem: (heading) => Text(heading),
userItem: (user) => Text(user.id),
),
);
}).toList(growable: false),
),
),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (Object error) => Offstage(),
pagination: pagination.copyWith(limit: limit),
groupAlphabetically: true,
);
final mockClient = MockClient();
final users = _generateUsers();
when(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).thenAnswer((_) async => QueryUsersResponse()..users = users);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StreamChatCore(
client: mockClient,
child: UsersBloc(
child: StatefulBuilder(builder: (context, stateSetter) {
// Assigning stateSetter for rebuilding UserListCore
_stateSetter = stateSetter;
return userListCoreBuilder(limit);
}),
),
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(listWidgetKey), findsOneWidget);
for (final user in users) {
expect(find.byKey(Key('HEADER-${user.name[0]}')), findsOneWidget);
expect(find.byKey(Key('USER-${user.id}')), findsOneWidget);
}
verify(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).called(1);
// Rebuilding UserListCore with new pagination limit
_stateSetter(() => limit = 6);
final updatedUsers = _generateUsers(count: limit);
final updatedPagination = pagination.copyWith(limit: limit);
when(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: updatedPagination,
)).thenAnswer((_) async => QueryUsersResponse()..users = updatedUsers);
await tester.pumpAndSettle();
for (final user in updatedUsers) {
expect(find.byKey(Key('HEADER-${user.name[0]}')), findsOneWidget);
expect(find.byKey(Key('USER-${user.id}')), findsOneWidget);
}
verify(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: updatedPagination,
)).called(1);
},
);
}
@@ -0,0 +1,327 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter_core/src/stream_chat_core.dart';
import 'package:stream_chat_flutter_core/src/users_bloc.dart';
import 'package:mockito/mockito.dart';
import 'matchers/users_matcher.dart';
import 'mocks.dart';
void main() {
List<User> _generateUsers({
int count = 3,
int offset = 0,
}) {
return List.generate(
count,
(index) {
index = index + offset;
return User(
id: 'testId$index',
role: 'testRole$index',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
lastActive: DateTime.now(),
online: true,
banned: false,
extraData: {'extra_data_key': 'extraDataValue'},
);
},
);
}
test(
'should throw assertion error if child is null',
() async {
const usersBlocKey = Key('usersBloc');
final usersBloc = () => UsersBloc(
key: usersBlocKey,
child: null,
);
expect(usersBloc, throwsA(isA<AssertionError>()));
},
);
testWidgets(
'usersBlocState.queryUsers() should throw if used where '
'StreamChat is not present in the widget tree',
(tester) async {
const usersBlocKey = Key('usersBloc');
const childKey = Key('child');
final usersBloc = UsersBloc(
key: usersBlocKey,
child: Offstage(key: childKey),
);
await tester.pumpWidget(usersBloc);
expect(find.byKey(usersBlocKey), findsOneWidget);
expect(find.byKey(childKey), findsOneWidget);
final usersBlocState = tester.state<UsersBlocState>(
find.byKey(usersBlocKey),
);
try {
await usersBlocState.queryUsers();
} catch (e) {
expect(e, isInstanceOf<Exception>());
}
},
);
testWidgets(
'usersBlocState.queryUsers() should emit data through usersStream',
(tester) async {
const usersBlocKey = Key('usersBloc');
const childKey = Key('child');
final usersBloc = UsersBloc(
key: usersBlocKey,
child: Offstage(key: childKey),
);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: usersBloc,
),
);
final usersBlocState = tester.state<UsersBlocState>(
find.byKey(usersBlocKey),
);
final users = _generateUsers();
when(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).thenAnswer((_) async => QueryUsersResponse()..users = users);
usersBlocState.queryUsers();
await expectLater(
usersBlocState.usersStream,
emits(isSameUserListAs(users)),
);
verify(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).called(1);
},
);
testWidgets(
'usersBlocState.usersStream should emit error '
'if client.queryUsers() throws',
(tester) async {
const usersBlocKey = Key('usersBloc');
const childKey = Key('child');
final usersBloc = UsersBloc(
key: usersBlocKey,
child: Offstage(key: childKey),
);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: usersBloc,
),
);
final usersBlocState = tester.state<UsersBlocState>(
find.byKey(usersBlocKey),
);
final error = 'Error! Error! Error!';
when(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).thenThrow(error);
usersBlocState.queryUsers();
await expectLater(
usersBlocState.usersStream,
emitsError(error),
);
verify(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).called(1);
},
);
testWidgets(
'calling usersBlocState.queryUsers() again with an offset '
'should emit new data through usersStream and also emit loading state '
'through queryUsersLoading',
(tester) async {
const usersBlocKey = Key('usersBloc');
const childKey = Key('child');
final usersBloc = UsersBloc(
key: usersBlocKey,
child: Offstage(key: childKey),
);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: usersBloc,
),
);
final usersBlocState = tester.state<UsersBlocState>(
find.byKey(usersBlocKey),
);
final users = _generateUsers();
when(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).thenAnswer((_) async => QueryUsersResponse()..users = users);
usersBlocState.queryUsers();
await expectLater(
usersBlocState.usersStream,
emits(isSameUserListAs(users)),
);
verify(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).called(1);
final offset = users.length;
final paginatedUsers = _generateUsers(offset: offset);
final pagination = PaginationParams(offset: offset);
when(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: pagination,
)).thenAnswer((_) async => QueryUsersResponse()..users = paginatedUsers);
usersBlocState.queryUsers(pagination: pagination);
await Future.wait([
expectLater(
usersBlocState.queryUsersLoading,
emitsInOrder([true, false]),
),
expectLater(
usersBlocState.usersStream,
emits(isSameUserListAs(users + paginatedUsers)),
),
]);
verify(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: pagination,
)).called(1);
},
);
testWidgets(
'calling usersBlocState.queryUsers() again with an offset '
'should emit error through queryUsersLoading if '
'client.queryUsers() throws',
(tester) async {
const usersBlocKey = Key('usersBloc');
const childKey = Key('child');
final usersBloc = UsersBloc(
key: usersBlocKey,
child: Offstage(key: childKey),
);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: usersBloc,
),
);
final usersBlocState = tester.state<UsersBlocState>(
find.byKey(usersBlocKey),
);
final users = _generateUsers();
when(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).thenAnswer((_) async => QueryUsersResponse()..users = users);
usersBlocState.queryUsers();
await expectLater(
usersBlocState.usersStream,
emits(isSameUserListAs(users)),
);
verify(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).called(1);
final offset = users.length;
final pagination = PaginationParams(offset: offset);
final error = 'Error! Error! Error!';
when(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: pagination,
)).thenThrow(error);
usersBlocState.queryUsers(pagination: pagination);
await expectLater(
usersBlocState.queryUsersLoading,
emitsError(error),
);
verify(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: pagination,
)).called(1);
},
);
}