blob: c8b3511ddd358178fb331ac7ed7fff8e7e68b959 [file] [log] [blame]
// 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);
});
}