| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| @TestOn('!chrome') |
| import 'dart:async'; |
| |
| import 'package:flutter/cupertino.dart'; |
| import 'package:flutter/services.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| void main() { |
| late FakeBuilder mockHelper; |
| |
| setUp(() { |
| mockHelper = FakeBuilder(); |
| }); |
| |
| int testListLength = 10; |
| SliverList buildAListOfStuff() { |
| return SliverList( |
| delegate: SliverChildBuilderDelegate( |
| (BuildContext context, int index) { |
| return SizedBox( |
| height: 200.0, |
| child: Center(child: Text(index.toString())), |
| ); |
| }, |
| childCount: testListLength, |
| ), |
| ); |
| } |
| |
| void uiTestGroup() { |
| testWidgets("doesn't invoke anything without user interaction", (WidgetTester tester) async { |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: CustomScrollView( |
| slivers: <Widget>[ |
| CupertinoSliverRefreshControl( |
| builder: mockHelper.builder, |
| ), |
| buildAListOfStuff(), |
| ], |
| ), |
| ), |
| ); |
| |
| expect(mockHelper.invocations, isEmpty); |
| |
| expect( |
| tester.getTopLeft(find.widgetWithText(SizedBox, '0')), |
| Offset.zero, |
| ); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); |
| |
| testWidgets('calls the indicator builder when starting to overscroll', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: CustomScrollView( |
| slivers: <Widget>[ |
| CupertinoSliverRefreshControl( |
| builder: mockHelper.builder, |
| ), |
| buildAListOfStuff(), |
| ], |
| ), |
| ), |
| ); |
| |
| // Drag down but not enough to trigger the refresh. |
| await tester.drag(find.text('0'), const Offset(0.0, 50.0), touchSlopY: 0); |
| await tester.pump(); |
| |
| // The function is referenced once while passing into CupertinoSliverRefreshControl |
| // and is called. |
| expect(mockHelper.invocations.first, matchesBuilder( |
| refreshState: RefreshIndicatorMode.drag, |
| pulledExtent: 50, |
| refreshTriggerPullDistance: 100, // default value. |
| refreshIndicatorExtent: 60, // default value. |
| )); |
| expect(mockHelper.invocations, hasLength(1)); |
| |
| expect( |
| tester.getTopLeft(find.widgetWithText(SizedBox, '0')), |
| const Offset(0.0, 50.0), |
| ); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); |
| |
| testWidgets( |
| "don't call the builder if overscroll doesn't move slivers like on Android", |
| (WidgetTester tester) async { |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: MediaQuery( |
| data: const MediaQueryData(), |
| child: CustomScrollView( |
| slivers: <Widget>[ |
| CupertinoSliverRefreshControl( |
| builder: mockHelper.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(); |
| |
| expect(mockHelper.invocations, isEmpty); |
| |
| expect( |
| tester.getTopLeft(find.widgetWithText(SizedBox, '0')), |
| Offset.zero, |
| ); |
| }, |
| variant: TargetPlatformVariant.only(TargetPlatform.android), |
| ); |
| |
| testWidgets('let the builder update as canceled drag scrolls away', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: CustomScrollView( |
| slivers: <Widget>[ |
| CupertinoSliverRefreshControl( |
| builder: mockHelper.builder, |
| ), |
| buildAListOfStuff(), |
| ], |
| ), |
| ), |
| ); |
| |
| // Drag down but not enough to trigger the refresh. |
| await tester.drag(find.text('0'), const Offset(0.0, 50.0), touchSlopY: 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)); |
| |
| expect(mockHelper.invocations, containsAllInOrder(<void>[ |
| matchesBuilder( |
| refreshState: RefreshIndicatorMode.drag, |
| pulledExtent: 50, |
| refreshTriggerPullDistance: 100, // default value. |
| refreshIndicatorExtent: 60, // default value. |
| ), |
| matchesBuilder( |
| refreshState: RefreshIndicatorMode.drag, |
| pulledExtent: moreOrLessEquals(48.36801747187993), |
| refreshTriggerPullDistance: 100, // default value. |
| refreshIndicatorExtent: 60, // default value. |
| ), |
| matchesBuilder( |
| refreshState: RefreshIndicatorMode.drag, |
| pulledExtent: moreOrLessEquals(44.63031931875867), |
| refreshTriggerPullDistance: 100, // default value. |
| refreshIndicatorExtent: 60, // default value. |
| ), |
| ])); |
| // The builder isn't called again when the sliver completely goes away. |
| expect(mockHelper.invocations, hasLength(3)); |
| |
| expect( |
| tester.getTopLeft(find.widgetWithText(SizedBox, '0')), |
| Offset.zero, |
| ); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); |
| |
| testWidgets('drag past threshold triggers refresh task', (WidgetTester tester) async { |
| final List<MethodCall> platformCallLog = <MethodCall>[]; |
| |
| tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async { |
| platformCallLog.add(methodCall); |
| }); |
| |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: CustomScrollView( |
| slivers: <Widget>[ |
| CupertinoSliverRefreshControl( |
| builder: mockHelper.builder, |
| onRefresh: mockHelper.refreshTask, |
| ), |
| buildAListOfStuff(), |
| ], |
| ), |
| ), |
| ); |
| |
| final TestGesture gesture = await tester.startGesture(Offset.zero); |
| 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(); |
| |
| expect(mockHelper.invocations, containsAllInOrder(<void>[ |
| matchesBuilder( |
| refreshState: RefreshIndicatorMode.drag, |
| pulledExtent: 99, |
| refreshTriggerPullDistance: 100, // default value. |
| refreshIndicatorExtent: 60, // default value. |
| ), |
| matchesBuilder( |
| refreshState: RefreshIndicatorMode.drag, |
| pulledExtent: moreOrLessEquals(86.78169), |
| refreshTriggerPullDistance: 100, // default value. |
| refreshIndicatorExtent: 60, // default value. |
| ), |
| matchesBuilder( |
| refreshState: RefreshIndicatorMode.armed, |
| pulledExtent: moreOrLessEquals(105.80452021305739), |
| refreshTriggerPullDistance: 100, // default value. |
| refreshIndicatorExtent: 60, // default value. |
| ), |
| ])); |
| // The refresh callback is triggered after the frame. |
| expect(mockHelper.invocations.last, const RefreshTaskInvocation()); |
| expect(mockHelper.invocations, hasLength(4)); |
| |
| expect( |
| platformCallLog.last, |
| isMethodCall('HapticFeedback.vibrate', arguments: 'HapticFeedbackType.mediumImpact'), |
| ); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); |
| |
| testWidgets( |
| 'refreshing task keeps the sliver expanded forever until done', |
| (WidgetTester tester) async { |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: CustomScrollView( |
| slivers: <Widget>[ |
| CupertinoSliverRefreshControl( |
| builder: mockHelper.builder, |
| onRefresh: mockHelper.refreshTask, |
| ), |
| buildAListOfStuff(), |
| ], |
| ), |
| ), |
| ); |
| |
| await tester.drag(find.text('0'), const Offset(0.0, 150.0), touchSlopY: 0); |
| await tester.pump(); |
| // Let it start snapping back. |
| await tester.pump(const Duration(milliseconds: 50)); |
| |
| expect(mockHelper.invocations, containsAllInOrder(<Matcher>[ |
| matchesBuilder( |
| refreshState: RefreshIndicatorMode.armed, |
| pulledExtent: 150, |
| refreshTriggerPullDistance: 100, // Default value. |
| refreshIndicatorExtent: 60, // Default value. |
| ), |
| equals(const RefreshTaskInvocation()), |
| matchesBuilder( |
| refreshState: RefreshIndicatorMode.armed, |
| pulledExtent: moreOrLessEquals(127.10396988577114), |
| refreshTriggerPullDistance: 100, // Default value. |
| refreshIndicatorExtent: 60, // Default value. |
| ), |
| ])); |
| |
| // Reaches refresh state and sliver's at 60.0 in height after a while. |
| await tester.pump(const Duration(seconds: 1)); |
| |
| expect(mockHelper.invocations, contains(matchesBuilder( |
| refreshState: RefreshIndicatorMode.refresh, |
| pulledExtent: 60, |
| refreshIndicatorExtent: 60, // Default value. |
| refreshTriggerPullDistance: 100, // Default value. |
| ))); |
| |
| // Stays in that state forever until future completes. |
| await tester.pump(const Duration(seconds: 1000)); |
| expect( |
| tester.getTopLeft(find.widgetWithText(SizedBox, '0')), |
| const Offset(0.0, 60.0), |
| ); |
| |
| mockHelper.refreshCompleter.complete(null); |
| await tester.pump(); |
| |
| expect(mockHelper.invocations, contains(matchesBuilder( |
| refreshState: RefreshIndicatorMode.done, |
| pulledExtent: 60, |
| refreshIndicatorExtent: 60, // Default value. |
| refreshTriggerPullDistance: 100, // Default value. |
| ))); |
| expect(mockHelper.invocations, hasLength(5)); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), |
| ); |
| |
| testWidgets( |
| 'refreshing task keeps the sliver expanded forever until completes with error', |
| (WidgetTester tester) async { |
| final FlutterError error = FlutterError('Oops'); |
| double errorCount = 0; |
| |
| runZonedGuarded( |
| () async { |
| mockHelper.refreshCompleter = Completer<void>.sync(); |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: CustomScrollView( |
| slivers: <Widget>[ |
| CupertinoSliverRefreshControl( |
| builder: mockHelper.builder, |
| onRefresh: mockHelper.refreshTask, |
| ), |
| buildAListOfStuff(), |
| ], |
| ), |
| ), |
| ); |
| |
| await tester.drag(find.text('0'), const Offset(0.0, 150.0), touchSlopY: 0); |
| await tester.pump(); |
| // Let it start snapping back. |
| await tester.pump(const Duration(milliseconds: 50)); |
| |
| expect(mockHelper.invocations, containsAllInOrder(<Matcher>[ |
| matchesBuilder( |
| refreshState: RefreshIndicatorMode.armed, |
| pulledExtent: 150, |
| refreshIndicatorExtent: 60, // Default value. |
| refreshTriggerPullDistance: 100, // Default value. |
| ), |
| equals(const RefreshTaskInvocation()), |
| matchesBuilder( |
| refreshState: RefreshIndicatorMode.armed, |
| pulledExtent: moreOrLessEquals(127.10396988577114), |
| refreshIndicatorExtent: 60, // Default value. |
| refreshTriggerPullDistance: 100, // Default value. |
| ), |
| ])); |
| |
| // Reaches refresh state and sliver's at 60.0 in height after a while. |
| await tester.pump(const Duration(seconds: 1)); |
| expect(mockHelper.invocations, contains(matchesBuilder( |
| refreshState: RefreshIndicatorMode.refresh, |
| pulledExtent: 60, |
| refreshIndicatorExtent: 60, // Default value. |
| refreshTriggerPullDistance: 100, // Default value. |
| ))); |
| |
| // Stays in that state forever until future completes. |
| await tester.pump(const Duration(seconds: 1000)); |
| expect( |
| tester.getTopLeft(find.widgetWithText(SizedBox, '0')), |
| const Offset(0.0, 60.0), |
| ); |
| |
| mockHelper.refreshCompleter.completeError(error); |
| await tester.pump(); |
| |
| expect(mockHelper.invocations, contains(matchesBuilder( |
| refreshState: RefreshIndicatorMode.done, |
| pulledExtent: 60, |
| refreshIndicatorExtent: 60, // Default value. |
| refreshTriggerPullDistance: 100, // Default value. |
| ))); |
| expect(mockHelper.invocations, hasLength(5)); |
| }, |
| (Object e, StackTrace stack) { |
| expect(e, error); |
| expect(errorCount, 0); |
| errorCount++; |
| }, |
| ); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), |
| ); |
| |
| testWidgets('expanded refreshing sliver scrolls normally', (WidgetTester tester) async { |
| mockHelper.refreshIndicator = const Center(child: Text('-1')); |
| |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: CustomScrollView( |
| slivers: <Widget>[ |
| CupertinoSliverRefreshControl( |
| builder: mockHelper.builder, |
| onRefresh: mockHelper.refreshTask, |
| ), |
| buildAListOfStuff(), |
| ], |
| ), |
| ), |
| ); |
| |
| await tester.drag(find.text('0'), const Offset(0.0, 150.0), touchSlopY: 0); |
| await tester.pump(); |
| |
| expect(mockHelper.invocations, contains(matchesBuilder( |
| refreshState: RefreshIndicatorMode.armed, |
| pulledExtent: 150, |
| refreshIndicatorExtent: 60, // Default value. |
| refreshTriggerPullDistance: 100, // Default value. |
| ))); |
| |
| // Given a box constraint of 150, the Center will occupy all that height. |
| expect( |
| tester.getRect(find.widgetWithText(Center, '-1')), |
| const Rect.fromLTRB(0.0, 0.0, 800.0, 150.0), |
| ); |
| |
| await tester.drag(find.text('0'), const Offset(0.0, -300.0), touchSlopY: 0, warnIfMissed: false); // hits the list |
| await tester.pump(); |
| |
| // Refresh indicator still being told to layout the same way. |
| expect(mockHelper.invocations, contains(matchesBuilder( |
| refreshState: RefreshIndicatorMode.refresh, |
| pulledExtent: 60, |
| refreshIndicatorExtent: 60, // Default value. |
| refreshTriggerPullDistance: 100, // 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), warnIfMissed: false); // hits the list |
| await tester.pump(); |
| await tester.pump(const Duration(seconds: 2)); |
| expect( |
| tester.getRect(find.widgetWithText(Center, '-1')), |
| const Rect.fromLTRB(0.0, 0.0, 800.0, 60.0), |
| ); |
| expect( |
| tester.getRect(find.widgetWithText(Center, '0')), |
| const Rect.fromLTRB(0.0, 60.0, 800.0, 260.0), |
| ); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); |
| |
| testWidgets('expanded refreshing sliver goes away when done', (WidgetTester tester) async { |
| mockHelper.refreshIndicator = const Center(child: Text('-1')); |
| |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: CustomScrollView( |
| slivers: <Widget>[ |
| CupertinoSliverRefreshControl( |
| builder: mockHelper.builder, |
| onRefresh: mockHelper.refreshTask, |
| ), |
| buildAListOfStuff(), |
| ], |
| ), |
| ), |
| ); |
| |
| await tester.drag(find.text('0'), const Offset(0.0, 150.0), touchSlopY: 0); |
| await tester.pump(); |
| expect(mockHelper.invocations, contains(matchesBuilder( |
| refreshState: RefreshIndicatorMode.armed, |
| pulledExtent: 150, |
| refreshIndicatorExtent: 60, // Default value. |
| refreshTriggerPullDistance: 100, // Default value. |
| ))); |
| expect( |
| tester.getRect(find.widgetWithText(Center, '-1')), |
| const Rect.fromLTRB(0.0, 0.0, 800.0, 150.0), |
| ); |
| expect(mockHelper.invocations, contains(const RefreshTaskInvocation())); |
| |
| // 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)); |
| |
| expect(mockHelper.invocations, contains(matchesBuilder( |
| refreshState: RefreshIndicatorMode.refresh, |
| pulledExtent: 60, |
| refreshIndicatorExtent: 60, // Default value. |
| refreshTriggerPullDistance: 100, // Default value. |
| ))); |
| expect( |
| tester.getRect(find.widgetWithText(Center, '-1')), |
| const Rect.fromLTRB(0.0, 0.0, 800.0, 60.0), |
| ); |
| expect( |
| tester.getRect(find.widgetWithText(Center, '0')), |
| const Rect.fromLTRB(0.0, 60.0, 800.0, 260.0), |
| ); |
| |
| mockHelper.refreshCompleter.complete(null); |
| await tester.pump(); |
| expect(mockHelper.invocations, contains(matchesBuilder( |
| refreshState: RefreshIndicatorMode.done, |
| pulledExtent: 60, |
| refreshIndicatorExtent: 60, // Default value. |
| refreshTriggerPullDistance: 100, // Default value. |
| ))); |
| |
| await tester.pump(const Duration(seconds: 5)); |
| expect(find.text('-1'), findsNothing); |
| expect( |
| tester.getRect(find.widgetWithText(Center, '0')), |
| const Rect.fromLTRB(0.0, 0.0, 800.0, 200.0), |
| ); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); |
| |
| testWidgets('builder still called when sliver snapped back more than 90%', (WidgetTester tester) async { |
| mockHelper.refreshIndicator = const Center(child: Text('-1')); |
| |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: CustomScrollView( |
| slivers: <Widget>[ |
| CupertinoSliverRefreshControl( |
| builder: mockHelper.builder, |
| onRefresh: mockHelper.refreshTask, |
| ), |
| buildAListOfStuff(), |
| ], |
| ), |
| ), |
| ); |
| |
| await tester.drag(find.text('0'), const Offset(0.0, 150.0), touchSlopY: 0); |
| await tester.pump(); |
| expect(mockHelper.invocations, contains(matchesBuilder( |
| refreshState: RefreshIndicatorMode.armed, |
| pulledExtent: 150, |
| refreshTriggerPullDistance: 100, // default value. |
| refreshIndicatorExtent: 60, // default value. |
| ))); |
| expect( |
| tester.getRect(find.widgetWithText(Center, '-1')), |
| const Rect.fromLTRB(0.0, 0.0, 800.0, 150.0), |
| ); |
| expect(mockHelper.invocations, contains(const RefreshTaskInvocation())); |
| |
| // 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)); |
| expect(mockHelper.invocations, contains(matchesBuilder( |
| refreshState: RefreshIndicatorMode.refresh, |
| pulledExtent: 60, |
| refreshTriggerPullDistance: 100, // default value. |
| refreshIndicatorExtent: 60, // default value. |
| ))); |
| expect( |
| tester.getRect(find.widgetWithText(Center, '-1')), |
| const Rect.fromLTRB(0.0, 0.0, 800.0, 60.0), |
| ); |
| expect( |
| tester.getRect(find.widgetWithText(Center, '0')), |
| const Rect.fromLTRB(0.0, 60.0, 800.0, 260.0), |
| ); |
| |
| mockHelper.refreshCompleter.complete(null); |
| await tester.pump(); |
| |
| expect(mockHelper.invocations, contains(matchesBuilder( |
| refreshState: RefreshIndicatorMode.done, |
| pulledExtent: 60, |
| refreshTriggerPullDistance: 100, // default value. |
| refreshIndicatorExtent: 60, // default value. |
| ))); |
| |
| // Waiting for refresh control to reach approximately 5% of height |
| await tester.pump(const Duration(milliseconds: 400)); |
| |
| expect( |
| tester.getRect(find.widgetWithText(Center, '0')).top, |
| moreOrLessEquals(3.0, epsilon: 4e-1), |
| ); |
| expect( |
| tester.getRect(find.widgetWithText(Center, '-1')).height, |
| moreOrLessEquals(3.0, epsilon: 4e-1), |
| ); |
| expect(mockHelper.invocations, contains(matchesBuilder( |
| refreshState: RefreshIndicatorMode.inactive, |
| pulledExtent: 2.6980688300546443, // ~5% of 60.0 |
| refreshTriggerPullDistance: 100, // default value. |
| refreshIndicatorExtent: 60, // default value. |
| ))); |
| expect(find.text('-1'), findsOneWidget); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); |
| |
| testWidgets( |
| 'retracting sliver during done cannot be pulled to refresh again until fully retracted', |
| (WidgetTester tester) async { |
| mockHelper.refreshIndicator = const Center(child: Text('-1')); |
| |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: CustomScrollView( |
| slivers: <Widget>[ |
| CupertinoSliverRefreshControl( |
| builder: mockHelper.builder, |
| onRefresh: mockHelper.refreshTask, |
| ), |
| buildAListOfStuff(), |
| ], |
| ), |
| ), |
| ); |
| |
| await tester.drag(find.text('0'), const Offset(0.0, 150.0), touchSlopY: 0.0); |
| await tester.pump(); |
| expect(mockHelper.invocations, contains(const RefreshTaskInvocation())); |
| |
| mockHelper.refreshCompleter.complete(null); |
| await tester.pump(); |
| expect(mockHelper.invocations, contains(matchesBuilder( |
| refreshState: RefreshIndicatorMode.done, |
| pulledExtent: 150.0, // Still overscrolled here. |
| refreshTriggerPullDistance: 100, // default value. |
| refreshIndicatorExtent: 60, // default value. |
| ))); |
| |
| // Let it start going away but not fully. |
| await tester.pump(const Duration(milliseconds: 100)); |
| // The refresh indicator is still building. |
| expect(mockHelper.invocations, contains(matchesBuilder( |
| refreshState: RefreshIndicatorMode.done, |
| pulledExtent: 91.31180913199277, |
| refreshTriggerPullDistance: 100, // default value. |
| refreshIndicatorExtent: 60, // 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), touchSlopY: 0.0, warnIfMissed: false); |
| await tester.pump(); |
| |
| // Instead, it's still in the done state because the sliver never |
| // fully retracted. |
| expect(mockHelper.invocations, contains(matchesBuilder( |
| refreshState: RefreshIndicatorMode.done, |
| pulledExtent: 147.3772721631821, |
| refreshTriggerPullDistance: 100, // default value. |
| refreshIndicatorExtent: 60, // 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')), |
| const 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), touchSlopY: 0.0); |
| await tester.pump(); |
| expect(mockHelper.invocations, contains(matchesBuilder( |
| refreshState: RefreshIndicatorMode.drag, |
| pulledExtent: 40, |
| refreshTriggerPullDistance: 100, // default value. |
| refreshIndicatorExtent: 60, // default value. |
| ))); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), |
| ); |
| |
| testWidgets( |
| 'sliver held in overscroll when task finishes completes normally', |
| (WidgetTester tester) async { |
| mockHelper.refreshIndicator = const Center(child: Text('-1')); |
| |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: CustomScrollView( |
| slivers: <Widget>[ |
| CupertinoSliverRefreshControl( |
| builder: mockHelper.builder, |
| onRefresh: mockHelper.refreshTask, |
| ), |
| buildAListOfStuff(), |
| ], |
| ), |
| ), |
| ); |
| |
| final TestGesture gesture = await tester.startGesture(Offset.zero); |
| // Start a refresh. |
| await gesture.moveBy(const Offset(0.0, 150.0)); |
| await tester.pump(); |
| expect(mockHelper.invocations, contains(const RefreshTaskInvocation())); |
| |
| // Complete the task while held down. |
| mockHelper.refreshCompleter.complete(null); |
| await tester.pump(); |
| |
| expect(mockHelper.invocations, contains(matchesBuilder( |
| refreshState: RefreshIndicatorMode.done, |
| pulledExtent: 150.0, // Still overscrolled here. |
| refreshTriggerPullDistance: 100, // default value. |
| refreshIndicatorExtent: 60, // default value. |
| ))); |
| expect( |
| tester.getRect(find.widgetWithText(Center, '0')), |
| const 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')), |
| const Rect.fromLTRB(0.0, 0.0, 800.0, 200.0), |
| ); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), |
| ); |
| |
| testWidgets( |
| 'sliver scrolled away when task completes properly removes itself', |
| (WidgetTester tester) async { |
| if (testListLength < 4) { |
| // This test only makes sense when the list is long enough that |
| // the indicator can be scrolled away while refreshing. |
| return; |
| } |
| mockHelper.refreshIndicator = const Center(child: Text('-1')); |
| |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: CustomScrollView( |
| slivers: <Widget>[ |
| CupertinoSliverRefreshControl( |
| builder: mockHelper.builder, |
| onRefresh: mockHelper.refreshTask, |
| ), |
| buildAListOfStuff(), |
| ], |
| ), |
| ), |
| ); |
| |
| // Start a refresh. |
| await tester.drag(find.text('0'), const Offset(0.0, 150.0)); |
| await tester.pump(); |
| expect(mockHelper.invocations, contains(const RefreshTaskInvocation())); |
| |
| 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. |
| expect(mockHelper.invocations, contains(matchesBuilder( |
| refreshState: RefreshIndicatorMode.done, |
| pulledExtent: 60, |
| refreshTriggerPullDistance: 100, // default value. |
| refreshIndicatorExtent: 60, // 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. |
| mockHelper.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(); |
| |
| expect(mockHelper.invocations, contains(matchesBuilder( |
| refreshState: RefreshIndicatorMode.done, |
| pulledExtent: 4.615384615384642, |
| refreshTriggerPullDistance: 100, // default value. |
| refreshIndicatorExtent: 60, // 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')), |
| const Rect.fromLTRB(0.0, 0.0, 800.0, 200.0), |
| ); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), |
| ); |
| |
| testWidgets( |
| "don't do anything unless it can be overscrolled at the start of the list", |
| (WidgetTester tester) async { |
| mockHelper.refreshIndicator = const Center(child: Text('-1')); |
| |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: CustomScrollView( |
| slivers: <Widget>[ |
| buildAListOfStuff(), |
| CupertinoSliverRefreshControl( // it's in the middle now. |
| builder: mockHelper.builder, |
| onRefresh: mockHelper.refreshTask, |
| ), |
| buildAListOfStuff(), |
| ], |
| ), |
| ), |
| ); |
| |
| await tester.fling(find.byType(SizedBox).first, const Offset(0.0, 200.0), 2000.0); |
| await tester.fling(find.byType(SizedBox).first, const Offset(0.0, -200.0), 3000.0, warnIfMissed: false); // IgnorePointer is enabled while scroll is ballistic. |
| |
| expect(mockHelper.invocations, isEmpty); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), |
| ); |
| |
| testWidgets( |
| 'without an onRefresh, builder is called with arm for one frame then sliver goes away', |
| (WidgetTester tester) async { |
| mockHelper.refreshIndicator = const Center(child: Text('-1')); |
| |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: CustomScrollView( |
| slivers: <Widget>[ |
| CupertinoSliverRefreshControl( |
| builder: mockHelper.builder, |
| ), |
| buildAListOfStuff(), |
| ], |
| ), |
| ), |
| ); |
| |
| await tester.drag(find.text('0'), const Offset(0.0, 150.0), touchSlopY: 0.0); |
| await tester.pump(); |
| |
| expect(mockHelper.invocations.first, matchesBuilder( |
| refreshState: RefreshIndicatorMode.armed, |
| pulledExtent: 150.0, |
| refreshTriggerPullDistance: 100.0, // Default value. |
| refreshIndicatorExtent: 60.0, // Default value. |
| )); |
| |
| await tester.pump(const Duration(milliseconds: 10)); |
| |
| expect(mockHelper.invocations.last, matchesBuilder( |
| refreshState: RefreshIndicatorMode.done, |
| pulledExtent: moreOrLessEquals(148.6463892921364), |
| refreshTriggerPullDistance: 100.0, // Default value. |
| refreshIndicatorExtent: 60.0, // Default value. |
| )); |
| |
| await tester.pump(const Duration(seconds: 5)); |
| expect(find.text('-1'), findsNothing); |
| expect( |
| tester.getRect(find.widgetWithText(Center, '0')), |
| const Rect.fromLTRB(0.0, 0.0, 800.0, 200.0), |
| ); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), |
| ); |
| |
| testWidgets('Should not crash when dragged', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: CustomScrollView( |
| physics: const BouncingScrollPhysics(), |
| slivers: <Widget>[ |
| CupertinoSliverRefreshControl( |
| onRefresh: () async => Future<void>.delayed(const Duration(days: 2000)), |
| ), |
| ], |
| ), |
| ), |
| ); |
| |
| await tester.dragFrom(const Offset(100, 10), const Offset(0.0, 50.0), touchSlopY: 0); |
| await tester.pump(); |
| |
| await tester.dragFrom(const Offset(100, 10), const Offset(0, 500), touchSlopY: 0); |
| await tester.pump(); |
| |
| expect(tester.takeException(), isNull); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); |
| |
| // Test to make sure the refresh sliver's overscroll isn't eaten by the |
| // nav bar sliver https://github.com/flutter/flutter/issues/74516. |
| testWidgets( |
| 'properly displays when the refresh sliver is behind the large title nav bar sliver', |
| (WidgetTester tester) async { |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: CustomScrollView( |
| slivers: <Widget>[ |
| const CupertinoSliverNavigationBar( |
| largeTitle: Text('Title'), |
| ), |
| CupertinoSliverRefreshControl( |
| builder: mockHelper.builder, |
| ), |
| buildAListOfStuff(), |
| ], |
| ), |
| ), |
| ); |
| |
| final double initialFirstCellY = tester.getTopLeft(find.widgetWithText(SizedBox, '0')).dy; |
| |
| // Drag down but not enough to trigger the refresh. |
| await tester.drag(find.text('0'), const Offset(0.0, 50.0), touchSlopY: 0); |
| await tester.pump(); |
| |
| expect(mockHelper.invocations.first, matchesBuilder( |
| refreshState: RefreshIndicatorMode.drag, |
| pulledExtent: 50, |
| refreshTriggerPullDistance: 100, // default value. |
| refreshIndicatorExtent: 60, // default value. |
| )); |
| expect(mockHelper.invocations, hasLength(1)); |
| |
| expect( |
| tester.getTopLeft(find.widgetWithText(SizedBox, '0')).dy, |
| initialFirstCellY + 50, |
| ); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), |
| ); |
| } |
| |
| void stateMachineTestGroup() { |
| testWidgets('starts in inactive state', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: CustomScrollView( |
| slivers: <Widget>[ |
| CupertinoSliverRefreshControl( |
| builder: mockHelper.builder, |
| ), |
| buildAListOfStuff(), |
| ], |
| ), |
| ), |
| ); |
| |
| expect( |
| CupertinoSliverRefreshControl.state(tester.element(find.byType(LayoutBuilder, skipOffstage: false))), |
| RefreshIndicatorMode.inactive, |
| ); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); |
| |
| testWidgets('goes to drag and returns to inactive in a small drag', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: CustomScrollView( |
| slivers: <Widget>[ |
| CupertinoSliverRefreshControl( |
| builder: mockHelper.builder, |
| ), |
| buildAListOfStuff(), |
| ], |
| ), |
| ), |
| ); |
| |
| await tester.drag(find.text('0'), const Offset(0.0, 20.0)); |
| await tester.pump(); |
| |
| expect( |
| CupertinoSliverRefreshControl.state(tester.element(find.byType(LayoutBuilder))), |
| RefreshIndicatorMode.drag, |
| ); |
| |
| await tester.pump(const Duration(seconds: 2)); |
| |
| expect( |
| CupertinoSliverRefreshControl.state(tester.element(find.byType(LayoutBuilder, skipOffstage: false))), |
| RefreshIndicatorMode.inactive, |
| ); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); |
| |
| testWidgets('goes to armed the frame it passes the threshold', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: CustomScrollView( |
| slivers: <Widget>[ |
| CupertinoSliverRefreshControl( |
| builder: mockHelper.builder, |
| refreshTriggerPullDistance: 80.0, |
| ), |
| buildAListOfStuff(), |
| ], |
| ), |
| ), |
| ); |
| |
| final TestGesture gesture = await tester.startGesture(Offset.zero); |
| await gesture.moveBy(const Offset(0.0, 79.0)); |
| await tester.pump(); |
| expect( |
| CupertinoSliverRefreshControl.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( |
| CupertinoSliverRefreshControl.state(tester.element(find.byType(LayoutBuilder))), |
| RefreshIndicatorMode.armed, |
| ); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); |
| |
| testWidgets( |
| 'goes to refresh the frame it crossed back the refresh threshold', |
| (WidgetTester tester) async { |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: CustomScrollView( |
| slivers: <Widget>[ |
| CupertinoSliverRefreshControl( |
| builder: mockHelper.builder, |
| onRefresh: mockHelper.refreshTask, |
| refreshTriggerPullDistance: 90.0, |
| refreshIndicatorExtent: 50.0, |
| ), |
| buildAListOfStuff(), |
| ], |
| ), |
| ), |
| ); |
| |
| final TestGesture gesture = await tester.startGesture(Offset.zero); |
| await gesture.moveBy(const Offset(0.0, 90.0)); // Arm it. |
| await tester.pump(); |
| expect( |
| CupertinoSliverRefreshControl.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(SizedBox, '0')).dy, |
| moreOrLessEquals(49.775111111111116), // Below 50 now. |
| ); |
| expect( |
| CupertinoSliverRefreshControl.state(tester.element(find.byType(LayoutBuilder))), |
| RefreshIndicatorMode.refresh, |
| ); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), |
| ); |
| |
| testWidgets( |
| 'goes to done internally as soon as the task finishes', |
| (WidgetTester tester) async { |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: CustomScrollView( |
| slivers: <Widget>[ |
| CupertinoSliverRefreshControl( |
| builder: mockHelper.builder, |
| onRefresh: mockHelper.refreshTask, |
| ), |
| buildAListOfStuff(), |
| ], |
| ), |
| ), |
| ); |
| |
| await tester.drag(find.text('0'), const Offset(0.0, 100.0), touchSlopY: 0.0); |
| await tester.pump(); |
| expect( |
| CupertinoSliverRefreshControl.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( |
| CupertinoSliverRefreshControl.state(tester.element(find.byType(LayoutBuilder))), |
| RefreshIndicatorMode.refresh, |
| ); |
| expect( |
| tester.getRect(find.widgetWithText(SizedBox, '0')), |
| const Rect.fromLTRB(0.0, 60.0, 800.0, 260.0), |
| ); |
| |
| mockHelper.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( |
| CupertinoSliverRefreshControl.state(tester.element(find.byType(LayoutBuilder))), |
| RefreshIndicatorMode.done, |
| ); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), |
| ); |
| |
| testWidgets( |
| 'goes back to inactive when retracting back past 10% of arming distance', |
| (WidgetTester tester) async { |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: CustomScrollView( |
| slivers: <Widget>[ |
| CupertinoSliverRefreshControl( |
| builder: mockHelper.builder, |
| onRefresh: mockHelper.refreshTask, |
| ), |
| buildAListOfStuff(), |
| ], |
| ), |
| ), |
| ); |
| |
| final TestGesture gesture = await tester.startGesture(Offset.zero); |
| await gesture.moveBy(const Offset(0.0, 150.0)); |
| await tester.pump(); |
| expect( |
| CupertinoSliverRefreshControl.state(tester.element(find.byType(LayoutBuilder))), |
| RefreshIndicatorMode.armed, |
| ); |
| |
| mockHelper.refreshCompleter.complete(null); |
| expect( |
| CupertinoSliverRefreshControl.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(SizedBox, '0')).dy, |
| moreOrLessEquals(27.944444444444457), |
| ); |
| // Need to bring it to 100 * 0.1 to reset to inactive. |
| expect( |
| CupertinoSliverRefreshControl.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(SizedBox, '0')).dy, |
| moreOrLessEquals(9.313890708161875), |
| ); |
| expect( |
| CupertinoSliverRefreshControl.state(tester.element(find.byType(LayoutBuilder))), |
| RefreshIndicatorMode.inactive, |
| ); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), |
| ); |
| |
| testWidgets( |
| 'goes back to inactive if already scrolled away when task completes', |
| (WidgetTester tester) async { |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: CustomScrollView( |
| slivers: <Widget>[ |
| CupertinoSliverRefreshControl( |
| builder: mockHelper.builder, |
| onRefresh: mockHelper.refreshTask, |
| ), |
| buildAListOfStuff(), |
| ], |
| ), |
| ), |
| ); |
| |
| final TestGesture gesture = await tester.startGesture(Offset.zero); |
| await gesture.moveBy(const Offset(0.0, 150.0)); |
| await tester.pump(); |
| expect( |
| CupertinoSliverRefreshControl.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(SizedBox, '0')).dy, |
| moreOrLessEquals(-145.0332383665717), |
| ); |
| expect( |
| CupertinoSliverRefreshControl.state(tester.element(find.byType(LayoutBuilder, skipOffstage: false))), |
| RefreshIndicatorMode.refresh, |
| ); |
| |
| mockHelper.refreshCompleter.complete(null); |
| // The sliver layout extent is removed on next frame. |
| await tester.pump(); |
| expect( |
| CupertinoSliverRefreshControl.state(tester.element(find.byType(LayoutBuilder, skipOffstage: false))), |
| RefreshIndicatorMode.inactive, |
| ); |
| // Nothing moved. |
| expect( |
| tester.getTopLeft(find.widgetWithText(SizedBox, '0')).dy, |
| moreOrLessEquals(-145.0332383665717), |
| ); |
| await tester.pump(const Duration(seconds: 2)); |
| // Everything stayed as is. |
| expect( |
| tester.getTopLeft(find.widgetWithText(SizedBox, '0')).dy, |
| moreOrLessEquals(-145.0332383665717), |
| ); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), |
| ); |
| |
| testWidgets( |
| "don't have to build any indicators or occupy space during refresh", |
| (WidgetTester tester) async { |
| mockHelper.refreshIndicator = const Center(child: Text('-1')); |
| |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: CustomScrollView( |
| slivers: <Widget>[ |
| CupertinoSliverRefreshControl( |
| builder: null, |
| onRefresh: mockHelper.refreshTask, |
| refreshIndicatorExtent: 0.0, |
| ), |
| buildAListOfStuff(), |
| ], |
| ), |
| ), |
| ); |
| |
| await tester.drag(find.text('0'), const Offset(0.0, 150.0)); |
| await tester.pump(); |
| expect( |
| CupertinoSliverRefreshControl.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( |
| CupertinoSliverRefreshControl.state(tester.element(find.byType(LayoutBuilder, skipOffstage: false))), |
| RefreshIndicatorMode.refresh, |
| ); |
| expect( |
| tester.getRect(find.widgetWithText(Center, '0')), |
| const Rect.fromLTRB(0.0, 0.0, 800.0, 200.0), |
| ); |
| |
| mockHelper.refreshCompleter.complete(null); |
| await tester.pump(); |
| // Goes to inactive right away since the sliver is already collapsed. |
| expect( |
| CupertinoSliverRefreshControl.state(tester.element(find.byType(LayoutBuilder, skipOffstage: false))), |
| RefreshIndicatorMode.inactive, |
| ); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), |
| ); |
| |
| testWidgets('buildRefreshIndicator progress', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Builder( |
| builder: (BuildContext context) { |
| return CupertinoSliverRefreshControl.buildRefreshIndicator( |
| context, |
| RefreshIndicatorMode.drag, |
| 10, 100, 10, |
| ); |
| }, |
| ), |
| ), |
| ); |
| expect(tester.widget<CupertinoActivityIndicator>(find.byType(CupertinoActivityIndicator)).progress, 10.0 / 100.0); |
| |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Builder( |
| builder: (BuildContext context) { |
| return CupertinoSliverRefreshControl.buildRefreshIndicator( |
| context, |
| RefreshIndicatorMode.drag, |
| 26, 100, 10, |
| ); |
| }, |
| ), |
| ), |
| ); |
| expect(tester.widget<CupertinoActivityIndicator>(find.byType(CupertinoActivityIndicator)).progress, 26.0 / 100.0); |
| |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Builder( |
| builder: (BuildContext context) { |
| return CupertinoSliverRefreshControl.buildRefreshIndicator( |
| context, |
| RefreshIndicatorMode.drag, |
| 100, 100, 10, |
| ); |
| }, |
| ), |
| ), |
| ); |
| expect(tester.widget<CupertinoActivityIndicator>(find.byType(CupertinoActivityIndicator)).progress, 100.0 / 100.0); |
| }); |
| |
| testWidgets('indicator should not become larger when overscrolled', (WidgetTester tester) async { |
| // test for https://github.com/flutter/flutter/issues/79841 |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: Builder( |
| builder: (BuildContext context) { |
| return CupertinoSliverRefreshControl.buildRefreshIndicator( |
| context, |
| RefreshIndicatorMode.done, |
| 120, 100, 10, |
| ); |
| }, |
| ), |
| ), |
| ); |
| |
| expect(tester.widget<CupertinoActivityIndicator>(find.byType(CupertinoActivityIndicator)).radius, 14.0); |
| }); |
| } |
| |
| group('UI tests long list', uiTestGroup); |
| |
| // Test the internal state machine directly to make sure the UI aren't just |
| // correct by coincidence. |
| group('state machine test long list', stateMachineTestGroup); |
| |
| // Retest everything and make sure that it still works when the whole list |
| // is smaller than the viewport size. |
| testListLength = 2; |
| group('UI tests short list', uiTestGroup); |
| |
| // Test the internal state machine directly to make sure the UI aren't just |
| // correct by coincidence. |
| group('state machine test short list', stateMachineTestGroup); |
| |
| testWidgets( |
| 'Does not crash when paintExtent > remainingPaintExtent', |
| (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/46871. |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: CustomScrollView( |
| physics: const BouncingScrollPhysics(), |
| slivers: <Widget>[ |
| const CupertinoSliverRefreshControl(), |
| SliverList( |
| delegate: SliverChildBuilderDelegate( |
| (BuildContext context, int index) => const SizedBox(height: 100), |
| childCount: 20, |
| ), |
| ), |
| ], |
| ), |
| ), |
| ); |
| |
| // Drag the content down far enough so that |
| // geometry.paintExtent > constraints.maxPaintExtent |
| await tester.dragFrom(const Offset(10, 10), const Offset(0, 500)); |
| await tester.pump(); |
| |
| expect(tester.takeException(), isNull); |
| }, |
| ); |
| } |
| |
| class FakeBuilder { |
| Completer<void> refreshCompleter = Completer<void>.sync(); |
| final List<MockHelperInvocation> invocations = <MockHelperInvocation>[]; |
| |
| Widget refreshIndicator = Container(); |
| |
| Widget builder( |
| BuildContext context, |
| RefreshIndicatorMode refreshState, |
| double pulledExtent, |
| double refreshTriggerPullDistance, |
| double refreshIndicatorExtent, |
| ) { |
| if (pulledExtent < 0.0) { |
| throw TestFailure('The pulledExtent should never be less than 0.0'); |
| } |
| if (refreshTriggerPullDistance < 0.0) { |
| throw TestFailure('The refreshTriggerPullDistance should never be less than 0.0'); |
| } |
| if (refreshIndicatorExtent < 0.0) { |
| throw TestFailure('The refreshIndicatorExtent should never be less than 0.0'); |
| } |
| invocations.add(BuilderInvocation( |
| refreshState: refreshState, |
| pulledExtent: pulledExtent, |
| refreshTriggerPullDistance: refreshTriggerPullDistance, |
| refreshIndicatorExtent: refreshIndicatorExtent, |
| )); |
| return refreshIndicator; |
| } |
| |
| Future<void> refreshTask() { |
| invocations.add(const RefreshTaskInvocation()); |
| return refreshCompleter.future; |
| } |
| } |
| |
| abstract class MockHelperInvocation { |
| const MockHelperInvocation(); |
| } |
| |
| @immutable |
| class RefreshTaskInvocation extends MockHelperInvocation { |
| const RefreshTaskInvocation(); |
| } |
| |
| @immutable |
| class BuilderInvocation extends MockHelperInvocation { |
| const BuilderInvocation({ |
| required this.refreshState, |
| required this.pulledExtent, |
| required this.refreshIndicatorExtent, |
| required this.refreshTriggerPullDistance, |
| }); |
| |
| final RefreshIndicatorMode refreshState; |
| final double pulledExtent; |
| final double refreshTriggerPullDistance; |
| final double refreshIndicatorExtent; |
| |
| @override |
| String toString() => '{refreshState: $refreshState, pulledExtent: $pulledExtent, refreshTriggerPullDistance: $refreshTriggerPullDistance, refreshIndicatorExtent: $refreshIndicatorExtent}'; |
| } |
| |
| Matcher matchesBuilder({ |
| required RefreshIndicatorMode refreshState, |
| required dynamic pulledExtent, |
| required dynamic refreshTriggerPullDistance, |
| required dynamic refreshIndicatorExtent, |
| }) { |
| return isA<BuilderInvocation>() |
| .having((BuilderInvocation invocation) => invocation.refreshState, 'refreshState', refreshState) |
| .having((BuilderInvocation invocation) => invocation.pulledExtent, 'pulledExtent', pulledExtent) |
| .having((BuilderInvocation invocation) => invocation.refreshTriggerPullDistance, 'refreshTriggerPullDistance', refreshTriggerPullDistance) |
| .having((BuilderInvocation invocation) => invocation.refreshIndicatorExtent, 'refreshIndicatorExtent', refreshIndicatorExtent); |
| } |