// 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'; import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; const DismissDirection defaultDismissDirection = DismissDirection.horizontal; const double crossAxisEndOffset = 0.5; bool reportedDismissUpdateReached = false; bool reportedDismissUpdatePreviousReached = false; double reportedDismissUpdateProgress = 0.0; 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; reportedDismissUpdateProgress = details.progress; }, 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); 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); 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); 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); } final TestGesture gesture = await tester.startGesture(downLocation); await gesture.moveTo(upLocation); await gesture.up(); } Future<void> dragElement(WidgetTester tester, Finder finder, { required AxisDirection gestureDirection, required double amount }) async { Offset delta; switch (gestureDirection) { case AxisDirection.left: delta = Offset(-amount, 0.0); case AxisDirection.right: delta = Offset(amount, 0.0); case AxisDirection.up: delta = Offset(0.0, -amount); case AxisDirection.down: delta = Offset(0.0, amount); } await tester.drag(finder, delta); } 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); case AxisDirection.right: delta = const Offset(300.0, 0.0); case AxisDirection.up: delta = const Offset(0.0, -300.0); case AxisDirection.down: delta = const Offset(0.0, 300.0); } 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 { final Finder itemFinder = find.text(item.toString()); expect(itemFinder, findsOneWidget); await mechanism(tester, itemFinder, gestureDirection: gestureDirection); await tester.pumpAndSettle(); } Future<void> dragItem( WidgetTester tester, int item, { required AxisDirection gestureDirection, required double amount, }) async { final Finder itemFinder = find.text(item.toString()); expect(itemFinder, findsOneWidget); await dragElement(tester, itemFinder, gestureDirection: gestureDirection, amount: amount); await tester.pump(); } Future<void> checkFlingItemBeforeMovementEnd( WidgetTester tester, int item, { required AxisDirection gestureDirection, DismissMethod mechanism = rollbackElement, }) async { 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 { 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); case AxisDirection.right: delta = const Offset(30.0, 0.0); case AxisDirection.up: delta = const Offset(0.0, -30.0); case AxisDirection.down: delta = const Offset(0.0, 30.0); } await tester.fling(finder, delta, 1000.0, initialOffset: delta * initialOffsetFactor); } class Test1215DismissibleWidget extends StatelessWidget { const Test1215DismissibleWidget(this.text, { super.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>[]; }); testWidgetsWithLeakTracking('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); }); testWidgetsWithLeakTracking('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); }); testWidgetsWithLeakTracking('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); }); testWidgetsWithLeakTracking('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); }); testWidgetsWithLeakTracking('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); }); testWidgetsWithLeakTracking('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])); }); testWidgetsWithLeakTracking('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])); }); testWidgetsWithLeakTracking('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); }); testWidgetsWithLeakTracking('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); }); testWidgetsWithLeakTracking('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])); }); testWidgetsWithLeakTracking('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])); }); testWidgetsWithLeakTracking('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); }); testWidgetsWithLeakTracking('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])); }); testWidgetsWithLeakTracking('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])); }); testWidgetsWithLeakTracking('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])); }); testWidgetsWithLeakTracking('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])); }); testWidgetsWithLeakTracking('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])); }); testWidgetsWithLeakTracking('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... testWidgetsWithLeakTracking('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. testWidgetsWithLeakTracking('dismissing bottom then top (smoketest)', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, child: Center( child: SizedBox( width: 100.0, height: 1000.0, child: Column( children: <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); }); testWidgetsWithLeakTracking('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)); }); testWidgetsWithLeakTracking('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); }); testWidgetsWithLeakTracking('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); }); testWidgetsWithLeakTracking('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); }); testWidgetsWithLeakTracking('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); }); testWidgetsWithLeakTracking('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); }); testWidgetsWithLeakTracking('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(); }); testWidgetsWithLeakTracking('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); }); testWidgetsWithLeakTracking('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); }); testWidgetsWithLeakTracking('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); }); testWidgetsWithLeakTracking('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', ); }); testWidgetsWithLeakTracking('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); }); testWidgetsWithLeakTracking('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); }); testWidgetsWithLeakTracking('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(); }); testWidgetsWithLeakTracking('onUpdate', (WidgetTester tester) async { await tester.pumpWidget(buildTest( scrollDirection: Axis.horizontal, )); expect(dismissedItems, isEmpty); // Unsuccessful dismiss, fractional progress reported await dragItem(tester, 0, gestureDirection: AxisDirection.right, amount: 20); expect(reportedDismissUpdateProgress, 0.2); // 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); expect(reportedDismissUpdateProgress, 1.0); // 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); expect(reportedDismissUpdateProgress, 0.0); // 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); expect(reportedDismissUpdateProgress, 1.0); 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); expect(reportedDismissUpdateProgress, 0.0); }); testWidgetsWithLeakTracking('Change direction does not lose child state', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/108961 Widget buildFrame(DismissDirection direction) { return Directionality( textDirection: TextDirection.ltr, child: Dismissible( dragStartBehavior: DragStartBehavior.down, direction: direction, key: const Key('Dismissible'), resizeDuration: null, child: const SizedBox( width: 100.0, height: 100.0, child: Text('I Love Flutter!'), ), ), ); } await tester.pumpWidget(buildFrame(DismissDirection.horizontal)); final RenderBox textRenderObjectBegin = tester.renderObject(find.text('I Love Flutter!')); await tester.pumpWidget(buildFrame(DismissDirection.none)); final RenderBox textRenderObjectEnd = tester.renderObject(find.text('I Love Flutter!')); expect(identical(textRenderObjectBegin, textRenderObjectEnd), true); }); }