| // 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. |
| |
| import 'dart:async'; |
| |
| import 'package:flutter/gestures.dart' show DragStartBehavior; |
| import 'package:flutter/widgets.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| const DismissDirection defaultDismissDirection = DismissDirection.horizontal; |
| const double crossAxisEndOffset = 0.5; |
| bool reportedDismissUpdateReached = false; |
| bool reportedDismissUpdatePreviousReached = false; |
| late DismissDirection reportedDismissUpdateReachedDirection; |
| |
| DismissDirection reportedDismissDirection = DismissDirection.horizontal; |
| List<int> dismissedItems = <int>[]; |
| |
| Widget buildTest({ |
| Axis scrollDirection = Axis.vertical, |
| DismissDirection dismissDirection = defaultDismissDirection, |
| double? startToEndThreshold, |
| TextDirection textDirection = TextDirection.ltr, |
| Future<bool?> Function(BuildContext context, DismissDirection direction)? confirmDismiss, |
| ScrollController? controller, |
| ScrollPhysics? scrollPhysics, |
| Widget? background, |
| }) { |
| return Directionality( |
| textDirection: textDirection, |
| child: StatefulBuilder( |
| builder: (BuildContext context, StateSetter setState) { |
| Widget buildDismissibleItem(int item) { |
| return Dismissible( |
| dragStartBehavior: DragStartBehavior.down, |
| key: ValueKey<int>(item), |
| direction: dismissDirection, |
| confirmDismiss: confirmDismiss == null ? null : (DismissDirection direction) { |
| return confirmDismiss(context, direction); |
| }, |
| onDismissed: (DismissDirection direction) { |
| setState(() { |
| reportedDismissDirection = direction; |
| expect(dismissedItems.contains(item), isFalse); |
| dismissedItems.add(item); |
| }); |
| }, |
| onResize: () { |
| expect(dismissedItems.contains(item), isFalse); |
| }, |
| onUpdate: (DismissUpdateDetails details) { |
| reportedDismissUpdateReachedDirection = details.direction; |
| reportedDismissUpdateReached = details.reached; |
| reportedDismissUpdatePreviousReached = details.previousReached; |
| }, |
| background: background, |
| dismissThresholds: startToEndThreshold == null |
| ? <DismissDirection, double>{} |
| : <DismissDirection, double>{DismissDirection.startToEnd: startToEndThreshold}, |
| crossAxisEndOffset: crossAxisEndOffset, |
| child: SizedBox( |
| width: 100.0, |
| height: 100.0, |
| child: Text(item.toString()), |
| ), |
| ); |
| } |
| |
| return Container( |
| padding: const EdgeInsets.all(10.0), |
| child: ListView( |
| physics: scrollPhysics, |
| controller: controller, |
| dragStartBehavior: DragStartBehavior.down, |
| scrollDirection: scrollDirection, |
| itemExtent: 100.0, |
| children: <int>[0, 1, 2, 3, 4, 5, 6, 7, 8] |
| .where((int i) => !dismissedItems.contains(i)) |
| .map<Widget>(buildDismissibleItem).toList(), |
| ), |
| ); |
| }, |
| ), |
| ); |
| } |
| |
| typedef DismissMethod = Future<void> Function(WidgetTester tester, Finder finder, { required AxisDirection gestureDirection }); |
| |
| Future<void> dismissElement(WidgetTester tester, Finder finder, { required AxisDirection gestureDirection }) async { |
| Offset downLocation; |
| Offset upLocation; |
| switch (gestureDirection) { |
| case AxisDirection.left: |
| // getTopRight() returns a point that's just beyond itemWidget's right |
| // edge and outside the Dismissible event listener's bounds. |
| downLocation = tester.getTopRight(finder) + const Offset(-0.1, 0.0); |
| upLocation = tester.getTopLeft(finder) + const Offset(-0.1, 0.0); |
| break; |
| case AxisDirection.right: |
| // we do the same thing here to keep the test symmetric |
| downLocation = tester.getTopLeft(finder) + const Offset(0.1, 0.0); |
| upLocation = tester.getTopRight(finder) + const Offset(0.1, 0.0); |
| break; |
| case AxisDirection.up: |
| // getBottomLeft() returns a point that's just below itemWidget's bottom |
| // edge and outside the Dismissible event listener's bounds. |
| downLocation = tester.getBottomLeft(finder) + const Offset(0.0, -0.1); |
| upLocation = tester.getTopLeft(finder) + const Offset(0.0, -0.1); |
| break; |
| case AxisDirection.down: |
| // again with doing the same here for symmetry |
| downLocation = tester.getTopLeft(finder) + const Offset(0.1, 0.0); |
| upLocation = tester.getBottomLeft(finder) + const Offset(0.1, 0.0); |
| break; |
| } |
| |
| final TestGesture gesture = await tester.startGesture(downLocation); |
| await gesture.moveTo(upLocation); |
| await gesture.up(); |
| } |
| |
| Future<void> flingElement(WidgetTester tester, Finder finder, { required AxisDirection gestureDirection, double initialOffsetFactor = 0.0 }) async { |
| Offset delta; |
| switch (gestureDirection) { |
| case AxisDirection.left: |
| delta = const Offset(-300.0, 0.0); |
| break; |
| case AxisDirection.right: |
| delta = const Offset(300.0, 0.0); |
| break; |
| case AxisDirection.up: |
| delta = const Offset(0.0, -300.0); |
| break; |
| case AxisDirection.down: |
| delta = const Offset(0.0, 300.0); |
| break; |
| } |
| await tester.fling(finder, delta, 1000.0, initialOffset: delta * initialOffsetFactor); |
| } |
| |
| Future<void> flingElementFromZero(WidgetTester tester, Finder finder, { required AxisDirection gestureDirection }) async { |
| // This is a special case where we drag in one direction, then fling back so |
| // that at the point of release, we're at exactly the point at which we |
| // started, but with velocity. This is needed to check a boundary condition |
| // in the flinging behavior. |
| await flingElement(tester, finder, gestureDirection: gestureDirection, initialOffsetFactor: -1.0); |
| } |
| |
| Future<void> dismissItem( |
| WidgetTester tester, |
| int item, { |
| required AxisDirection gestureDirection, |
| DismissMethod mechanism = dismissElement, |
| }) async { |
| assert(gestureDirection != null); |
| final Finder itemFinder = find.text(item.toString()); |
| expect(itemFinder, findsOneWidget); |
| |
| await mechanism(tester, itemFinder, gestureDirection: gestureDirection); |
| await tester.pumpAndSettle(); |
| } |
| |
| Future<void> checkFlingItemBeforeMovementEnd( |
| WidgetTester tester, |
| int item, { |
| required AxisDirection gestureDirection, |
| DismissMethod mechanism = rollbackElement, |
| }) async { |
| assert(gestureDirection != null); |
| final Finder itemFinder = find.text(item.toString()); |
| expect(itemFinder, findsOneWidget); |
| |
| await mechanism(tester, itemFinder, gestureDirection: gestureDirection); |
| |
| await tester.pump(); // start the slide |
| await tester.pump(const Duration(milliseconds: 100)); |
| } |
| |
| Future<void> checkFlingItemAfterMovement( |
| WidgetTester tester, |
| int item, { |
| required AxisDirection gestureDirection, |
| DismissMethod mechanism = rollbackElement, |
| }) async { |
| assert(gestureDirection != null); |
| final Finder itemFinder = find.text(item.toString()); |
| expect(itemFinder, findsOneWidget); |
| |
| await mechanism(tester, itemFinder, gestureDirection: gestureDirection); |
| |
| await tester.pump(); // start the slide |
| await tester.pump(const Duration(milliseconds: 300)); |
| } |
| |
| Future<void> rollbackElement(WidgetTester tester, Finder finder, { required AxisDirection gestureDirection, double initialOffsetFactor = 0.0 }) async { |
| Offset delta; |
| switch (gestureDirection) { |
| case AxisDirection.left: |
| delta = const Offset(-30.0, 0.0); |
| break; |
| case AxisDirection.right: |
| delta = const Offset(30.0, 0.0); |
| break; |
| case AxisDirection.up: |
| delta = const Offset(0.0, -30.0); |
| break; |
| case AxisDirection.down: |
| delta = const Offset(0.0, 30.0); |
| break; |
| } |
| await tester.fling(finder, delta, 1000.0, initialOffset: delta * initialOffsetFactor); |
| } |
| |
| class Test1215DismissibleWidget extends StatelessWidget { |
| const Test1215DismissibleWidget(this.text, { Key? key }) : super(key: key); |
| |
| final String text; |
| |
| @override |
| Widget build(BuildContext context) { |
| return Dismissible( |
| dragStartBehavior: DragStartBehavior.down, |
| key: ObjectKey(text), |
| child: AspectRatio( |
| aspectRatio: 1.0, |
| child: Text(text), |
| ), |
| ); |
| } |
| } |
| |
| void main() { |
| setUp(() { |
| // Reset "results" variables. |
| reportedDismissDirection = defaultDismissDirection; |
| dismissedItems = <int>[]; |
| }); |
| |
| testWidgets('Horizontal drag triggers dismiss scrollDirection=vertical', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildTest(), |
| ); |
| expect(dismissedItems, isEmpty); |
| |
| await dismissItem(tester, 0, gestureDirection: AxisDirection.right); |
| expect(find.text('0'), findsNothing); |
| expect(dismissedItems, equals(<int>[0])); |
| expect(reportedDismissDirection, DismissDirection.startToEnd); |
| |
| await dismissItem(tester, 1, gestureDirection: AxisDirection.left); |
| expect(find.text('1'), findsNothing); |
| expect(dismissedItems, equals(<int>[0, 1])); |
| expect(reportedDismissDirection, DismissDirection.endToStart); |
| }); |
| |
| testWidgets('Horizontal fling triggers dismiss scrollDirection=vertical', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildTest(), |
| ); |
| expect(dismissedItems, isEmpty); |
| |
| await dismissItem(tester, 0, gestureDirection: AxisDirection.right, mechanism: flingElement); |
| expect(find.text('0'), findsNothing); |
| expect(dismissedItems, equals(<int>[0])); |
| expect(reportedDismissDirection, DismissDirection.startToEnd); |
| |
| await dismissItem(tester, 1, gestureDirection: AxisDirection.left, mechanism: flingElement); |
| expect(find.text('1'), findsNothing); |
| expect(dismissedItems, equals(<int>[0, 1])); |
| expect(reportedDismissDirection, DismissDirection.endToStart); |
| }); |
| |
| testWidgets('Horizontal fling does not trigger at zero offset, but does otherwise', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildTest( |
| startToEndThreshold: 0.95, |
| ), |
| ); |
| expect(dismissedItems, isEmpty); |
| |
| await dismissItem(tester, 0, gestureDirection: AxisDirection.right, mechanism: flingElementFromZero); |
| expect(find.text('0'), findsOneWidget); |
| expect(dismissedItems, equals(<int>[])); |
| |
| await dismissItem(tester, 0, gestureDirection: AxisDirection.left, mechanism: flingElementFromZero); |
| expect(find.text('0'), findsOneWidget); |
| expect(dismissedItems, equals(<int>[])); |
| |
| await dismissItem(tester, 0, gestureDirection: AxisDirection.right, mechanism: flingElement); |
| expect(find.text('0'), findsNothing); |
| expect(dismissedItems, equals(<int>[0])); |
| expect(reportedDismissDirection, DismissDirection.startToEnd); |
| |
| await dismissItem(tester, 1, gestureDirection: AxisDirection.left, mechanism: flingElement); |
| expect(find.text('1'), findsNothing); |
| expect(dismissedItems, equals(<int>[0, 1])); |
| expect(reportedDismissDirection, DismissDirection.endToStart); |
| }); |
| |
| testWidgets('Vertical drag triggers dismiss scrollDirection=horizontal', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildTest( |
| scrollDirection: Axis.horizontal, |
| dismissDirection: DismissDirection.vertical, |
| ), |
| ); |
| expect(dismissedItems, isEmpty); |
| |
| await dismissItem(tester, 0, gestureDirection: AxisDirection.up); |
| expect(find.text('0'), findsNothing); |
| expect(dismissedItems, equals(<int>[0])); |
| expect(reportedDismissDirection, DismissDirection.up); |
| |
| await dismissItem(tester, 1, gestureDirection: AxisDirection.down); |
| expect(find.text('1'), findsNothing); |
| expect(dismissedItems, equals(<int>[0, 1])); |
| expect(reportedDismissDirection, DismissDirection.down); |
| }); |
| |
| testWidgets('drag-left with DismissDirection.endToStart triggers dismiss (LTR)', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildTest( |
| dismissDirection: DismissDirection.endToStart, |
| ), |
| ); |
| expect(dismissedItems, isEmpty); |
| |
| await dismissItem(tester, 0, gestureDirection: AxisDirection.right); |
| expect(find.text('0'), findsOneWidget); |
| expect(dismissedItems, isEmpty); |
| await dismissItem(tester, 1, gestureDirection: AxisDirection.right); |
| |
| await dismissItem(tester, 0, gestureDirection: AxisDirection.left); |
| expect(find.text('0'), findsNothing); |
| expect(dismissedItems, equals(<int>[0])); |
| await dismissItem(tester, 1, gestureDirection: AxisDirection.left); |
| }); |
| |
| testWidgets('drag-right with DismissDirection.startToEnd triggers dismiss (LTR)', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildTest( |
| dismissDirection: DismissDirection.startToEnd, |
| ), |
| ); |
| expect(dismissedItems, isEmpty); |
| |
| await dismissItem(tester, 0, gestureDirection: AxisDirection.left); |
| expect(find.text('0'), findsOneWidget); |
| expect(dismissedItems, isEmpty); |
| |
| await dismissItem(tester, 0, gestureDirection: AxisDirection.right); |
| expect(find.text('0'), findsNothing); |
| expect(dismissedItems, equals(<int>[0])); |
| }); |
| |
| testWidgets('drag-right with DismissDirection.endToStart triggers dismiss (RTL)', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildTest( |
| textDirection: TextDirection.rtl, |
| dismissDirection: DismissDirection.endToStart, |
| ), |
| ); |
| |
| expect(dismissedItems, isEmpty); |
| |
| await dismissItem(tester, 0, gestureDirection: AxisDirection.left); |
| expect(find.text('0'), findsOneWidget); |
| expect(dismissedItems, isEmpty); |
| |
| await dismissItem(tester, 0, gestureDirection: AxisDirection.right); |
| expect(find.text('0'), findsNothing); |
| expect(dismissedItems, equals(<int>[0])); |
| }); |
| |
| testWidgets('drag-left with DismissDirection.startToEnd triggers dismiss (RTL)', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildTest( |
| textDirection: TextDirection.rtl, |
| dismissDirection: DismissDirection.startToEnd, |
| ), |
| ); |
| expect(dismissedItems, isEmpty); |
| |
| await dismissItem(tester, 0, gestureDirection: AxisDirection.right); |
| expect(find.text('0'), findsOneWidget); |
| expect(dismissedItems, isEmpty); |
| await dismissItem(tester, 1, gestureDirection: AxisDirection.right); |
| |
| await dismissItem(tester, 0, gestureDirection: AxisDirection.left); |
| expect(find.text('0'), findsNothing); |
| expect(dismissedItems, equals(<int>[0])); |
| await dismissItem(tester, 1, gestureDirection: AxisDirection.left); |
| }); |
| |
| testWidgets('fling-left with DismissDirection.endToStart triggers dismiss (LTR)', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildTest( |
| dismissDirection: DismissDirection.endToStart, |
| ), |
| ); |
| expect(dismissedItems, isEmpty); |
| |
| await dismissItem(tester, 0, gestureDirection: AxisDirection.right); |
| expect(find.text('0'), findsOneWidget); |
| expect(dismissedItems, isEmpty); |
| await dismissItem(tester, 1, gestureDirection: AxisDirection.right); |
| |
| await dismissItem(tester, 0, gestureDirection: AxisDirection.left); |
| expect(find.text('0'), findsNothing); |
| expect(dismissedItems, equals(<int>[0])); |
| await dismissItem(tester, 1, gestureDirection: AxisDirection.left); |
| }); |
| |
| testWidgets('fling-right with DismissDirection.startToEnd triggers dismiss (LTR)', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildTest( |
| dismissDirection: DismissDirection.startToEnd, |
| ), |
| ); |
| |
| expect(dismissedItems, isEmpty); |
| |
| await dismissItem(tester, 0, mechanism: flingElement, gestureDirection: AxisDirection.left); |
| expect(find.text('0'), findsOneWidget); |
| expect(dismissedItems, isEmpty); |
| |
| await dismissItem(tester, 0, mechanism: flingElement, gestureDirection: AxisDirection.right); |
| expect(find.text('0'), findsNothing); |
| expect(dismissedItems, equals(<int>[0])); |
| }); |
| |
| testWidgets('fling-right with DismissDirection.endToStart triggers dismiss (RTL)', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildTest( |
| textDirection: TextDirection.rtl, |
| dismissDirection: DismissDirection.endToStart, |
| ), |
| ); |
| expect(dismissedItems, isEmpty); |
| |
| await dismissItem(tester, 0, mechanism: flingElement, gestureDirection: AxisDirection.left); |
| expect(find.text('0'), findsOneWidget); |
| expect(dismissedItems, isEmpty); |
| |
| await dismissItem(tester, 0, mechanism: flingElement, gestureDirection: AxisDirection.right); |
| expect(find.text('0'), findsNothing); |
| expect(dismissedItems, equals(<int>[0])); |
| }); |
| |
| testWidgets('fling-left with DismissDirection.startToEnd triggers dismiss (RTL)', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildTest( |
| textDirection: TextDirection.rtl, |
| dismissDirection: DismissDirection.startToEnd, |
| ), |
| ); |
| expect(dismissedItems, isEmpty); |
| |
| await dismissItem(tester, 0, mechanism: flingElement, gestureDirection: AxisDirection.right); |
| expect(find.text('0'), findsOneWidget); |
| expect(dismissedItems, isEmpty); |
| await dismissItem(tester, 1, mechanism: flingElement, gestureDirection: AxisDirection.right); |
| |
| await dismissItem(tester, 0, mechanism: flingElement, gestureDirection: AxisDirection.left); |
| expect(find.text('0'), findsNothing); |
| expect(dismissedItems, equals(<int>[0])); |
| await dismissItem(tester, 1, mechanism: flingElement, gestureDirection: AxisDirection.left); |
| }); |
| |
| testWidgets('drag-up with DismissDirection.up triggers dismiss', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildTest( |
| scrollDirection: Axis.horizontal, |
| dismissDirection: DismissDirection.up, |
| ), |
| ); |
| expect(dismissedItems, isEmpty); |
| |
| await dismissItem(tester, 0, gestureDirection: AxisDirection.down); |
| expect(find.text('0'), findsOneWidget); |
| expect(dismissedItems, isEmpty); |
| |
| await dismissItem(tester, 0, gestureDirection: AxisDirection.up); |
| expect(find.text('0'), findsNothing); |
| expect(dismissedItems, equals(<int>[0])); |
| }); |
| |
| testWidgets('drag-down with DismissDirection.down triggers dismiss', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildTest( |
| scrollDirection: Axis.horizontal, |
| dismissDirection: DismissDirection.down, |
| ), |
| ); |
| expect(dismissedItems, isEmpty); |
| |
| await dismissItem(tester, 0, gestureDirection: AxisDirection.up); |
| expect(find.text('0'), findsOneWidget); |
| expect(dismissedItems, isEmpty); |
| |
| await dismissItem(tester, 0, gestureDirection: AxisDirection.down); |
| expect(find.text('0'), findsNothing); |
| expect(dismissedItems, equals(<int>[0])); |
| }); |
| |
| testWidgets('fling-up with DismissDirection.up triggers dismiss', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildTest( |
| scrollDirection: Axis.horizontal, |
| dismissDirection: DismissDirection.up, |
| ), |
| ); |
| expect(dismissedItems, isEmpty); |
| |
| await dismissItem(tester, 0, mechanism: flingElement, gestureDirection: AxisDirection.down); |
| expect(find.text('0'), findsOneWidget); |
| expect(dismissedItems, isEmpty); |
| |
| await dismissItem(tester, 0, mechanism: flingElement, gestureDirection: AxisDirection.up); |
| expect(find.text('0'), findsNothing); |
| expect(dismissedItems, equals(<int>[0])); |
| }); |
| |
| testWidgets('fling-down with DismissDirection.down triggers dismiss', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildTest( |
| scrollDirection: Axis.horizontal, |
| dismissDirection: DismissDirection.down, |
| ), |
| ); |
| expect(dismissedItems, isEmpty); |
| |
| await dismissItem(tester, 0, mechanism: flingElement, gestureDirection: AxisDirection.up); |
| expect(find.text('0'), findsOneWidget); |
| expect(dismissedItems, isEmpty); |
| |
| await dismissItem(tester, 0, mechanism: flingElement, gestureDirection: AxisDirection.down); |
| expect(find.text('0'), findsNothing); |
| expect(dismissedItems, equals(<int>[0])); |
| }); |
| |
| testWidgets('drag-left has no effect on dismissible with a high dismiss threshold', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildTest( |
| startToEndThreshold: 1.0, |
| ), |
| ); |
| expect(dismissedItems, isEmpty); |
| |
| await dismissItem(tester, 0, gestureDirection: AxisDirection.right); |
| expect(find.text('0'), findsOneWidget); |
| expect(dismissedItems, isEmpty); |
| |
| await dismissItem(tester, 0, gestureDirection: AxisDirection.left); |
| expect(find.text('0'), findsNothing); |
| expect(dismissedItems, equals(<int>[0])); |
| }); |
| |
| testWidgets('fling-left has no effect on dismissible with a high dismiss threshold', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildTest( |
| startToEndThreshold: 1.0, |
| ), |
| ); |
| expect(dismissedItems, isEmpty); |
| |
| await dismissItem(tester, 0, mechanism: flingElement, gestureDirection: AxisDirection.right); |
| expect(find.text('0'), findsOneWidget); |
| expect(dismissedItems, isEmpty); |
| |
| await dismissItem(tester, 0, mechanism: flingElement, gestureDirection: AxisDirection.left); |
| expect(find.text('0'), findsNothing); |
| expect(dismissedItems, equals(<int>[0])); |
| }); |
| |
| // This is a regression test for an fn2 bug where dragging a card caused an |
| // assert "'!_disqualifiedFromEverAppearingAgain' is not true". The old URL |
| // was https://github.com/domokit/sky_engine/issues/1068 but that issue is 404 |
| // now since we migrated to the new repo. The bug was fixed by |
| // https://github.com/flutter/engine/pull/1134 at the time, and later made |
| // irrelevant by fn3, but just in case... |
| testWidgets('Verify that drag-move events do not assert', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildTest( |
| scrollDirection: Axis.horizontal, |
| dismissDirection: DismissDirection.down, |
| ), |
| ); |
| final Offset location = tester.getTopLeft(find.text('0')); |
| const Offset offset = Offset(0.0, 5.0); |
| final TestGesture gesture = await tester.startGesture(location, pointer: 5); |
| await gesture.moveBy(offset); |
| await tester.pumpWidget(buildTest()); |
| await gesture.moveBy(offset); |
| await tester.pumpWidget(buildTest()); |
| await gesture.moveBy(offset); |
| await tester.pumpWidget(buildTest()); |
| await gesture.moveBy(offset); |
| await tester.pumpWidget(buildTest()); |
| await gesture.up(); |
| }); |
| |
| // This one is for a case where dismissing a widget above a previously |
| // dismissed widget threw an exception, which was documented at the |
| // now-obsolete URL https://github.com/flutter/engine/issues/1215 (the URL |
| // died in the migration to the new repo). Don't copy this test; it doesn't |
| // actually remove the dismissed widget, which is a violation of the |
| // Dismissible contract. This is not an example of good practice. |
| testWidgets('dismissing bottom then top (smoketest)', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: Center( |
| child: SizedBox( |
| width: 100.0, |
| height: 1000.0, |
| child: Column( |
| children: const <Widget>[ |
| Test1215DismissibleWidget('1'), |
| Test1215DismissibleWidget('2'), |
| ], |
| ), |
| ), |
| ), |
| ), |
| ); |
| expect(find.text('1'), findsOneWidget); |
| expect(find.text('2'), findsOneWidget); |
| await dismissElement(tester, find.text('2'), gestureDirection: AxisDirection.right); |
| await tester.pump(); // start the slide away |
| await tester.pump(const Duration(seconds: 1)); // finish the slide away |
| expect(find.text('1'), findsOneWidget); |
| expect(find.text('2'), findsNothing); |
| await dismissElement(tester, find.text('1'), gestureDirection: AxisDirection.right); |
| await tester.pump(); // start the slide away |
| await tester.pump(const Duration(seconds: 1)); // finish the slide away (at which point the child is no longer included in the tree) |
| expect(find.text('1'), findsNothing); |
| expect(find.text('2'), findsNothing); |
| }); |
| |
| testWidgets('Dismissible starts from the full size when collapsing', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| buildTest( |
| background: const Text('background'), |
| ), |
| ); |
| expect(dismissedItems, isEmpty); |
| |
| final Finder itemFinder = find.text('0'); |
| expect(itemFinder, findsOneWidget); |
| await dismissElement(tester, itemFinder, gestureDirection: AxisDirection.right); |
| await tester.pump(); |
| |
| expect(find.text('background'), findsOneWidget); // The other four have been culled. |
| final RenderBox backgroundBox = tester.firstRenderObject(find.text('background')); |
| expect(backgroundBox.size.height, equals(100.0)); |
| }); |
| |
| testWidgets('Checking fling item before movementDuration completes', (WidgetTester tester) async { |
| await tester.pumpWidget(buildTest()); |
| expect(dismissedItems, isEmpty); |
| |
| await checkFlingItemBeforeMovementEnd(tester, 0, gestureDirection: AxisDirection.left, mechanism: flingElement); |
| expect(find.text('0'), findsOneWidget); |
| |
| await checkFlingItemBeforeMovementEnd(tester, 1, gestureDirection: AxisDirection.right, mechanism: flingElement); |
| expect(find.text('1'), findsOneWidget); |
| }); |
| |
| testWidgets('Checking fling item after movementDuration', (WidgetTester tester) async { |
| await tester.pumpWidget(buildTest()); |
| expect(dismissedItems, isEmpty); |
| |
| await checkFlingItemAfterMovement(tester, 1, gestureDirection: AxisDirection.left, mechanism: flingElement); |
| expect(find.text('1'), findsNothing); |
| |
| await checkFlingItemAfterMovement(tester, 0, gestureDirection: AxisDirection.right, mechanism: flingElement); |
| expect(find.text('0'), findsNothing); |
| }); |
| |
| testWidgets('Horizontal fling less than threshold', (WidgetTester tester) async { |
| await tester.pumpWidget(buildTest(scrollDirection: Axis.horizontal)); |
| expect(dismissedItems, isEmpty); |
| |
| await checkFlingItemAfterMovement(tester, 0, gestureDirection: AxisDirection.left); |
| expect(find.text('0'), findsOneWidget); |
| expect(dismissedItems, isEmpty); |
| |
| await checkFlingItemAfterMovement(tester, 1, gestureDirection: AxisDirection.right); |
| expect(find.text('1'), findsOneWidget); |
| expect(dismissedItems, isEmpty); |
| }); |
| |
| testWidgets('Vertical fling less than threshold', (WidgetTester tester) async { |
| await tester.pumpWidget(buildTest()); |
| expect(dismissedItems, isEmpty); |
| |
| await checkFlingItemAfterMovement(tester, 0, gestureDirection: AxisDirection.left); |
| expect(find.text('0'), findsOneWidget); |
| expect(dismissedItems, isEmpty); |
| |
| await checkFlingItemAfterMovement(tester, 1, gestureDirection: AxisDirection.right); |
| expect(find.text('1'), findsOneWidget); |
| expect(dismissedItems, isEmpty); |
| }); |
| |
| testWidgets('confirmDismiss returns values: true, false, null', (WidgetTester tester) async { |
| late DismissDirection confirmDismissDirection; |
| |
| Widget buildFrame(bool? confirmDismissValue) { |
| return buildTest( |
| confirmDismiss: (BuildContext context, DismissDirection dismissDirection) { |
| confirmDismissDirection = dismissDirection; |
| return Future<bool?>.value(confirmDismissValue); |
| }, |
| ); |
| } |
| |
| // Dismiss is confirmed IFF confirmDismiss() returns true. |
| await tester.pumpWidget(buildFrame(true)); |
| expect(dismissedItems, isEmpty); |
| |
| await dismissItem(tester, 0, gestureDirection: AxisDirection.right, mechanism: flingElement); |
| expect(find.text('0'), findsNothing); |
| expect(dismissedItems, equals(<int>[0])); |
| expect(reportedDismissDirection, DismissDirection.startToEnd); |
| expect(confirmDismissDirection, DismissDirection.startToEnd); |
| |
| await dismissItem(tester, 1, gestureDirection: AxisDirection.left, mechanism: flingElement); |
| expect(find.text('1'), findsNothing); |
| expect(dismissedItems, equals(<int>[0, 1])); |
| expect(reportedDismissDirection, DismissDirection.endToStart); |
| expect(confirmDismissDirection, DismissDirection.endToStart); |
| |
| // Dismiss is not confirmed if confirmDismiss() returns false |
| dismissedItems = <int>[]; |
| await tester.pumpWidget(buildFrame(false)); |
| |
| await dismissItem(tester, 0, gestureDirection: AxisDirection.right, mechanism: flingElement); |
| expect(find.text('0'), findsOneWidget); |
| expect(dismissedItems, isEmpty); |
| expect(confirmDismissDirection, DismissDirection.startToEnd); |
| |
| await dismissItem(tester, 1, gestureDirection: AxisDirection.left, mechanism: flingElement); |
| expect(find.text('1'), findsOneWidget); |
| expect(dismissedItems, isEmpty); |
| expect(confirmDismissDirection, DismissDirection.endToStart); |
| |
| // Dismiss is not confirmed if confirmDismiss() returns null |
| dismissedItems = <int>[]; |
| await tester.pumpWidget(buildFrame(null)); |
| |
| await dismissItem(tester, 0, gestureDirection: AxisDirection.right, mechanism: flingElement); |
| expect(find.text('0'), findsOneWidget); |
| expect(dismissedItems, isEmpty); |
| expect(confirmDismissDirection, DismissDirection.startToEnd); |
| |
| await dismissItem(tester, 1, gestureDirection: AxisDirection.left, mechanism: flingElement); |
| expect(find.text('1'), findsOneWidget); |
| expect(dismissedItems, isEmpty); |
| expect(confirmDismissDirection, DismissDirection.endToStart); |
| }); |
| |
| testWidgets('Pending confirmDismiss does not cause errors', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/54990 |
| |
| late Completer<bool?> completer; |
| Widget buildFrame() { |
| completer = Completer<bool?>(); |
| return buildTest( |
| confirmDismiss: (BuildContext context, DismissDirection dismissDirection) { |
| return completer.future; |
| }, |
| ); |
| } |
| |
| // false for _handleDragEnd - when dragged to the end and released |
| |
| await tester.pumpWidget(buildFrame()); |
| |
| await dismissItem(tester, 0, gestureDirection: AxisDirection.right); |
| expect(find.text('0'), findsOneWidget); |
| expect(dismissedItems, isEmpty); |
| |
| await tester.pumpWidget(const SizedBox()); |
| completer.complete(false); |
| await tester.pump(); |
| |
| // true for _handleDragEnd - when dragged to the end and released |
| |
| await tester.pumpWidget(buildFrame()); |
| |
| await dismissItem(tester, 0, gestureDirection: AxisDirection.right); |
| expect(find.text('0'), findsOneWidget); |
| expect(dismissedItems, isEmpty); |
| |
| await tester.pumpWidget(const SizedBox()); |
| completer.complete(true); |
| await tester.pump(); |
| |
| // false for _handleDismissStatusChanged - when fling reaches the end |
| |
| await tester.pumpWidget(buildFrame()); |
| |
| await dismissItem(tester, 0, gestureDirection: AxisDirection.right, mechanism: flingElement); |
| expect(find.text('0'), findsOneWidget); |
| expect(dismissedItems, isEmpty); |
| |
| await tester.pumpWidget(const SizedBox()); |
| completer.complete(false); |
| await tester.pump(); |
| |
| // true for _handleDismissStatusChanged - when fling reaches the end |
| |
| await tester.pumpWidget(buildFrame()); |
| |
| await dismissItem(tester, 0, gestureDirection: AxisDirection.right, mechanism: flingElement); |
| expect(find.text('0'), findsOneWidget); |
| expect(dismissedItems, isEmpty); |
| |
| await tester.pumpWidget(const SizedBox()); |
| completer.complete(true); |
| await tester.pump(); |
| }); |
| |
| testWidgets('Dismissible cannot be dragged with pending confirmDismiss', (WidgetTester tester) async { |
| final Completer<bool?> completer = Completer<bool?>(); |
| await tester.pumpWidget( |
| buildTest( |
| confirmDismiss: (BuildContext context, DismissDirection dismissDirection) { |
| return completer.future; |
| }, |
| ), |
| ); |
| |
| // Trigger confirmDismiss call. |
| await dismissItem(tester, 0, gestureDirection: AxisDirection.right); |
| final Offset position = tester.getTopLeft(find.text('0')); |
| |
| // Try to move and verify it has not moved. |
| Offset dragAt = tester.getTopLeft(find.text('0')); |
| dragAt = Offset(100.0, dragAt.dy); |
| final TestGesture gesture = await tester.startGesture(dragAt); |
| await gesture.moveTo(dragAt + const Offset(100.0, 0.0)); |
| await gesture.up(); |
| await tester.pump(); |
| expect(tester.getTopLeft(find.text('0')), position); |
| }); |
| |
| testWidgets('Drag to end and release - items does not get stuck if confirmDismiss returns false', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/87556 |
| |
| final Completer<bool?> completer = Completer<bool?>(); |
| await tester.pumpWidget( |
| buildTest( |
| confirmDismiss: (BuildContext context, DismissDirection dismissDirection) { |
| return completer.future; |
| }, |
| ), |
| ); |
| |
| final Offset position = tester.getTopLeft(find.text('0')); |
| await dismissItem(tester, 0, gestureDirection: AxisDirection.right); |
| completer.complete(false); |
| await tester.pumpAndSettle(); |
| expect(tester.getTopLeft(find.text('0')), position); |
| }); |
| |
| testWidgets('Dismissible with null resizeDuration calls onDismissed immediately', (WidgetTester tester) async { |
| bool resized = false; |
| bool dismissed = false; |
| |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: Dismissible( |
| dragStartBehavior: DragStartBehavior.down, |
| key: UniqueKey(), |
| resizeDuration: null, |
| onDismissed: (DismissDirection direction) { |
| dismissed = true; |
| }, |
| onResize: () { |
| resized = true; |
| }, |
| child: const SizedBox( |
| width: 100.0, |
| height: 100.0, |
| child: Text('0'), |
| ), |
| ), |
| ), |
| ); |
| |
| await dismissElement(tester, find.text('0'), gestureDirection: AxisDirection.right); |
| await tester.pump(); |
| expect(dismissed, true); |
| expect(resized, false); |
| }); |
| |
| testWidgets('setState that does not remove the Dismissible from tree should throw Error', (WidgetTester tester) async { |
| await tester.pumpWidget(Directionality( |
| textDirection: TextDirection.ltr, |
| child: StatefulBuilder( |
| builder: (BuildContext context, StateSetter setState) { |
| return ListView( |
| dragStartBehavior: DragStartBehavior.down, |
| itemExtent: 100.0, |
| children: <Widget>[ |
| Dismissible( |
| dragStartBehavior: DragStartBehavior.down, |
| key: const ValueKey<int>(1), |
| onDismissed: (DismissDirection direction) { |
| setState(() { |
| reportedDismissDirection = direction; |
| expect(dismissedItems.contains(1), isFalse); |
| dismissedItems.add(1); |
| }); |
| }, |
| crossAxisEndOffset: crossAxisEndOffset, |
| child: SizedBox( |
| width: 100.0, |
| height: 100.0, |
| child: Text(1.toString()), |
| ), |
| ), |
| ], |
| ); |
| }, |
| ), |
| )); |
| expect(dismissedItems, isEmpty); |
| await dismissItem(tester, 1, gestureDirection: AxisDirection.right); |
| expect(dismissedItems, equals(<int>[1])); |
| final dynamic exception = tester.takeException(); |
| expect(exception, isNotNull); |
| expect(exception, isFlutterError); |
| final FlutterError error = exception as FlutterError; |
| expect(error.diagnostics.last.level, DiagnosticLevel.hint); |
| expect( |
| error.diagnostics.last.toStringDeep(), |
| equalsIgnoringHashCodes( |
| 'Make sure to implement the onDismissed handler and to immediately\n' |
| 'remove the Dismissible widget from the application once that\n' |
| 'handler has fired.\n', |
| ), |
| ); |
| expect( |
| error.toStringDeep(), |
| 'FlutterError\n' |
| ' A dismissed Dismissible widget is still part of the tree.\n' |
| ' Make sure to implement the onDismissed handler and to immediately\n' |
| ' remove the Dismissible widget from the application once that\n' |
| ' handler has fired.\n', |
| ); |
| }); |
| |
| testWidgets('Dismissible.behavior should behave correctly during hit testing', (WidgetTester tester) async { |
| bool didReceivePointerDown = false; |
| |
| Widget buildStack({required Widget child}) { |
| return Directionality( |
| textDirection: TextDirection.ltr, |
| child: Stack( |
| children: <Widget>[ |
| Listener( |
| onPointerDown: (_) { |
| didReceivePointerDown = true; |
| }, |
| child: Container( |
| width: 100.0, |
| height: 100.0, |
| color: const Color(0xFF00FF00), |
| ), |
| ), |
| child, |
| ], |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget( |
| buildStack( |
| child: const Dismissible( |
| key: ValueKey<int>(1), |
| child: SizedBox( |
| width: 100.0, |
| height: 100.0, |
| ), |
| ), |
| ), |
| ); |
| await tester.tapAt(const Offset(10.0, 10.0)); |
| expect(didReceivePointerDown, isFalse); |
| |
| Future<void> pumpWidgetTree(HitTestBehavior behavior) { |
| return tester.pumpWidget( |
| buildStack( |
| child: Dismissible( |
| key: const ValueKey<int>(1), |
| behavior: behavior, |
| child: const SizedBox( |
| width: 100.0, |
| height: 100.0, |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| didReceivePointerDown = false; |
| await pumpWidgetTree(HitTestBehavior.deferToChild); |
| await tester.tapAt(const Offset(10.0, 10.0)); |
| expect(didReceivePointerDown, isTrue); |
| |
| didReceivePointerDown = false; |
| await pumpWidgetTree(HitTestBehavior.opaque); |
| await tester.tapAt(const Offset(10.0, 10.0)); |
| expect(didReceivePointerDown, isFalse); |
| |
| didReceivePointerDown = false; |
| await pumpWidgetTree(HitTestBehavior.translucent); |
| await tester.tapAt(const Offset(10.0, 10.0)); |
| expect(didReceivePointerDown, isTrue); |
| }); |
| |
| testWidgets('DismissDirection.none does not trigger dismiss', (WidgetTester tester) async { |
| await tester.pumpWidget(buildTest( |
| dismissDirection: DismissDirection.none, |
| scrollPhysics: const NeverScrollableScrollPhysics(), |
| )); |
| expect(dismissedItems, isEmpty); |
| |
| await dismissItem(tester, 0, gestureDirection: AxisDirection.left); |
| await dismissItem(tester, 0, gestureDirection: AxisDirection.right); |
| await dismissItem(tester, 0, gestureDirection: AxisDirection.up); |
| await dismissItem(tester, 0, gestureDirection: AxisDirection.down); |
| expect(find.text('0'), findsOneWidget); |
| }); |
| |
| testWidgets('DismissDirection.none does not prevent scrolling', (WidgetTester tester) async { |
| final ScrollController controller = ScrollController(); |
| |
| await tester.pumpWidget( |
| buildTest( |
| controller: controller, |
| dismissDirection: DismissDirection.none, |
| ), |
| ); |
| expect(dismissedItems, isEmpty); |
| expect(controller.offset, 0.0); |
| |
| await dismissItem(tester, 0, gestureDirection: AxisDirection.left); |
| expect(controller.offset, 0.0); |
| await dismissItem(tester, 0, gestureDirection: AxisDirection.right); |
| expect(controller.offset, 0.0); |
| await dismissItem(tester, 0, gestureDirection: AxisDirection.down); |
| expect(controller.offset, 0.0); |
| await dismissItem(tester, 0, gestureDirection: AxisDirection.up); |
| expect(controller.offset, 100.0); |
| controller.dispose(); |
| }); |
| |
| testWidgets('onUpdate', (WidgetTester tester) async { |
| await tester.pumpWidget(buildTest( |
| scrollDirection: Axis.horizontal, |
| )); |
| expect(dismissedItems, isEmpty); |
| |
| // Successful dismiss therefore threshold has been reached |
| await dismissItem(tester, 0, mechanism: flingElement, gestureDirection: AxisDirection.left); |
| expect(find.text('0'), findsNothing); |
| expect(dismissedItems, equals(<int>[0])); |
| expect(reportedDismissUpdateReachedDirection, DismissDirection.endToStart); |
| expect(reportedDismissUpdateReached, true); |
| expect(reportedDismissUpdatePreviousReached, true); |
| |
| // Unsuccessful dismiss, threshold has not been reached |
| await checkFlingItemAfterMovement(tester, 1, gestureDirection: AxisDirection.right); |
| expect(find.text('1'), findsOneWidget); |
| expect(dismissedItems, equals(<int>[0])); |
| expect(reportedDismissUpdateReachedDirection, DismissDirection.startToEnd); |
| expect(reportedDismissUpdateReached, false); |
| expect(reportedDismissUpdatePreviousReached, false); |
| |
| // Another successful dismiss from another direction |
| await dismissItem(tester, 1, mechanism: flingElement, gestureDirection: AxisDirection.right); |
| expect(find.text('1'), findsNothing); |
| expect(dismissedItems, equals(<int>[0, 1])); |
| expect(reportedDismissUpdateReachedDirection, DismissDirection.startToEnd); |
| expect(reportedDismissUpdateReached, true); |
| expect(reportedDismissUpdatePreviousReached, true); |
| |
| await tester.pumpWidget(buildTest( |
| scrollDirection: Axis.horizontal, |
| confirmDismiss: (BuildContext context, DismissDirection dismissDirection) { |
| return Future<bool>.value(false); |
| }, |
| )); |
| |
| // Threshold has been reached but dismiss was not confirmed |
| await dismissItem(tester, 2, mechanism: flingElement, gestureDirection: AxisDirection.right); |
| expect(find.text('2'), findsOneWidget); |
| expect(dismissedItems, equals(<int>[0, 1])); |
| expect(reportedDismissUpdateReachedDirection, DismissDirection.startToEnd); |
| expect(reportedDismissUpdateReached, false); |
| expect(reportedDismissUpdatePreviousReached, false); |
| }); |
| } |