Commit c6757570 authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Improve testing of the RefreshIndicator widget. (#8111)

parent 88a01ac4
...@@ -17,8 +17,8 @@ const double _kDragContainerExtentPercentage = 0.25; ...@@ -17,8 +17,8 @@ const double _kDragContainerExtentPercentage = 0.25;
// displacement; max displacement = _kDragSizeFactorLimit * displacement. // displacement; max displacement = _kDragSizeFactorLimit * displacement.
const double _kDragSizeFactorLimit = 1.5; const double _kDragSizeFactorLimit = 1.5;
// How far the indicator must be dragged to trigger the refresh callback.
const double _kDragThresholdFactor = 0.75;
// When the scroll ends, the duration of the refresh indicator's animation // When the scroll ends, the duration of the refresh indicator's animation
// to the RefreshIndicator's displacment. // to the RefreshIndicator's displacment.
...@@ -44,9 +44,6 @@ enum RefreshIndicatorLocation { ...@@ -44,9 +44,6 @@ enum RefreshIndicatorLocation {
/// The refresh indicator will appear at the bottom of the scrollable. /// The refresh indicator will appear at the bottom of the scrollable.
bottom, bottom,
/// The refresh indicator will appear at both ends of the scrollable.
both
} }
// The state machine moves through these modes only when the scrollable // The state machine moves through these modes only when the scrollable
...@@ -56,12 +53,12 @@ enum _RefreshIndicatorMode { ...@@ -56,12 +53,12 @@ enum _RefreshIndicatorMode {
armed, // Dragged far enough that an up event will run the refresh callback. armed, // Dragged far enough that an up event will run the refresh callback.
snap, // Animating to the indicator's final "displacement". snap, // Animating to the indicator's final "displacement".
refresh, // Running the refresh callback. refresh, // Running the refresh callback.
dismiss // Animating the indicator's fade-out. dismiss, // Animating the indicator's fade-out.
} }
enum _DismissTransition { enum _DismissTransition {
shrink, // Refresh callback completed, scale the indicator to 0. shrink, // Refresh callback completed, scale the indicator to 0.
slide // No refresh, translate the indicator out of view. slide, // No refresh, translate the indicator out of view.
} }
/// A widget that supports the Material "swipe to refresh" idiom. /// A widget that supports the Material "swipe to refresh" idiom.
...@@ -191,8 +188,6 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS ...@@ -191,8 +188,6 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS
return scrollOffset <= minScrollOffset; return scrollOffset <= minScrollOffset;
case RefreshIndicatorLocation.bottom: case RefreshIndicatorLocation.bottom:
return scrollOffset >= maxScrollOffset; return scrollOffset >= maxScrollOffset;
case RefreshIndicatorLocation.both:
return scrollOffset <= minScrollOffset || scrollOffset >= maxScrollOffset;
} }
return false; return false;
} }
...@@ -206,14 +201,6 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS ...@@ -206,14 +201,6 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS
return scrollOffset <= minScrollOffset ? -_dragOffset : 0.0; return scrollOffset <= minScrollOffset ? -_dragOffset : 0.0;
case RefreshIndicatorLocation.bottom: case RefreshIndicatorLocation.bottom:
return scrollOffset >= maxScrollOffset ? _dragOffset : 0.0; return scrollOffset >= maxScrollOffset ? _dragOffset : 0.0;
case RefreshIndicatorLocation.both: {
if (scrollOffset <= minScrollOffset)
return -_dragOffset;
else if (scrollOffset >= maxScrollOffset)
return _dragOffset;
else
return 0.0;
}
} }
return 0.0; return 0.0;
} }
...@@ -266,6 +253,8 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS ...@@ -266,6 +253,8 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS
// Stop showing the refresh indicator // Stop showing the refresh indicator
Future<Null> _dismiss(_DismissTransition transition) async { Future<Null> _dismiss(_DismissTransition transition) async {
// This can only be called from _show() when refreshing
// and _handlePointerUp when dragging.
setState(() { setState(() {
_mode = _RefreshIndicatorMode.dismiss; _mode = _RefreshIndicatorMode.dismiss;
}); });
...@@ -284,38 +273,44 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS ...@@ -284,38 +273,44 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS
} }
} }
Future<Null> _show() async { void _show() {
assert(_mode != _RefreshIndicatorMode.refresh);
assert(_mode != _RefreshIndicatorMode.snap);
Completer<Null> completer = new Completer<Null>();
_pendingRefreshFuture = completer.future;
_mode = _RefreshIndicatorMode.snap; _mode = _RefreshIndicatorMode.snap;
await _sizeController.animateTo(1.0 / _kDragSizeFactorLimit, duration: _kIndicatorSnapDuration); _sizeController
.animateTo(1.0 / _kDragSizeFactorLimit, duration: _kIndicatorSnapDuration)
.whenComplete(() {
if (mounted && _mode == _RefreshIndicatorMode.snap) { if (mounted && _mode == _RefreshIndicatorMode.snap) {
assert(config.refresh != null); assert(config.refresh != null);
setState(() { setState(() {
_mode = _RefreshIndicatorMode.refresh; // Show the indeterminate progress indicator. // Show the indeterminate progress indicator.
_mode = _RefreshIndicatorMode.refresh;
}); });
// Only one refresh callback is allowed to run at a time. If the user config.refresh().whenComplete(() {
// attempts to start a refresh while one is still running ("pending") we if (mounted && _mode == _RefreshIndicatorMode.refresh) {
// just continue to wait on the pending refresh. completer.complete();
if (_pendingRefreshFuture == null)
_pendingRefreshFuture = config.refresh();
await _pendingRefreshFuture;
bool completed = _pendingRefreshFuture != null;
_pendingRefreshFuture = null;
if (mounted && completed && _mode == _RefreshIndicatorMode.refresh)
_dismiss(_DismissTransition.slide); _dismiss(_DismissTransition.slide);
} }
});
}
});
} }
Future<Null> _doHandlePointerUp(PointerUpEvent event) async { void _handlePointerUp(PointerEvent event) {
if (_mode == _RefreshIndicatorMode.armed) switch (_mode) {
case _RefreshIndicatorMode.armed:
_show(); _show();
else if (_mode == _RefreshIndicatorMode.drag) break;
case _RefreshIndicatorMode.drag:
_dismiss(_DismissTransition.shrink); _dismiss(_DismissTransition.shrink);
break;
default:
// do nothing
break;
} }
void _handlePointerUp(PointerEvent event) {
_doHandlePointerUp(event);
} }
/// Show the refresh indicator and run the refresh callback as if it had /// Show the refresh indicator and run the refresh callback as if it had
...@@ -324,12 +319,14 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS ...@@ -324,12 +319,14 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS
/// ///
/// Creating the RefreshIndicator with a [GlobalKey<RefreshIndicatorState>] /// Creating the RefreshIndicator with a [GlobalKey<RefreshIndicatorState>]
/// makes it possible to refer to the [RefreshIndicatorState]. /// makes it possible to refer to the [RefreshIndicatorState].
Future<Null> show() async { Future<Null> show() {
if (_mode != _RefreshIndicatorMode.refresh) { if (_mode != _RefreshIndicatorMode.refresh &&
_mode != _RefreshIndicatorMode.snap) {
_sizeController.value = 0.0; _sizeController.value = 0.0;
_scaleController.value = 0.0; _scaleController.value = 0.0;
await _show(); _show();
} }
return _pendingRefreshFuture;
} }
ScrollableEdge get _clampOverscrollsEdge { ScrollableEdge get _clampOverscrollsEdge {
...@@ -338,8 +335,6 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS ...@@ -338,8 +335,6 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS
return ScrollableEdge.leading; return ScrollableEdge.leading;
case RefreshIndicatorLocation.bottom: case RefreshIndicatorLocation.bottom:
return ScrollableEdge.trailing; return ScrollableEdge.trailing;
case RefreshIndicatorLocation.both:
return ScrollableEdge.both;
} }
return ScrollableEdge.none; return ScrollableEdge.none;
} }
...@@ -354,8 +349,7 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS ...@@ -354,8 +349,7 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS
_valueColor = new ColorTween( _valueColor = new ColorTween(
begin: (config.color ?? theme.accentColor).withOpacity(0.0), begin: (config.color ?? theme.accentColor).withOpacity(0.0),
end: (config.color ?? theme.accentColor).withOpacity(1.0) end: (config.color ?? theme.accentColor).withOpacity(1.0)
) ).animate(new CurvedAnimation(
.animate(new CurvedAnimation(
parent: _sizeController, parent: _sizeController,
curve: const Interval(0.0, 1.0 / _kDragSizeFactorLimit) curve: const Interval(0.0, 1.0 / _kDragSizeFactorLimit)
)); ));
......
...@@ -9,15 +9,21 @@ import 'package:flutter/material.dart'; ...@@ -9,15 +9,21 @@ import 'package:flutter/material.dart';
final GlobalKey<ScrollableState> scrollableKey = new GlobalKey<ScrollableState>(); final GlobalKey<ScrollableState> scrollableKey = new GlobalKey<ScrollableState>();
void main() { bool refreshCalled = false;
bool refreshCalled = false;
Future<Null> refresh() { Future<Null> refresh() {
refreshCalled = true; refreshCalled = true;
return new Future<Null>.value(); return new Future<Null>.value();
} }
Future<Null> holdRefresh() {
refreshCalled = true;
return new Completer<Null>().future;
}
void main() {
testWidgets('RefreshIndicator', (WidgetTester tester) async { testWidgets('RefreshIndicator', (WidgetTester tester) async {
refreshCalled = false;
await tester.pumpWidget( await tester.pumpWidget(
new RefreshIndicator( new RefreshIndicator(
scrollableKey: scrollableKey, scrollableKey: scrollableKey,
...@@ -29,16 +35,191 @@ void main() { ...@@ -29,16 +35,191 @@ void main() {
height: 200.0, height: 200.0,
child: new Text(item) child: new Text(item)
); );
}).toList() }).toList(),
) ),
) ),
);
await tester.fling(find.text('A'), const Offset(0.0, 300.0), 1000.0);
await tester.pump();
await tester.pump(const Duration(seconds: 1)); // finish the scroll animation
await tester.pump(const Duration(seconds: 1)); // finish the indicator settle animation
await tester.pump(const Duration(seconds: 1)); // finish the indicator hide animation
expect(refreshCalled, true);
});
testWidgets('RefreshIndicator - bottom', (WidgetTester tester) async {
refreshCalled = false;
await tester.pumpWidget(
new RefreshIndicator(
scrollableKey: scrollableKey,
refresh: refresh,
location: RefreshIndicatorLocation.bottom,
child: new Block(
scrollableKey: scrollableKey,
children: <Widget>[
new SizedBox(
height: 200.0,
child: new Text('X')
),
],
),
),
); );
await tester.fling(find.text('A'), const Offset(0.0, 300.0), -1000.0); await tester.fling(find.text('X'), const Offset(0.0, -300.0), 1000.0);
await tester.pump(); await tester.pump();
await tester.pump(const Duration(seconds: 1)); // finish the scroll animation await tester.pump(const Duration(seconds: 1)); // finish the scroll animation
await tester.pump(const Duration(seconds: 1)); // finish the indicator settle animation await tester.pump(const Duration(seconds: 1)); // finish the indicator settle animation
await tester.pump(const Duration(seconds: 1)); // finish the indicator hide animation await tester.pump(const Duration(seconds: 1)); // finish the indicator hide animation
expect(refreshCalled, true); expect(refreshCalled, true);
}); });
testWidgets('RefreshIndicator - not enough', (WidgetTester tester) async {
refreshCalled = false;
await tester.pumpWidget(
new RefreshIndicator(
scrollableKey: scrollableKey,
refresh: refresh,
child: new Block(
scrollableKey: scrollableKey,
children: <Widget>[
new SizedBox(
height: 200.0,
child: new Text('X')
),
],
),
),
);
await tester.fling(find.text('X'), const Offset(0.0, 100.0), 1000.0);
await tester.pump();
await tester.pump(const Duration(seconds: 1));
await tester.pump(const Duration(seconds: 1));
await tester.pump(const Duration(seconds: 1));
expect(refreshCalled, false);
});
testWidgets('RefreshIndicator - show - slow', (WidgetTester tester) async {
refreshCalled = false;
await tester.pumpWidget(
new RefreshIndicator(
scrollableKey: scrollableKey,
refresh: holdRefresh, // this one never returns
child: new Block(
scrollableKey: scrollableKey,
children: <Widget>[
new SizedBox(
height: 200.0,
child: new Text('X')
),
],
),
),
);
bool completed = false;
tester.state<RefreshIndicatorState>(find.byType(RefreshIndicator))
.show()
.then<Null>((Null value) { completed = true; });
await tester.pump();
expect(completed, false);
await tester.pump(const Duration(seconds: 1));
await tester.pump(const Duration(seconds: 1));
await tester.pump(const Duration(seconds: 1));
expect(refreshCalled, true);
expect(completed, false);
completed = false;
refreshCalled = false;
tester.state<RefreshIndicatorState>(find.byType(RefreshIndicator))
.show()
.then<Null>((Null value) { completed = true; });
await tester.pump();
expect(completed, false);
await tester.pump(const Duration(seconds: 1));
await tester.pump(const Duration(seconds: 1));
await tester.pump(const Duration(seconds: 1));
expect(refreshCalled, false);
});
testWidgets('RefreshIndicator - show - fast', (WidgetTester tester) async {
refreshCalled = false;
await tester.pumpWidget(
new RefreshIndicator(
scrollableKey: scrollableKey,
refresh: refresh,
child: new Block(
scrollableKey: scrollableKey,
children: <Widget>[
new SizedBox(
height: 200.0,
child: new Text('X')
),
],
),
),
);
bool completed = false;
tester.state<RefreshIndicatorState>(find.byType(RefreshIndicator))
.show()
.then<Null>((Null value) { completed = true; });
await tester.pump();
expect(completed, false);
await tester.pump(const Duration(seconds: 1));
await tester.pump(const Duration(seconds: 1));
await tester.pump(const Duration(seconds: 1));
expect(refreshCalled, true);
expect(completed, true);
completed = false;
refreshCalled = false;
tester.state<RefreshIndicatorState>(find.byType(RefreshIndicator))
.show()
.then<Null>((Null value) { completed = true; });
await tester.pump();
expect(completed, false);
await tester.pump(const Duration(seconds: 1));
await tester.pump(const Duration(seconds: 1));
await tester.pump(const Duration(seconds: 1));
expect(refreshCalled, true);
expect(completed, true);
});
testWidgets('RefreshIndicator - show - fast - twice', (WidgetTester tester) async {
refreshCalled = false;
await tester.pumpWidget(
new RefreshIndicator(
scrollableKey: scrollableKey,
refresh: refresh,
child: new Block(
scrollableKey: scrollableKey,
children: <Widget>[
new SizedBox(
height: 200.0,
child: new Text('X')
),
],
),
),
);
bool completed1 = false;
tester.state<RefreshIndicatorState>(find.byType(RefreshIndicator))
.show()
.then<Null>((Null value) { completed1 = true; });
bool completed2 = false;
tester.state<RefreshIndicatorState>(find.byType(RefreshIndicator))
.show()
.then<Null>((Null value) { completed2 = true; });
await tester.pump();
expect(completed1, false);
expect(completed2, false);
await tester.pump(const Duration(seconds: 1));
await tester.pump(const Duration(seconds: 1));
await tester.pump(const Duration(seconds: 1));
expect(refreshCalled, true);
expect(completed1, true);
expect(completed2, true);
});
} }
...@@ -308,8 +308,51 @@ void main() { ...@@ -308,8 +308,51 @@ void main() {
expect(find.text('RIGHT CHILD'), findsNothing); expect(find.text('RIGHT CHILD'), findsNothing);
}); });
testWidgets('TabBar left/right fling reverse (1)', (WidgetTester tester) async {
List<String> tabs = <String>['LEFT', 'RIGHT'];
await tester.pumpWidget(buildLeftRightApp(tabs: tabs, value: 'LEFT'));
expect(find.text('LEFT'), findsOneWidget);
expect(find.text('RIGHT'), findsOneWidget);
expect(find.text('LEFT CHILD'), findsOneWidget);
expect(find.text('RIGHT CHILD'), findsNothing);
TabController controller = DefaultTabController.of(tester.element(find.text('LEFT')));
expect(controller.index, 0);
Point flingStart = tester.getCenter(find.text('LEFT CHILD'));
await tester.flingFrom(flingStart, const Offset(200.0, 0.0), 10000.0);
await tester.pump();
await tester.pump(const Duration(seconds: 1)); // finish the scroll animation
expect(controller.index, 0);
expect(find.text('LEFT CHILD'), findsOneWidget);
expect(find.text('RIGHT CHILD'), findsNothing);
});
testWidgets('TabBar left/right fling reverse (2)', (WidgetTester tester) async {
List<String> tabs = <String>['LEFT', 'RIGHT'];
await tester.pumpWidget(buildLeftRightApp(tabs: tabs, value: 'LEFT'));
expect(find.text('LEFT'), findsOneWidget);
expect(find.text('RIGHT'), findsOneWidget);
expect(find.text('LEFT CHILD'), findsOneWidget);
expect(find.text('RIGHT CHILD'), findsNothing);
TabController controller = DefaultTabController.of(tester.element(find.text('LEFT')));
expect(controller.index, 0);
Point flingStart = tester.getCenter(find.text('LEFT CHILD'));
await tester.flingFrom(flingStart, const Offset(-200.0, 0.0), 10000.0);
await tester.pump();
// this is similar to a test above, but that one does many more pumps
await tester.pump(const Duration(seconds: 1)); // finish the scroll animation
expect(controller.index, 1);
expect(find.text('LEFT CHILD'), findsNothing);
expect(find.text('RIGHT CHILD'), findsOneWidget);
});
// A regression test for https://github.com/flutter/flutter/issues/5095 // A regression test for https://github.com/flutter/flutter/issues/5095
testWidgets('TabBar left/right fling reverse', (WidgetTester tester) async { testWidgets('TabBar left/right fling reverse (2)', (WidgetTester tester) async {
List<String> tabs = <String>['LEFT', 'RIGHT']; List<String> tabs = <String>['LEFT', 'RIGHT'];
await tester.pumpWidget(buildLeftRightApp(tabs: tabs, value: 'LEFT')); await tester.pumpWidget(buildLeftRightApp(tabs: tabs, value: 'LEFT'));
...@@ -321,11 +364,20 @@ void main() { ...@@ -321,11 +364,20 @@ void main() {
TabController controller = DefaultTabController.of(tester.element(find.text('LEFT'))); TabController controller = DefaultTabController.of(tester.element(find.text('LEFT')));
expect(controller.index, 0); expect(controller.index, 0);
Point flingStart = tester.getCenter(find.text('LEFT CHILD'));
TestGesture gesture = await tester.startGesture(flingStart);
for (int index = 0; index > 50; index += 1) {
await gesture.moveBy(const Offset(-10.0, 0.0));
await tester.pump(const Duration(milliseconds: 1));
}
// End the fling by reversing direction. This should cause not cause // End the fling by reversing direction. This should cause not cause
// a change to the selected tab, everything should just settle back to // a change to the selected tab, everything should just settle back to
// to where it started. // to where it started.
Point flingStart = tester.getCenter(find.text('LEFT CHILD')); for (int index = 0; index > 50; index += 1) {
await tester.flingFrom(flingStart, const Offset(-200.0, 0.0), -10000.0); await gesture.moveBy(const Offset(10.0, 0.0));
await tester.pump(const Duration(milliseconds: 1));
}
await gesture.up();
await tester.pump(); await tester.pump();
await tester.pump(const Duration(seconds: 1)); // finish the scroll animation await tester.pump(const Duration(seconds: 1)); // finish the scroll animation
expect(controller.index, 0); expect(controller.index, 0);
......
...@@ -313,9 +313,9 @@ class WidgetController { ...@@ -313,9 +313,9 @@ class WidgetController {
/// then one frame is pumped each time that amount of time elapses while /// then one frame is pumped each time that amount of time elapses while
/// sending events, or each time an event is synthesised, whichever is rarer. /// sending events, or each time an event is synthesised, whichever is rarer.
Future<Null> flingFrom(Point startLocation, Offset offset, double velocity, { int pointer: 1, Duration frameInterval: const Duration(milliseconds: 16) }) { Future<Null> flingFrom(Point startLocation, Offset offset, double velocity, { int pointer: 1, Duration frameInterval: const Duration(milliseconds: 16) }) {
return TestAsyncUtils.guard(() async {
assert(offset.distance > 0.0); assert(offset.distance > 0.0);
assert(velocity != 0.0); // velocity is pixels/second assert(velocity > 0.0); // velocity is pixels/second
return TestAsyncUtils.guard(() async {
final TestPointer p = new TestPointer(pointer); final TestPointer p = new TestPointer(pointer);
final HitTestResult result = hitTestOnBinding(startLocation); final HitTestResult result = hitTestOnBinding(startLocation);
const int kMoveCount = 50; // Needs to be >= kHistorySize, see _LeastSquaresVelocityTrackerStrategy const int kMoveCount = 50; // Needs to be >= kHistorySize, see _LeastSquaresVelocityTrackerStrategy
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment