| // Copyright 2018 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| import 'dart:async'; |
| |
| import 'package:flutter/cupertino.dart'; |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/services.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| import 'package:mockito/mockito.dart'; |
| |
| void main() { |
| MockHelper mockHelper; |
| |
| /// Completer that holds the future given to the CupertinoRefreshControl. |
| Completer<void> refreshCompleter; |
| |
| /// The widget that the indicator builder given to the CupertinoRefreshControl |
| /// returns. |
| Widget refreshIndicator; |
| |
| /// These two Functions are required to avoid tearing off of the MockHelper object, |
| /// which is not supported when using Dart 2 runtime semantics. |
| final Function builder = (BuildContext context, RefreshIndicatorMode refreshState, |
| double pulledExtent, |
| double refreshTriggerPullDistance, |
| double refreshIndicatorExtent) => |
| mockHelper.builder(context, refreshState, pulledExtent, refreshTriggerPullDistance, |
| refreshIndicatorExtent); |
| |
| final Function onRefresh = () => mockHelper.refreshTask(); |
| |
| setUp(() { |
| mockHelper = new MockHelper(); |
| refreshCompleter = new Completer<void>.sync(); |
| refreshIndicator = new Container(); |
| |
| when(mockHelper.builder( |
| typed(any), typed(any), typed(any), typed(any), typed(any))) |
| .thenAnswer((Invocation i) { |
| final RefreshIndicatorMode refreshState = i.positionalArguments[1]; |
| final double pulledExtent = i.positionalArguments[2]; |
| final double refreshTriggerPullDistance = i.positionalArguments[3]; |
| final double refreshIndicatorExtent = i.positionalArguments[4]; |
| if (refreshState == RefreshIndicatorMode.inactive) { |
| throw new TestFailure( |
| 'RefreshControlIndicatorBuilder should never be called with the ' |
| "inactive state because there's nothing to build in that case" |
| ); |
| } |
| if (pulledExtent < 0.0) { |
| throw new TestFailure('The pulledExtent should never be less than 0.0'); |
| } |
| if (refreshTriggerPullDistance < 0.0) { |
| throw new TestFailure('The refreshTriggerPullDistance should never be less than 0.0'); |
| } |
| if (refreshIndicatorExtent < 0.0) { |
| throw new TestFailure('The refreshIndicatorExtent should never be less than 0.0'); |
| } |
| return refreshIndicator; |
| }); |
| when(mockHelper.refreshTask()).thenAnswer((_) => refreshCompleter.future); |
| }); |
| |
| SliverList buildAListOfStuff() { |
| return new SliverList( |
| delegate: new SliverChildBuilderDelegate( |
| (BuildContext context, int index) { |
| return new Container( |
| height: 200.0, |
| child: new Center(child: new Text(index.toString())), |
| ); |
| }, |
| childCount: 10, |
| ), |
| ); |
| } |
| |
| group('UI tests', () { |
| testWidgets("doesn't invoke anything without user interaction", (WidgetTester tester) async { |
| debugDefaultTargetPlatformOverride = TargetPlatform.iOS; |
| |
| await tester.pumpWidget( |
| new Directionality( |
| textDirection: TextDirection.ltr, |
| child: new CustomScrollView( |
| slivers: <Widget>[ |
| new CupertinoRefreshControl( |
| builder: builder, |
| ), |
| buildAListOfStuff(), |
| ], |
| ), |
| ), |
| ); |
| |
| verifyNoMoreInteractions(mockHelper); |
| |
| expect( |
| tester.getTopLeft(find.widgetWithText(Container, '0')), |
| const Offset(0.0, 0.0), |
| ); |
| |
| debugDefaultTargetPlatformOverride = null; |
| }); |
| |
| testWidgets('calls the indicator builder when starting to overscroll', (WidgetTester tester) async { |
| debugDefaultTargetPlatformOverride = TargetPlatform.iOS; |
| |
| await tester.pumpWidget( |
| new Directionality( |
| textDirection: TextDirection.ltr, |
| child: new CustomScrollView( |
| slivers: <Widget>[ |
| new CupertinoRefreshControl( |
| builder: builder, |
| ), |
| buildAListOfStuff(), |
| ], |
| ), |
| ), |
| ); |
| |
| // Drag down but not enough to trigger the refresh. |
| await tester.drag(find.text('0'), const Offset(0.0, 50.0)); |
| await tester.pump(); |
| |
| // The function is referenced once while passing into CupertinoRefreshControl |
| // and is called. |
| verify(mockHelper.builder( |
| typed(any), |
| RefreshIndicatorMode.drag, |
| 50.0, |
| 100.0, // Default value. |
| 60.0, // Default value. |
| )); |
| verifyNoMoreInteractions(mockHelper); |
| |
| expect( |
| tester.getTopLeft(find.widgetWithText(Container, '0')), |
| const Offset(0.0, 50.0), |
| ); |
| |
| debugDefaultTargetPlatformOverride = null; |
| }); |
| |
| testWidgets( |
| "don't call the builder if overscroll doesn't move slivers like on Android", |
| (WidgetTester tester) async { |
| debugDefaultTargetPlatformOverride = TargetPlatform.android; |
| |
| await tester.pumpWidget( |
| new Directionality( |
| textDirection: TextDirection.ltr, |
| child: new CustomScrollView( |
| slivers: <Widget>[ |
| new CupertinoRefreshControl( |
| builder: builder, |
| ), |
| buildAListOfStuff(), |
| ], |
| ), |
| ), |
| ); |
| |
| // Drag down but not enough to trigger the refresh. |
| await tester.drag(find.text('0'), const Offset(0.0, 50.0)); |
| await tester.pump(); |
| |
| verifyNoMoreInteractions(mockHelper); |
| |
| expect( |
| tester.getTopLeft(find.widgetWithText(Container, '0')), |
| const Offset(0.0, 0.0), |
| ); |
| |
| debugDefaultTargetPlatformOverride = null; |
| }, |
| ); |
| |
| testWidgets('let the builder update as cancelled drag scrolls away', (WidgetTester tester) async { |
| debugDefaultTargetPlatformOverride = TargetPlatform.iOS; |
| |
| await tester.pumpWidget( |
| new Directionality( |
| textDirection: TextDirection.ltr, |
| child: new CustomScrollView( |
| slivers: <Widget>[ |
| new CupertinoRefreshControl( |
| builder: builder, |
| ), |
| buildAListOfStuff(), |
| ], |
| ), |
| ), |
| ); |
| |
| // Drag down but not enough to trigger the refresh. |
| await tester.drag(find.text('0'), const Offset(0.0, 50.0)); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 20)); |
| await tester.pump(const Duration(milliseconds: 20)); |
| await tester.pump(const Duration(seconds: 3)); |
| |
| verifyInOrder(<void>[ |
| mockHelper.builder( |
| typed(any), |
| RefreshIndicatorMode.drag, |
| 50.0, |
| 100.0, // Default value. |
| 60.0, // Default value. |
| ), |
| mockHelper.builder( |
| typed(any), |
| RefreshIndicatorMode.drag, |
| typed(argThat(moreOrLessEquals(48.36801747187993))), |
| 100.0, // Default value. |
| 60.0, // Default value. |
| ), |
| mockHelper.builder( |
| typed(any), |
| RefreshIndicatorMode.drag, |
| typed(argThat(moreOrLessEquals(44.63031931875867))), |
| 100.0, // Default value. |
| 60.0, // Default value. |
| ), |
| // The builder isn't called again when the sliver completely goes away. |
| ]); |
| verifyNoMoreInteractions(mockHelper); |
| |
| expect( |
| tester.getTopLeft(find.widgetWithText(Container, '0')), |
| const Offset(0.0, 0.0), |
| ); |
| |
| debugDefaultTargetPlatformOverride = null; |
| }); |
| |
| testWidgets('drag past threshold triggers refresh task', (WidgetTester tester) async { |
| debugDefaultTargetPlatformOverride = TargetPlatform.iOS; |
| |
| final List<MethodCall> platformCallLog = <MethodCall>[]; |
| |
| SystemChannels.platform.setMockMethodCallHandler((MethodCall methodCall) async { |
| platformCallLog.add(methodCall); |
| }); |
| |
| await tester.pumpWidget( |
| new Directionality( |
| textDirection: TextDirection.ltr, |
| child: new CustomScrollView( |
| slivers: <Widget>[ |
| new CupertinoRefreshControl( |
| builder: builder, |
| onRefresh: onRefresh, |
| ), |
| buildAListOfStuff(), |
| ], |
| ), |
| ), |
| ); |
| |
| final TestGesture gesture = await tester.startGesture(const Offset(0.0, 0.0)); |
| await gesture.moveBy(const Offset(0.0, 99.0)); |
| await tester.pump(); |
| await gesture.moveBy(const Offset(0.0, -30.0)); |
| await tester.pump(); |
| await gesture.moveBy(const Offset(0.0, 50.0)); |
| await tester.pump(); |
| |
| verifyInOrder(<void>[ |
| mockHelper.builder( |
| typed(any), |
| RefreshIndicatorMode.drag, |
| 99.0, |
| 100.0, // Default value. |
| 60.0, // Default value. |
| ), |
| mockHelper.builder( |
| typed(any), |
| RefreshIndicatorMode.drag, |
| typed(argThat(moreOrLessEquals(86.78169))), |
| 100.0, // Default value. |
| 60.0, // Default value. |
| ), |
| mockHelper.builder( |
| typed(any), |
| RefreshIndicatorMode.armed, |
| typed(argThat(moreOrLessEquals(105.80452021305739))), |
| 100.0, // Default value. |
| 60.0, // Default value. |
| ), |
| // The refresh callback is triggered after the frame. |
| mockHelper.refreshTask(), |
| ]); |
| verifyNoMoreInteractions(mockHelper); |
| |
| expect( |
| platformCallLog.last, |
| isMethodCall('HapticFeedback.vibrate', arguments: 'HapticFeedbackType.mediumImpact'), |
| ); |
| debugDefaultTargetPlatformOverride = null; |
| }); |
| |
| testWidgets( |
| 'refreshing task keeps the sliver expanded forever until done', |
| (WidgetTester tester) async { |
| debugDefaultTargetPlatformOverride = TargetPlatform.iOS; |
| |
| await tester.pumpWidget( |
| new Directionality( |
| textDirection: TextDirection.ltr, |
| child: new CustomScrollView( |
| slivers: <Widget>[ |
| new CupertinoRefreshControl( |
| builder: builder, |
| onRefresh: onRefresh, |
| ), |
| buildAListOfStuff(), |
| ], |
| ), |
| ), |
| ); |
| |
| await tester.drag(find.text('0'), const Offset(0.0, 150.0)); |
| await tester.pump(); |
| // Let it start snapping back. |
| await tester.pump(const Duration(milliseconds: 50)); |
| |
| verifyInOrder(<void>[ |
| mockHelper.builder( |
| typed(any), |
| RefreshIndicatorMode.armed, |
| 150.0, |
| 100.0, // Default value. |
| 60.0, // Default value. |
| ), |
| mockHelper.refreshTask(), |
| mockHelper.builder( |
| typed(any), |
| RefreshIndicatorMode.armed, |
| typed(argThat(moreOrLessEquals(127.10396988577114))), |
| 100.0, // Default value. |
| 60.0, // Default value. |
| ), |
| ]); |
| |
| // Reaches refresh state and sliver's at 60.0 in height after a while. |
| await tester.pump(const Duration(seconds: 1)); |
| verify(mockHelper.builder( |
| typed(any), |
| RefreshIndicatorMode.refresh, |
| 60.0, |
| 100.0, // Default value. |
| 60.0, // Default value. |
| )); |
| |
| // Stays in that state forever until future completes. |
| await tester.pump(const Duration(seconds: 1000)); |
| verifyNoMoreInteractions(mockHelper); |
| expect( |
| tester.getTopLeft(find.widgetWithText(Container, '0')), |
| const Offset(0.0, 60.0), |
| ); |
| |
| refreshCompleter.complete(null); |
| await tester.pump(); |
| |
| verify(mockHelper.builder( |
| typed(any), |
| RefreshIndicatorMode.done, |
| 60.0, |
| 100.0, // Default value. |
| 60.0, // Default value. |
| )); |
| verifyNoMoreInteractions(mockHelper); |
| |
| debugDefaultTargetPlatformOverride = null; |
| }, |
| ); |
| |
| testWidgets('expanded refreshing sliver scrolls normally', (WidgetTester tester) async { |
| debugDefaultTargetPlatformOverride = TargetPlatform.iOS; |
| |
| refreshIndicator = const Center(child: const Text('-1')); |
| |
| await tester.pumpWidget( |
| new Directionality( |
| textDirection: TextDirection.ltr, |
| child: new CustomScrollView( |
| slivers: <Widget>[ |
| new CupertinoRefreshControl( |
| builder: builder, |
| onRefresh: onRefresh, |
| ), |
| buildAListOfStuff(), |
| ], |
| ), |
| ), |
| ); |
| |
| await tester.drag(find.text('0'), const Offset(0.0, 150.0)); |
| await tester.pump(); |
| |
| verify(mockHelper.builder( |
| typed(any), |
| RefreshIndicatorMode.armed, |
| 150.0, |
| 100.0, // Default value. |
| 60.0, // Default value. |
| )); |
| |
| // Given a box constraint of 150, the Center will occupy all that height. |
| expect( |
| tester.getRect(find.widgetWithText(Center, '-1')), |
| new Rect.fromLTRB(0.0, 0.0, 800.0, 150.0), |
| ); |
| |
| await tester.drag(find.text('0'), const Offset(0.0, -300.0)); |
| await tester.pump(); |
| |
| // Refresh indicator still being told to layout the same way. |
| verify(mockHelper.builder( |
| typed(any), |
| RefreshIndicatorMode.refresh, |
| 60.0, |
| 100.0, // Default value. |
| 60.0, // Default value. |
| )); |
| |
| // Now the sliver is scrolled off screen. |
| expect( |
| tester.getTopLeft(find.widgetWithText(Center, '-1', skipOffstage: false)).dy, |
| moreOrLessEquals(-175.38461538461536), |
| ); |
| expect( |
| tester.getBottomLeft(find.widgetWithText(Center, '-1', skipOffstage: false)).dy, |
| moreOrLessEquals(-115.38461538461536), |
| ); |
| expect( |
| tester.getTopLeft(find.widgetWithText(Center, '0')).dy, |
| moreOrLessEquals(-115.38461538461536), |
| ); |
| |
| // Scroll the top of the refresh indicator back to overscroll, it will |
| // snap to the size of the refresh indicator and stay there. |
| await tester.drag(find.text('1'), const Offset(0.0, 200.0)); |
| await tester.pump(); |
| await tester.pump(const Duration(seconds: 2)); |
| expect( |
| tester.getRect(find.widgetWithText(Center, '-1')), |
| new Rect.fromLTRB(0.0, 0.0, 800.0, 60.0), |
| ); |
| expect( |
| tester.getRect(find.widgetWithText(Center, '0')), |
| new Rect.fromLTRB(0.0, 60.0, 800.0, 260.0), |
| ); |
| |
| debugDefaultTargetPlatformOverride = null; |
| }); |
| |
| testWidgets('expanded refreshing sliver goes away when done', (WidgetTester tester) async { |
| debugDefaultTargetPlatformOverride = TargetPlatform.iOS; |
| |
| refreshIndicator = const Center(child: const Text('-1')); |
| |
| await tester.pumpWidget( |
| new Directionality( |
| textDirection: TextDirection.ltr, |
| child: new CustomScrollView( |
| slivers: <Widget>[ |
| new CupertinoRefreshControl( |
| builder: builder, |
| onRefresh: onRefresh, |
| ), |
| buildAListOfStuff(), |
| ], |
| ), |
| ), |
| ); |
| |
| await tester.drag(find.text('0'), const Offset(0.0, 150.0)); |
| await tester.pump(); |
| verify(mockHelper.builder( |
| typed(any), |
| RefreshIndicatorMode.armed, |
| 150.0, |
| 100.0, // Default value. |
| 60.0, // Default value. |
| )); |
| expect( |
| tester.getRect(find.widgetWithText(Center, '-1')), |
| new Rect.fromLTRB(0.0, 0.0, 800.0, 150.0), |
| ); |
| verify(mockHelper.refreshTask()); |
| |
| // Rebuilds the sliver with a layout extent now. |
| await tester.pump(); |
| // Let it snap back to occupy the indicator's final sliver space only. |
| await tester.pump(const Duration(seconds: 2)); |
| verify(mockHelper.builder( |
| typed(any), |
| RefreshIndicatorMode.refresh, |
| 60.0, |
| 100.0, // Default value. |
| 60.0, // Default value. |
| )); |
| expect( |
| tester.getRect(find.widgetWithText(Center, '-1')), |
| new Rect.fromLTRB(0.0, 0.0, 800.0, 60.0), |
| ); |
| expect( |
| tester.getRect(find.widgetWithText(Center, '0')), |
| new Rect.fromLTRB(0.0, 60.0, 800.0, 260.0), |
| ); |
| |
| refreshCompleter.complete(null); |
| await tester.pump(); |
| verify(mockHelper.builder( |
| typed(any), |
| RefreshIndicatorMode.done, |
| 60.0, |
| 100.0, // Default value. |
| 60.0, // Default value. |
| )); |
| |
| await tester.pump(const Duration(seconds: 5)); |
| expect(find.text('-1'), findsNothing); |
| expect( |
| tester.getRect(find.widgetWithText(Center, '0')), |
| new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0), |
| ); |
| |
| debugDefaultTargetPlatformOverride = null; |
| }); |
| |
| testWidgets( |
| 'retracting sliver during done cannot be pulled to refresh again until fully retracted', |
| (WidgetTester tester) async { |
| debugDefaultTargetPlatformOverride = TargetPlatform.iOS; |
| |
| refreshIndicator = const Center(child: const Text('-1')); |
| |
| await tester.pumpWidget( |
| new Directionality( |
| textDirection: TextDirection.ltr, |
| child: new CustomScrollView( |
| slivers: <Widget>[ |
| new CupertinoRefreshControl( |
| builder: builder, |
| onRefresh: onRefresh, |
| ), |
| buildAListOfStuff(), |
| ], |
| ), |
| ), |
| ); |
| |
| await tester.drag(find.text('0'), const Offset(0.0, 150.0)); |
| await tester.pump(); |
| verify(mockHelper.refreshTask()); |
| |
| refreshCompleter.complete(null); |
| await tester.pump(); |
| verify(mockHelper.builder( |
| typed(any), |
| RefreshIndicatorMode.done, |
| 150.0, // Still overscrolled here. |
| 100.0, // Default value. |
| 60.0, // Default value. |
| )); |
| |
| // Let it start going away but not fully. |
| await tester.pump(const Duration(milliseconds: 100)); |
| // The refresh indicator is still building. |
| verify(mockHelper.builder( |
| typed(any), |
| RefreshIndicatorMode.done, |
| 91.31180913199277, |
| 100.0, // Default value. |
| 60.0, // Default value. |
| )); |
| expect( |
| tester.getBottomLeft(find.widgetWithText(Center, '-1')).dy, |
| moreOrLessEquals(91.311809131992776), |
| ); |
| |
| // Start another drag by an amount that would have been enough to |
| // trigger another refresh if it were in the right state. |
| await tester.drag(find.text('0'), const Offset(0.0, 150.0)); |
| await tester.pump(); |
| |
| // Instead, it's still in the done state because the sliver never |
| // fully retracted. |
| verify(mockHelper.builder( |
| typed(any), |
| RefreshIndicatorMode.done, |
| 147.3772721631821, |
| 100.0, // Default value. |
| 60.0, // Default value. |
| )); |
| |
| // Now let it fully go away. |
| await tester.pump(const Duration(seconds: 5)); |
| expect(find.text('-1'), findsNothing); |
| expect( |
| tester.getRect(find.widgetWithText(Center, '0')), |
| new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0), |
| ); |
| |
| // Start another drag. It's now in drag mode. |
| await tester.drag(find.text('0'), const Offset(0.0, 40.0)); |
| await tester.pump(); |
| verify(mockHelper.builder( |
| typed(any), |
| RefreshIndicatorMode.drag, |
| 40.0, |
| 100.0, // Default value. |
| 60.0, // Default value. |
| )); |
| |
| debugDefaultTargetPlatformOverride = null; |
| }, |
| ); |
| |
| testWidgets( |
| 'sliver held in overscroll when task finishes completes normally', |
| (WidgetTester tester) async { |
| debugDefaultTargetPlatformOverride = TargetPlatform.iOS; |
| |
| refreshIndicator = const Center(child: const Text('-1')); |
| |
| await tester.pumpWidget( |
| new Directionality( |
| textDirection: TextDirection.ltr, |
| child: new CustomScrollView( |
| slivers: <Widget>[ |
| new CupertinoRefreshControl( |
| builder: builder, |
| onRefresh: onRefresh, |
| ), |
| buildAListOfStuff(), |
| ], |
| ), |
| ), |
| ); |
| |
| final TestGesture gesture = await tester.startGesture(const Offset(0.0, 0.0)); |
| // Start a refresh. |
| await gesture.moveBy(const Offset(0.0, 150.0)); |
| await tester.pump(); |
| verify(mockHelper.refreshTask()); |
| |
| // Complete the task while held down. |
| refreshCompleter.complete(null); |
| await tester.pump(); |
| verify(mockHelper.builder( |
| typed(any), |
| RefreshIndicatorMode.done, |
| 150.0, // Still overscrolled here. |
| 100.0, // Default value. |
| 60.0, // Default value. |
| )); |
| expect( |
| tester.getRect(find.widgetWithText(Center, '0')), |
| new Rect.fromLTRB(0.0, 150.0, 800.0, 350.0), |
| ); |
| |
| await gesture.up(); |
| await tester.pump(); |
| await tester.pump(const Duration(seconds: 5)); |
| expect(find.text('-1'), findsNothing); |
| expect( |
| tester.getRect(find.widgetWithText(Center, '0')), |
| new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0), |
| ); |
| |
| debugDefaultTargetPlatformOverride = null; |
| }, |
| ); |
| |
| testWidgets( |
| 'sliver scrolled away when task completes properly removes itself', |
| (WidgetTester tester) async { |
| debugDefaultTargetPlatformOverride = TargetPlatform.iOS; |
| |
| refreshIndicator = const Center(child: const Text('-1')); |
| |
| await tester.pumpWidget( |
| new Directionality( |
| textDirection: TextDirection.ltr, |
| child: new CustomScrollView( |
| slivers: <Widget>[ |
| new CupertinoRefreshControl( |
| builder: builder, |
| onRefresh: onRefresh, |
| ), |
| buildAListOfStuff(), |
| ], |
| ), |
| ), |
| ); |
| |
| // Start a refresh. |
| await tester.drag(find.text('0'), const Offset(0.0, 150.0)); |
| await tester.pump(); |
| verify(mockHelper.refreshTask()); |
| |
| await tester.drag(find.text('0'), const Offset(0.0, -300.0)); |
| await tester.pump(); |
| |
| // Refresh indicator still being told to layout the same way. |
| verify(mockHelper.builder( |
| typed(any), |
| RefreshIndicatorMode.refresh, |
| 60.0, |
| 100.0, // Default value. |
| 60.0, // Default value. |
| )); |
| |
| // Now the sliver is scrolled off screen. |
| expect( |
| tester.getTopLeft(find.widgetWithText(Center, '-1', skipOffstage: false)).dy, |
| moreOrLessEquals(-175.38461538461536), |
| ); |
| expect( |
| tester.getBottomLeft(find.widgetWithText(Center, '-1', skipOffstage: false)).dy, |
| moreOrLessEquals(-115.38461538461536), |
| ); |
| |
| // Complete the task while scrolled away. |
| refreshCompleter.complete(null); |
| // The sliver is instantly gone since there is no overscroll physics |
| // simulation. |
| await tester.pump(); |
| |
| // The next item's position is not disturbed. |
| expect( |
| tester.getTopLeft(find.widgetWithText(Center, '0')).dy, |
| moreOrLessEquals(-115.38461538461536), |
| ); |
| |
| // Scrolling past the first item still results in a new overscroll. |
| // The layout extent is gone. |
| await tester.drag(find.text('1'), const Offset(0.0, 120.0)); |
| await tester.pump(); |
| |
| verify(mockHelper.builder( |
| typed(any), |
| RefreshIndicatorMode.drag, |
| 4.615384615384642, |
| 100.0, // Default value. |
| 60.0, // Default value. |
| )); |
| |
| // Snaps away normally. |
| await tester.pump(); |
| await tester.pump(const Duration(seconds: 2)); |
| expect(find.text('-1'), findsNothing); |
| expect( |
| tester.getRect(find.widgetWithText(Center, '0')), |
| new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0), |
| ); |
| |
| debugDefaultTargetPlatformOverride = null; |
| }, |
| ); |
| |
| testWidgets( |
| "don't do anything unless it can be overscrolled at the start of the list", |
| (WidgetTester tester) async { |
| debugDefaultTargetPlatformOverride = TargetPlatform.iOS; |
| |
| refreshIndicator = const Center(child: const Text('-1')); |
| |
| await tester.pumpWidget( |
| new Directionality( |
| textDirection: TextDirection.ltr, |
| child: new CustomScrollView( |
| slivers: <Widget>[ |
| buildAListOfStuff(), |
| new CupertinoRefreshControl( // it's in the middle now. |
| builder: builder, |
| onRefresh: onRefresh, |
| ), |
| buildAListOfStuff(), |
| ], |
| ), |
| ), |
| ); |
| |
| await tester.fling(find.byType(Container).first, const Offset(0.0, 200.0), 2000.0); |
| |
| await tester.fling(find.byType(Container).first, const Offset(0.0, -200.0), 3000.0); |
| |
| verifyNoMoreInteractions(mockHelper); |
| |
| debugDefaultTargetPlatformOverride = null; |
| }, |
| ); |
| |
| testWidgets( |
| 'without an onRefresh, builder is called with arm for one frame then sliver goes away', |
| (WidgetTester tester) async { |
| debugDefaultTargetPlatformOverride = TargetPlatform.iOS; |
| |
| refreshIndicator = const Center(child: const Text('-1')); |
| |
| await tester.pumpWidget( |
| new Directionality( |
| textDirection: TextDirection.ltr, |
| child: new CustomScrollView( |
| slivers: <Widget>[ |
| new CupertinoRefreshControl( |
| builder: builder, |
| ), |
| buildAListOfStuff(), |
| ], |
| ), |
| ), |
| ); |
| |
| await tester.drag(find.text('0'), const Offset(0.0, 150.0)); |
| await tester.pump(); |
| verify(mockHelper.builder( |
| typed(any), |
| RefreshIndicatorMode.armed, |
| 150.0, |
| 100.0, // Default value. |
| 60.0, // Default value. |
| )); |
| |
| await tester.pump(const Duration(milliseconds: 10)); |
| verify(mockHelper.builder( |
| typed(any), |
| RefreshIndicatorMode.done, // Goes to done on the next frame. |
| 148.6463892921364, |
| 100.0, // Default value. |
| 60.0, // Default value. |
| )); |
| |
| await tester.pump(const Duration(seconds: 5)); |
| expect(find.text('-1'), findsNothing); |
| expect( |
| tester.getRect(find.widgetWithText(Center, '0')), |
| new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0), |
| ); |
| |
| debugDefaultTargetPlatformOverride = null; |
| } |
| ); |
| }); |
| |
| // Test the internal state machine directly to make sure the UI aren't just |
| // correct by coincidence. |
| group('state machine test', () { |
| testWidgets('starts in inactive state', (WidgetTester tester) async { |
| debugDefaultTargetPlatformOverride = TargetPlatform.iOS; |
| |
| await tester.pumpWidget( |
| new Directionality( |
| textDirection: TextDirection.ltr, |
| child: new CustomScrollView( |
| slivers: <Widget>[ |
| new CupertinoRefreshControl( |
| builder: builder, |
| ), |
| buildAListOfStuff(), |
| ], |
| ), |
| ), |
| ); |
| |
| expect( |
| CupertinoRefreshControl.state(tester.element(find.byType(LayoutBuilder, skipOffstage: false))), |
| RefreshIndicatorMode.inactive, |
| ); |
| |
| debugDefaultTargetPlatformOverride = null; |
| }); |
| |
| testWidgets('goes to drag and returns to inactive in a small drag', (WidgetTester tester) async { |
| debugDefaultTargetPlatformOverride = TargetPlatform.iOS; |
| |
| await tester.pumpWidget( |
| new Directionality( |
| textDirection: TextDirection.ltr, |
| child: new CustomScrollView( |
| slivers: <Widget>[ |
| new CupertinoRefreshControl( |
| builder: builder, |
| ), |
| buildAListOfStuff(), |
| ], |
| ), |
| ), |
| ); |
| |
| await tester.drag(find.text('0'), const Offset(0.0, 20.0)); |
| await tester.pump(); |
| |
| expect( |
| CupertinoRefreshControl.state(tester.element(find.byType(LayoutBuilder))), |
| RefreshIndicatorMode.drag, |
| ); |
| |
| await tester.pump(const Duration(seconds: 2)); |
| |
| expect( |
| CupertinoRefreshControl.state(tester.element(find.byType(LayoutBuilder, skipOffstage: false))), |
| RefreshIndicatorMode.inactive, |
| ); |
| |
| debugDefaultTargetPlatformOverride = null; |
| }); |
| |
| testWidgets('goes to armed the frame it passes the threshold', (WidgetTester tester) async { |
| debugDefaultTargetPlatformOverride = TargetPlatform.iOS; |
| |
| await tester.pumpWidget( |
| new Directionality( |
| textDirection: TextDirection.ltr, |
| child: new CustomScrollView( |
| slivers: <Widget>[ |
| new CupertinoRefreshControl( |
| builder: builder, |
| refreshTriggerPullDistance: 80.0, |
| ), |
| buildAListOfStuff(), |
| ], |
| ), |
| ), |
| ); |
| |
| final TestGesture gesture = await tester.startGesture(const Offset(0.0, 0.0)); |
| await gesture.moveBy(const Offset(0.0, 79.0)); |
| await tester.pump(); |
| expect( |
| CupertinoRefreshControl.state(tester.element(find.byType(LayoutBuilder))), |
| RefreshIndicatorMode.drag, |
| ); |
| |
| await gesture.moveBy(const Offset(0.0, 3.0)); // Overscrolling, need to move more than 1px. |
| await tester.pump(); |
| expect( |
| CupertinoRefreshControl.state(tester.element(find.byType(LayoutBuilder))), |
| RefreshIndicatorMode.armed, |
| ); |
| |
| debugDefaultTargetPlatformOverride = null; |
| }); |
| |
| testWidgets( |
| 'goes to refresh the frame it crossed back the refresh threshold', |
| (WidgetTester tester) async { |
| debugDefaultTargetPlatformOverride = TargetPlatform.iOS; |
| |
| await tester.pumpWidget( |
| new Directionality( |
| textDirection: TextDirection.ltr, |
| child: new CustomScrollView( |
| slivers: <Widget>[ |
| new CupertinoRefreshControl( |
| builder: builder, |
| onRefresh: onRefresh, |
| refreshTriggerPullDistance: 90.0, |
| refreshIndicatorExtent: 50.0, |
| ), |
| buildAListOfStuff(), |
| ], |
| ), |
| ), |
| ); |
| |
| final TestGesture gesture = await tester.startGesture(const Offset(0.0, 0.0)); |
| await gesture.moveBy(const Offset(0.0, 90.0)); // Arm it. |
| await tester.pump(); |
| expect( |
| CupertinoRefreshControl.state(tester.element(find.byType(LayoutBuilder))), |
| RefreshIndicatorMode.armed, |
| ); |
| |
| await gesture.moveBy(const Offset(0.0, -80.0)); // Overscrolling, need to move more than -40. |
| await tester.pump(); |
| expect( |
| tester.getTopLeft(find.widgetWithText(Container, '0')).dy, |
| moreOrLessEquals(49.775111111111116), // Below 50 now. |
| ); |
| expect( |
| CupertinoRefreshControl.state(tester.element(find.byType(LayoutBuilder))), |
| RefreshIndicatorMode.refresh, |
| ); |
| |
| debugDefaultTargetPlatformOverride = null; |
| }, |
| ); |
| |
| testWidgets( |
| 'goes to done internally as soon as the task finishes', |
| (WidgetTester tester) async { |
| debugDefaultTargetPlatformOverride = TargetPlatform.iOS; |
| |
| await tester.pumpWidget( |
| new Directionality( |
| textDirection: TextDirection.ltr, |
| child: new CustomScrollView( |
| slivers: <Widget>[ |
| new CupertinoRefreshControl( |
| builder: builder, |
| onRefresh: onRefresh, |
| ), |
| buildAListOfStuff(), |
| ], |
| ), |
| ), |
| ); |
| |
| await tester.drag(find.text('0'), const Offset(0.0, 100.0)); |
| await tester.pump(); |
| expect( |
| CupertinoRefreshControl.state(tester.element(find.byType(LayoutBuilder))), |
| RefreshIndicatorMode.armed, |
| ); |
| // The sliver scroll offset correction is applied on the next frame. |
| await tester.pump(); |
| |
| await tester.pump(const Duration(seconds: 2)); |
| expect( |
| CupertinoRefreshControl.state(tester.element(find.byType(LayoutBuilder))), |
| RefreshIndicatorMode.refresh, |
| ); |
| expect( |
| tester.getRect(find.widgetWithText(Container, '0')), |
| new Rect.fromLTRB(0.0, 60.0, 800.0, 260.0), |
| ); |
| |
| refreshCompleter.complete(null); |
| // The task completed between frames. The internal state goes to done |
| // right away even though the sliver gets a new offset correction the |
| // next frame. |
| expect( |
| CupertinoRefreshControl.state(tester.element(find.byType(LayoutBuilder))), |
| RefreshIndicatorMode.done, |
| ); |
| |
| debugDefaultTargetPlatformOverride = null; |
| }, |
| ); |
| |
| testWidgets( |
| 'goes back to inactive when retracting back past 10% of arming distance', |
| (WidgetTester tester) async { |
| debugDefaultTargetPlatformOverride = TargetPlatform.iOS; |
| |
| await tester.pumpWidget( |
| new Directionality( |
| textDirection: TextDirection.ltr, |
| child: new CustomScrollView( |
| slivers: <Widget>[ |
| new CupertinoRefreshControl( |
| builder: builder, |
| onRefresh: onRefresh, |
| ), |
| buildAListOfStuff(), |
| ], |
| ), |
| ), |
| ); |
| |
| final TestGesture gesture = await tester.startGesture(const Offset(0.0, 0.0)); |
| await gesture.moveBy(const Offset(0.0, 150.0)); |
| await tester.pump(); |
| expect( |
| CupertinoRefreshControl.state(tester.element(find.byType(LayoutBuilder))), |
| RefreshIndicatorMode.armed, |
| ); |
| |
| refreshCompleter.complete(null); |
| expect( |
| CupertinoRefreshControl.state(tester.element(find.byType(LayoutBuilder))), |
| RefreshIndicatorMode.done, |
| ); |
| await tester.pump(); |
| |
| // Now back in overscroll mode. |
| await gesture.moveBy(const Offset(0.0, -200.0)); |
| await tester.pump(); |
| expect( |
| tester.getTopLeft(find.widgetWithText(Container, '0')).dy, |
| moreOrLessEquals(27.944444444444457), |
| ); |
| // Need to bring it to 100 * 0.1 to reset to inactive. |
| expect( |
| CupertinoRefreshControl.state(tester.element(find.byType(LayoutBuilder))), |
| RefreshIndicatorMode.done, |
| ); |
| |
| await gesture.moveBy(const Offset(0.0, -35.0)); |
| await tester.pump(); |
| expect( |
| tester.getTopLeft(find.widgetWithText(Container, '0')).dy, |
| moreOrLessEquals(9.313890708161875), |
| ); |
| expect( |
| CupertinoRefreshControl.state(tester.element(find.byType(LayoutBuilder))), |
| RefreshIndicatorMode.inactive, |
| ); |
| |
| debugDefaultTargetPlatformOverride = null; |
| }, |
| ); |
| |
| testWidgets( |
| 'goes back to inactive if already scrolled away when task completes', |
| (WidgetTester tester) async { |
| debugDefaultTargetPlatformOverride = TargetPlatform.iOS; |
| |
| await tester.pumpWidget( |
| new Directionality( |
| textDirection: TextDirection.ltr, |
| child: new CustomScrollView( |
| slivers: <Widget>[ |
| new CupertinoRefreshControl( |
| builder: builder, |
| onRefresh: onRefresh, |
| ), |
| buildAListOfStuff(), |
| ], |
| ), |
| ), |
| ); |
| |
| final TestGesture gesture = await tester.startGesture(const Offset(0.0, 0.0)); |
| await gesture.moveBy(const Offset(0.0, 150.0)); |
| await tester.pump(); |
| expect( |
| CupertinoRefreshControl.state(tester.element(find.byType(LayoutBuilder))), |
| RefreshIndicatorMode.armed, |
| ); |
| await tester.pump(); // Sliver scroll offset correction is applied one frame later. |
| |
| await gesture.moveBy(const Offset(0.0, -300.0)); |
| await tester.pump(); |
| // The refresh indicator is offscreen now. |
| expect( |
| tester.getTopLeft(find.widgetWithText(Container, '0')).dy, |
| moreOrLessEquals(-145.0332383665717), |
| ); |
| expect( |
| CupertinoRefreshControl.state(tester.element(find.byType(LayoutBuilder, skipOffstage: false))), |
| RefreshIndicatorMode.refresh, |
| ); |
| |
| refreshCompleter.complete(null); |
| // The sliver layout extent is removed on next frame. |
| await tester.pump(); |
| expect( |
| CupertinoRefreshControl.state(tester.element(find.byType(LayoutBuilder, skipOffstage: false))), |
| RefreshIndicatorMode.inactive, |
| ); |
| // Nothing moved. |
| expect( |
| tester.getTopLeft(find.widgetWithText(Container, '0')).dy, |
| moreOrLessEquals(-145.0332383665717), |
| ); |
| await tester.pump(const Duration(seconds: 2)); |
| // Everything stayed as is. |
| expect( |
| tester.getTopLeft(find.widgetWithText(Container, '0')).dy, |
| moreOrLessEquals(-145.0332383665717), |
| ); |
| |
| debugDefaultTargetPlatformOverride = null; |
| }, |
| ); |
| |
| testWidgets( |
| "don't have to build any indicators or occupy space during refresh", |
| (WidgetTester tester) async { |
| debugDefaultTargetPlatformOverride = TargetPlatform.iOS; |
| |
| refreshIndicator = const Center(child: const Text('-1')); |
| |
| await tester.pumpWidget( |
| new Directionality( |
| textDirection: TextDirection.ltr, |
| child: new CustomScrollView( |
| slivers: <Widget>[ |
| new CupertinoRefreshControl( |
| builder: null, |
| onRefresh: onRefresh, |
| refreshIndicatorExtent: 0.0, |
| ), |
| buildAListOfStuff(), |
| ], |
| ), |
| ), |
| ); |
| |
| await tester.drag(find.text('0'), const Offset(0.0, 150.0)); |
| await tester.pump(); |
| expect( |
| CupertinoRefreshControl.state(tester.element(find.byType(LayoutBuilder))), |
| RefreshIndicatorMode.armed, |
| ); |
| |
| await tester.pump(); |
| await tester.pump(const Duration(seconds: 5)); |
| // In refresh mode but has no UI. |
| expect( |
| CupertinoRefreshControl.state(tester.element(find.byType(LayoutBuilder, skipOffstage: false))), |
| RefreshIndicatorMode.refresh, |
| ); |
| expect( |
| tester.getRect(find.widgetWithText(Center, '0')), |
| new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0), |
| ); |
| verify(mockHelper.refreshTask()); // The refresh function still called. |
| |
| refreshCompleter.complete(null); |
| await tester.pump(); |
| // Goes to inactive right away since the sliver is already collapsed. |
| expect( |
| CupertinoRefreshControl.state(tester.element(find.byType(LayoutBuilder, skipOffstage: false))), |
| RefreshIndicatorMode.inactive, |
| ); |
| |
| debugDefaultTargetPlatformOverride = null; |
| } |
| ); |
| }); |
| } |
| |
| class MockHelper extends Mock { |
| Widget builder( |
| BuildContext context, |
| RefreshIndicatorMode refreshState, |
| double pulledExtent, |
| double refreshTriggerPullDistance, |
| double refreshIndicatorExtent, |
| ); |
| |
| Future<void> refreshTask(); |
| } |