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;
// displacement; max displacement = _kDragSizeFactorLimit * displacement.
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
// to the RefreshIndicator's displacment.
......@@ -44,9 +44,6 @@ enum RefreshIndicatorLocation {
/// The refresh indicator will appear at the bottom of the scrollable.
bottom,
/// The refresh indicator will appear at both ends of the scrollable.
both
}
// The state machine moves through these modes only when the scrollable
......@@ -56,12 +53,12 @@ enum _RefreshIndicatorMode {
armed, // Dragged far enough that an up event will run the refresh callback.
snap, // Animating to the indicator's final "displacement".
refresh, // Running the refresh callback.
dismiss // Animating the indicator's fade-out.
dismiss, // Animating the indicator's fade-out.
}
enum _DismissTransition {
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.
......@@ -191,8 +188,6 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS
return scrollOffset <= minScrollOffset;
case RefreshIndicatorLocation.bottom:
return scrollOffset >= maxScrollOffset;
case RefreshIndicatorLocation.both:
return scrollOffset <= minScrollOffset || scrollOffset >= maxScrollOffset;
}
return false;
}
......@@ -206,14 +201,6 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS
return scrollOffset <= minScrollOffset ? -_dragOffset : 0.0;
case RefreshIndicatorLocation.bottom:
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;
}
......@@ -266,6 +253,8 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS
// Stop showing the refresh indicator
Future<Null> _dismiss(_DismissTransition transition) async {
// This can only be called from _show() when refreshing
// and _handlePointerUp when dragging.
setState(() {
_mode = _RefreshIndicatorMode.dismiss;
});
......@@ -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;
await _sizeController.animateTo(1.0 / _kDragSizeFactorLimit, duration: _kIndicatorSnapDuration);
if (mounted && _mode == _RefreshIndicatorMode.snap) {
assert(config.refresh != null);
setState(() {
_mode = _RefreshIndicatorMode.refresh; // Show the indeterminate progress indicator.
_sizeController
.animateTo(1.0 / _kDragSizeFactorLimit, duration: _kIndicatorSnapDuration)
.whenComplete(() {
if (mounted && _mode == _RefreshIndicatorMode.snap) {
assert(config.refresh != null);
setState(() {
// Show the indeterminate progress indicator.
_mode = _RefreshIndicatorMode.refresh;
});
config.refresh().whenComplete(() {
if (mounted && _mode == _RefreshIndicatorMode.refresh) {
completer.complete();
_dismiss(_DismissTransition.slide);
}
});
}
});
// Only one refresh callback is allowed to run at a time. If the user
// attempts to start a refresh while one is still running ("pending") we
// just continue to wait on the pending refresh.
if (_pendingRefreshFuture == null)
_pendingRefreshFuture = config.refresh();
await _pendingRefreshFuture;
bool completed = _pendingRefreshFuture != null;
_pendingRefreshFuture = null;
if (mounted && completed && _mode == _RefreshIndicatorMode.refresh)
_dismiss(_DismissTransition.slide);
}
}
Future<Null> _doHandlePointerUp(PointerUpEvent event) async {
if (_mode == _RefreshIndicatorMode.armed)
_show();
else if (_mode == _RefreshIndicatorMode.drag)
_dismiss(_DismissTransition.shrink);
}
void _handlePointerUp(PointerEvent event) {
_doHandlePointerUp(event);
switch (_mode) {
case _RefreshIndicatorMode.armed:
_show();
break;
case _RefreshIndicatorMode.drag:
_dismiss(_DismissTransition.shrink);
break;
default:
// do nothing
break;
}
}
/// Show the refresh indicator and run the refresh callback as if it had
......@@ -324,12 +319,14 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS
///
/// Creating the RefreshIndicator with a [GlobalKey<RefreshIndicatorState>]
/// makes it possible to refer to the [RefreshIndicatorState].
Future<Null> show() async {
if (_mode != _RefreshIndicatorMode.refresh) {
Future<Null> show() {
if (_mode != _RefreshIndicatorMode.refresh &&
_mode != _RefreshIndicatorMode.snap) {
_sizeController.value = 0.0;
_scaleController.value = 0.0;
await _show();
_show();
}
return _pendingRefreshFuture;
}
ScrollableEdge get _clampOverscrollsEdge {
......@@ -338,8 +335,6 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS
return ScrollableEdge.leading;
case RefreshIndicatorLocation.bottom:
return ScrollableEdge.trailing;
case RefreshIndicatorLocation.both:
return ScrollableEdge.both;
}
return ScrollableEdge.none;
}
......@@ -354,8 +349,7 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS
_valueColor = new ColorTween(
begin: (config.color ?? theme.accentColor).withOpacity(0.0),
end: (config.color ?? theme.accentColor).withOpacity(1.0)
)
.animate(new CurvedAnimation(
).animate(new CurvedAnimation(
parent: _sizeController,
curve: const Interval(0.0, 1.0 / _kDragSizeFactorLimit)
));
......
......@@ -9,15 +9,21 @@ import 'package:flutter/material.dart';
final GlobalKey<ScrollableState> scrollableKey = new GlobalKey<ScrollableState>();
void main() {
bool refreshCalled = false;
bool refreshCalled = false;
Future<Null> refresh() {
refreshCalled = true;
return new Future<Null>.value();
}
Future<Null> refresh() {
refreshCalled = true;
return new Future<Null>.value();
}
Future<Null> holdRefresh() {
refreshCalled = true;
return new Completer<Null>().future;
}
void main() {
testWidgets('RefreshIndicator', (WidgetTester tester) async {
refreshCalled = false;
await tester.pumpWidget(
new RefreshIndicator(
scrollableKey: scrollableKey,
......@@ -29,16 +35,191 @@ void main() {
height: 200.0,
child: new Text(item)
);
}).toList()
)
)
}).toList(),
),
),
);
await tester.fling(find.text('A'), const Offset(0.0, 300.0), -1000.0);
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('X'), 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 - 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() {
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
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'];
await tester.pumpWidget(buildLeftRightApp(tabs: tabs, value: 'LEFT'));
......@@ -321,11 +364,20 @@ void main() {
TabController controller = DefaultTabController.of(tester.element(find.text('LEFT')));
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
// a change to the selected tab, everything should just settle back to
// to where it started.
Point flingStart = tester.getCenter(find.text('LEFT CHILD'));
await tester.flingFrom(flingStart, const Offset(-200.0, 0.0), -10000.0);
for (int index = 0; index > 50; index += 1) {
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(const Duration(seconds: 1)); // finish the scroll animation
expect(controller.index, 0);
......
......@@ -313,9 +313,9 @@ class WidgetController {
/// 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.
Future<Null> flingFrom(Point startLocation, Offset offset, double velocity, { int pointer: 1, Duration frameInterval: const Duration(milliseconds: 16) }) {
assert(offset.distance > 0.0);
assert(velocity > 0.0); // velocity is pixels/second
return TestAsyncUtils.guard(() async {
assert(offset.distance > 0.0);
assert(velocity != 0.0); // velocity is pixels/second
final TestPointer p = new TestPointer(pointer);
final HitTestResult result = hitTestOnBinding(startLocation);
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