blob: 915347390a64f8b01a6fbca9bda24de376233345 [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 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
Widget _boilerplate(VoidCallback? onButtonPressed, {
DraggableScrollableController? controller,
int itemCount = 100,
double initialChildSize = .5,
double maxChildSize = 1.0,
double minChildSize = .25,
bool snap = false,
List<double>? snapSizes,
double? itemExtent,
Key? containerKey,
Key? stackKey,
NotificationListenerCallback<ScrollNotification>? onScrollNotification,
bool ignoreController = false,
}) {
return Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: Stack(
key: stackKey,
children: <Widget>[
TextButton(
onPressed: onButtonPressed,
child: const Text('TapHere'),
),
DraggableScrollableActuator(
child: DraggableScrollableSheet(
controller: controller,
maxChildSize: maxChildSize,
minChildSize: minChildSize,
initialChildSize: initialChildSize,
snap: snap,
snapSizes: snapSizes,
builder: (BuildContext context, ScrollController scrollController) {
return NotificationListener<ScrollNotification>(
onNotification: onScrollNotification,
child: Container(
key: containerKey,
color: const Color(0xFFABCDEF),
child: ListView.builder(
controller: ignoreController ? null : scrollController,
itemExtent: itemExtent,
itemCount: itemCount,
itemBuilder: (BuildContext context, int index) => Text('Item $index'),
),
),
);
},
),
),
],
),
),
);
}
testWidgets('Scrolls correct amount when maxChildSize < 1.0', (WidgetTester tester) async {
const Key key = ValueKey<String>('container');
await tester.pumpWidget(_boilerplate(
null,
maxChildSize: .6,
initialChildSize: .25,
itemExtent: 25.0,
containerKey: key,
));
expect(tester.getRect(find.byKey(key)), const Rect.fromLTRB(0.0, 450.0, 800.0, 600.0));
await tester.drag(find.text('Item 5'), const Offset(0, -125));
await tester.pumpAndSettle();
expect(tester.getRect(find.byKey(key)), const Rect.fromLTRB(0.0, 325.0, 800.0, 600.0));
});
testWidgets('Scrolls correct amount when maxChildSize == 1.0', (WidgetTester tester) async {
const Key key = ValueKey<String>('container');
await tester.pumpWidget(_boilerplate(
null,
initialChildSize: .25,
itemExtent: 25.0,
containerKey: key,
));
expect(tester.getRect(find.byKey(key)), const Rect.fromLTRB(0.0, 450.0, 800.0, 600.0));
await tester.drag(find.text('Item 5'), const Offset(0, -125));
await tester.pumpAndSettle();
expect(tester.getRect(find.byKey(key)), const Rect.fromLTRB(0.0, 325.0, 800.0, 600.0));
});
testWidgets('Invalid snap targets throw assertion errors.', (WidgetTester tester) async {
await tester.pumpWidget(_boilerplate(
null,
maxChildSize: .8,
snapSizes: <double>[.9],
));
expect(tester.takeException(), isAssertionError);
await tester.pumpWidget(_boilerplate(
null,
snapSizes: <double>[.1],
));
expect(tester.takeException(), isAssertionError);
await tester.pumpWidget(_boilerplate(
null,
snapSizes: <double>[.6, .6, .9],
));
expect(tester.takeException(), isAssertionError);
});
group('Scroll Physics', () {
testWidgets('Can be dragged up without covering its container', (WidgetTester tester) async {
int taps = 0;
await tester.pumpWidget(_boilerplate(() => taps++));
expect(find.text('TapHere'), findsOneWidget);
await tester.tap(find.text('TapHere'));
expect(taps, 1);
expect(find.text('Item 1'), findsOneWidget);
expect(find.text('Item 21'), findsOneWidget);
expect(find.text('Item 31'), findsNothing);
await tester.drag(find.text('Item 1'), const Offset(0, -200));
await tester.pumpAndSettle();
expect(find.text('TapHere'), findsOneWidget);
await tester.tap(find.text('TapHere'));
expect(taps, 2);
expect(find.text('Item 1'), findsOneWidget);
expect(find.text('Item 21'), findsOneWidget);
expect(find.text('Item 31'), findsOneWidget);
}, variant: TargetPlatformVariant.all());
testWidgets('Can be dragged down when not full height', (WidgetTester tester) async {
await tester.pumpWidget(_boilerplate(null));
expect(find.text('Item 1'), findsOneWidget);
expect(find.text('Item 21'), findsOneWidget);
expect(find.text('Item 36'), findsNothing);
await tester.drag(find.text('Item 1'), const Offset(0, 325));
await tester.pumpAndSettle();
expect(find.text('Item 1'), findsOneWidget);
expect(find.text('Item 21'), findsNothing);
expect(find.text('Item 36'), findsNothing);
}, variant: TargetPlatformVariant.all());
testWidgets('Can be dragged down when list is shorter than full height', (WidgetTester tester) async {
await tester.pumpWidget(_boilerplate(null, itemCount: 30, initialChildSize: .25));
expect(find.text('Item 1').hitTestable(), findsOneWidget);
expect(find.text('Item 29').hitTestable(), findsNothing);
await tester.drag(find.text('Item 1'), const Offset(0, -325));
await tester.pumpAndSettle();
expect(find.text('Item 1').hitTestable(), findsOneWidget);
expect(find.text('Item 29').hitTestable(), findsOneWidget);
await tester.drag(find.text('Item 1'), const Offset(0, 325));
await tester.pumpAndSettle();
expect(find.text('Item 1').hitTestable(), findsOneWidget);
expect(find.text('Item 29').hitTestable(), findsNothing);
}, variant: TargetPlatformVariant.all());
testWidgets('Can be dragged up and cover its container and scroll in single motion, and then dragged back down', (WidgetTester tester) async {
int taps = 0;
await tester.pumpWidget(_boilerplate(() => taps++));
expect(find.text('TapHere'), findsOneWidget);
await tester.tap(find.text('TapHere'));
expect(taps, 1);
expect(find.text('Item 1'), findsOneWidget);
expect(find.text('Item 21'), findsOneWidget);
expect(find.text('Item 36'), findsNothing);
await tester.drag(find.text('Item 1'), const Offset(0, -325));
await tester.pumpAndSettle();
expect(find.text('TapHere'), findsOneWidget);
await tester.tap(find.text('TapHere'), warnIfMissed: false);
expect(taps, 1);
expect(find.text('Item 1'), findsOneWidget);
expect(find.text('Item 21'), findsOneWidget);
expect(find.text('Item 36'), findsOneWidget);
await tester.dragFrom(const Offset(20, 20), const Offset(0, 325));
await tester.pumpAndSettle();
await tester.tap(find.text('TapHere'));
expect(taps, 2);
expect(find.text('Item 1'), findsOneWidget);
expect(find.text('Item 18'), findsOneWidget);
expect(find.text('Item 36'), findsNothing);
}, variant: TargetPlatformVariant.all());
testWidgets('Can be flung up gently', (WidgetTester tester) async {
int taps = 0;
await tester.pumpWidget(_boilerplate(() => taps++));
expect(find.text('TapHere'), findsOneWidget);
await tester.tap(find.text('TapHere'));
expect(taps, 1);
expect(find.text('Item 1'), findsOneWidget);
expect(find.text('Item 21'), findsOneWidget);
expect(find.text('Item 36'), findsNothing);
expect(find.text('Item 70'), findsNothing);
await tester.fling(find.text('Item 1'), const Offset(0, -200), 350);
await tester.pumpAndSettle();
expect(find.text('TapHere'), findsOneWidget);
await tester.tap(find.text('TapHere'));
expect(taps, 2);
expect(find.text('Item 1'), findsOneWidget);
expect(find.text('Item 21'), findsOneWidget);
expect(find.text('Item 36'), findsOneWidget);
expect(find.text('Item 70'), findsNothing);
}, variant: TargetPlatformVariant.all());
testWidgets('Can be flung up', (WidgetTester tester) async {
int taps = 0;
await tester.pumpWidget(_boilerplate(() => taps++));
expect(find.text('TapHere'), findsOneWidget);
await tester.tap(find.text('TapHere'));
expect(taps, 1);
expect(find.text('Item 1'), findsOneWidget);
expect(find.text('Item 21'), findsOneWidget);
expect(find.text('Item 70'), findsNothing);
await tester.fling(find.text('Item 1'), const Offset(0, -200), 2000);
await tester.pumpAndSettle();
expect(find.text('TapHere'), findsOneWidget);
await tester.tap(find.text('TapHere'), warnIfMissed: false);
expect(taps, 1);
expect(find.text('Item 1'), findsNothing);
expect(find.text('Item 21'), findsNothing);
expect(find.text('Item 70'), findsOneWidget);
}, variant: TargetPlatformVariant.all());
testWidgets('Can be flung down when not full height', (WidgetTester tester) async {
await tester.pumpWidget(_boilerplate(null));
expect(find.text('Item 1'), findsOneWidget);
expect(find.text('Item 21'), findsOneWidget);
expect(find.text('Item 36'), findsNothing);
await tester.fling(find.text('Item 1'), const Offset(0, 325), 2000);
await tester.pumpAndSettle();
expect(find.text('Item 1'), findsOneWidget);
expect(find.text('Item 21'), findsNothing);
expect(find.text('Item 36'), findsNothing);
}, variant: TargetPlatformVariant.all());
testWidgets('Can be flung up and then back down', (WidgetTester tester) async {
int taps = 0;
await tester.pumpWidget(_boilerplate(() => taps++));
expect(find.text('TapHere'), findsOneWidget);
await tester.tap(find.text('TapHere'));
expect(taps, 1);
expect(find.text('Item 1'), findsOneWidget);
expect(find.text('Item 21'), findsOneWidget);
expect(find.text('Item 70'), findsNothing);
await tester.fling(find.text('Item 1'), const Offset(0, -200), 2000);
await tester.pumpAndSettle();
expect(find.text('TapHere'), findsOneWidget);
await tester.tap(find.text('TapHere'), warnIfMissed: false);
expect(taps, 1);
expect(find.text('Item 1'), findsNothing);
expect(find.text('Item 21'), findsNothing);
expect(find.text('Item 70'), findsOneWidget);
await tester.fling(find.text('Item 70'), const Offset(0, 200), 2000);
await tester.pumpAndSettle();
expect(find.text('TapHere'), findsOneWidget);
await tester.tap(find.text('TapHere'), warnIfMissed: false);
expect(taps, 1);
expect(find.text('Item 1'), findsOneWidget);
expect(find.text('Item 21'), findsOneWidget);
expect(find.text('Item 70'), findsNothing);
await tester.fling(find.text('Item 1'), const Offset(0, 200), 2000);
await tester.pumpAndSettle();
expect(find.text('TapHere'), findsOneWidget);
await tester.tap(find.text('TapHere'));
expect(taps, 2);
expect(find.text('Item 1'), findsOneWidget);
expect(find.text('Item 21'), findsNothing);
expect(find.text('Item 70'), findsNothing);
}, variant: TargetPlatformVariant.all());
testWidgets('Ballistic animation on fling can be interrupted', (WidgetTester tester) async {
int taps = 0;
await tester.pumpWidget(_boilerplate(() => taps++));
expect(find.text('TapHere'), findsOneWidget);
await tester.tap(find.text('TapHere'));
expect(taps, 1);
expect(find.text('Item 1'), findsOneWidget);
expect(find.text('Item 31'), findsNothing);
expect(find.text('Item 70'), findsNothing);
await tester.fling(find.text('Item 1'), const Offset(0, -200), 2000);
// Don't pump and settle because we want to interrupt the ballistic scrolling animation.
expect(find.text('TapHere'), findsOneWidget);
await tester.tap(find.text('TapHere'), warnIfMissed: false);
expect(taps, 2);
expect(find.text('Item 1'), findsOneWidget);
expect(find.text('Item 31'), findsOneWidget);
expect(find.text('Item 70'), findsNothing);
// Use `dragFrom` here because calling `drag` on a list item without
// first calling `pumpAndSettle` fails with a hit test error.
await tester.dragFrom(const Offset(0, 200), const Offset(0, 200));
await tester.pumpAndSettle();
// Verify that the ballistic animation has canceled and the sheet has
// returned to it's original position.
await tester.tap(find.text('TapHere'));
expect(taps, 3);
expect(find.text('Item 1'), findsOneWidget);
expect(find.text('Item 31'), findsNothing);
expect(find.text('Item 70'), findsNothing);
}, variant: TargetPlatformVariant.all());
debugDefaultTargetPlatformOverride = null;
});
testWidgets('Does not snap away from initial child on build', (WidgetTester tester) async {
const Key containerKey = ValueKey<String>('container');
const Key stackKey = ValueKey<String>('stack');
await tester.pumpWidget(_boilerplate(null,
snap: true,
initialChildSize: .7,
containerKey: containerKey,
stackKey: stackKey,
));
await tester.pumpAndSettle();
final double screenHeight = tester.getSize(find.byKey(stackKey)).height;
// The sheet should not have snapped.
expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(.7, precisionErrorTolerance,
));
}, variant: TargetPlatformVariant.all());
for (final bool useActuator in <bool>[false, true]) {
testWidgets('Does not snap away from initial child on ${useActuator ? 'actuator' : 'controller'}.reset()', (WidgetTester tester) async {
const Key containerKey = ValueKey<String>('container');
const Key stackKey = ValueKey<String>('stack');
final DraggableScrollableController controller = DraggableScrollableController();
await tester.pumpWidget(_boilerplate(
null,
controller: controller,
snap: true,
containerKey: containerKey,
stackKey: stackKey,
));
await tester.pumpAndSettle();
final double screenHeight = tester.getSize(find.byKey(stackKey)).height;
await tester.drag(find.text('Item 1'), Offset(0, -.4 * screenHeight));
await tester.pumpAndSettle();
expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(1.0, precisionErrorTolerance),
);
if (useActuator) {
DraggableScrollableActuator.reset(tester.element(find.byKey(containerKey)));
} else {
controller.reset();
}
await tester.pumpAndSettle();
// The sheet should have reset without snapping away from initial child.
expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(.5, precisionErrorTolerance),
);
});
}
testWidgets('Zero velocity drag snaps to nearest snap target', (WidgetTester tester) async {
const Key stackKey = ValueKey<String>('stack');
const Key containerKey = ValueKey<String>('container');
await tester.pumpWidget(_boilerplate(null,
snap: true,
stackKey: stackKey,
containerKey: containerKey,
snapSizes: <double>[.25, .5, .75, 1.0],
));
await tester.pumpAndSettle();
final double screenHeight = tester.getSize(find.byKey(stackKey)).height;
// We are dragging up, but we'll snap down because we're closer to .75 than 1.
await tester.drag(find.text('Item 1'), Offset(0, -.35 * screenHeight));
await tester.pumpAndSettle();
expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(.75, precisionErrorTolerance),
);
// Drag up and snap up.
await tester.drag(find.text('Item 1'), Offset(0, -.2 * screenHeight));
await tester.pumpAndSettle();
expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(1.0, precisionErrorTolerance),
);
// Drag down and snap up.
await tester.drag(find.text('Item 1'), Offset(0, .1 * screenHeight));
await tester.pumpAndSettle();
expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(1.0, precisionErrorTolerance),
);
// Drag down and snap down.
await tester.drag(find.text('Item 1'), Offset(0, .45 * screenHeight));
await tester.pumpAndSettle();
expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(.5, precisionErrorTolerance),
);
// Fling up with negligible velocity and snap down.
await tester.fling(find.text('Item 1'), Offset(0, .1 * screenHeight), 1);
await tester.pumpAndSettle();
expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(.5, precisionErrorTolerance),
);
}, variant: TargetPlatformVariant.all());
for (final List<double>? snapSizes in <List<double>?>[null, <double>[]]) {
testWidgets('Setting snapSizes to $snapSizes resolves to min and max', (WidgetTester tester) async {
const Key stackKey = ValueKey<String>('stack');
const Key containerKey = ValueKey<String>('container');
await tester.pumpWidget(_boilerplate(null,
snap: true,
stackKey: stackKey,
containerKey: containerKey,
snapSizes: snapSizes,
));
await tester.pumpAndSettle();
final double screenHeight = tester.getSize(find.byKey(stackKey)).height;
await tester.drag(find.text('Item 1'), Offset(0, -.4 * screenHeight));
await tester.pumpAndSettle();
expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(1.0, precisionErrorTolerance,
));
await tester.drag(find.text('Item 1'), Offset(0, .7 * screenHeight));
await tester.pumpAndSettle();
expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(.25, precisionErrorTolerance),
);
}, variant: TargetPlatformVariant.all());
}
testWidgets('Min and max are implicitly added to snapSizes', (WidgetTester tester) async {
const Key stackKey = ValueKey<String>('stack');
const Key containerKey = ValueKey<String>('container');
await tester.pumpWidget(_boilerplate(null,
snap: true,
stackKey: stackKey,
containerKey: containerKey,
snapSizes: <double>[.5],
));
await tester.pumpAndSettle();
final double screenHeight = tester.getSize(find.byKey(stackKey)).height;
await tester.drag(find.text('Item 1'), Offset(0, -.4 * screenHeight));
await tester.pumpAndSettle();
expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(1.0, precisionErrorTolerance),
);
await tester.drag(find.text('Item 1'), Offset(0, .7 * screenHeight));
await tester.pumpAndSettle();
expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(.25, precisionErrorTolerance),
);
}, variant: TargetPlatformVariant.all());
testWidgets('Changes to widget parameters are propagated', (WidgetTester tester) async {
const Key stackKey = ValueKey<String>('stack');
const Key containerKey = ValueKey<String>('container');
await tester.pumpWidget(_boilerplate(
null,
stackKey: stackKey,
containerKey: containerKey,
));
await tester.pumpAndSettle();
final double screenHeight = tester.getSize(find.byKey(stackKey)).height;
expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(.5, precisionErrorTolerance),
);
// Pump the same widget but with a new initial child size.
await tester.pumpWidget(_boilerplate(
null,
stackKey: stackKey,
containerKey: containerKey,
initialChildSize: .6,
));
await tester.pumpAndSettle();
// We jump to the new initial size because the sheet hasn't changed yet.
expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(.6, precisionErrorTolerance),
);
// Pump the same widget but with a new max child size.
await tester.pumpWidget(_boilerplate(
null,
stackKey: stackKey,
containerKey: containerKey,
initialChildSize: .6,
maxChildSize: .9
));
await tester.pumpAndSettle();
expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(.6, precisionErrorTolerance),
);
await tester.drag(find.text('Item 1'), Offset(0, -.6 * screenHeight));
await tester.pumpAndSettle();
expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(.9, precisionErrorTolerance),
);
// Pump the same widget but with a new max child size and initial size.
await tester.pumpWidget(_boilerplate(
null,
stackKey: stackKey,
containerKey: containerKey,
maxChildSize: .8,
initialChildSize: .7,
));
await tester.pumpAndSettle();
// The max child size has been reduced, we should be rebuilt at the new
// max of .8. We changed the initial size again, but the sheet has already
// been changed so the new initial is ignored.
expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(.8, precisionErrorTolerance),
);
await tester.drag(find.text('Item 1'), Offset(0, .2 * screenHeight));
// Pump the same widget but with snapping enabled.
await tester.pumpWidget(_boilerplate(
null,
snap: true,
stackKey: stackKey,
containerKey: containerKey,
maxChildSize: .8,
snapSizes: <double>[.5],
));
await tester.pumpAndSettle();
// Sheet snaps immediately on a change to snap.
expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(.5, precisionErrorTolerance),
);
final List<double> snapSizes = <double>[.6];
// Change the snap sizes.
await tester.pumpWidget(_boilerplate(
null,
snap: true,
stackKey: stackKey,
containerKey: containerKey,
maxChildSize: .8,
snapSizes: snapSizes,
));
await tester.pumpAndSettle();
expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(.6, precisionErrorTolerance),
);
}, variant: TargetPlatformVariant.all());
testWidgets('Fling snaps in direction of momentum', (WidgetTester tester) async {
const Key stackKey = ValueKey<String>('stack');
const Key containerKey = ValueKey<String>('container');
await tester.pumpWidget(_boilerplate(null,
snap: true,
stackKey: stackKey,
containerKey: containerKey,
snapSizes: <double>[.5, .75],
));
await tester.pumpAndSettle();
final double screenHeight = tester.getSize(find.byKey(stackKey)).height;
await tester.fling(find.text('Item 1'), Offset(0, -.1 * screenHeight), 1000);
await tester.pumpAndSettle();
expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(.75, precisionErrorTolerance),
);
await tester.fling(find.text('Item 1'), Offset(0, .3 * screenHeight), 1000);
await tester.pumpAndSettle();
expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(.25, precisionErrorTolerance),
);
}, variant: TargetPlatformVariant.all());
testWidgets("Changing parameters with an un-listened controller doesn't throw", (WidgetTester tester) async {
await tester.pumpWidget(_boilerplate(
null,
snap: true,
// Will prevent the sheet's child from listening to the controller.
ignoreController: true,
));
await tester.pumpAndSettle();
await tester.pumpWidget(_boilerplate(
null,
snap: true,
));
await tester.pumpAndSettle();
}, variant: TargetPlatformVariant.all());
testWidgets('ScrollNotification correctly dispatched when flung without covering its container', (WidgetTester tester) async {
final List<Type> notificationTypes = <Type>[];
await tester.pumpWidget(_boilerplate(
null,
onScrollNotification: (ScrollNotification notification) {
notificationTypes.add(notification.runtimeType);
return false;
},
));
await tester.fling(find.text('Item 1'), const Offset(0, -200), 200);
await tester.pumpAndSettle();
// TODO(itome): Make sure UserScrollNotification and ScrollUpdateNotification are called correctly.
final List<Type> types = <Type>[
ScrollStartNotification,
ScrollEndNotification,
];
expect(notificationTypes, equals(types));
});
testWidgets('ScrollNotification correctly dispatched when flung with contents scroll', (WidgetTester tester) async {
final List<Type> notificationTypes = <Type>[];
await tester.pumpWidget(_boilerplate(
null,
onScrollNotification: (ScrollNotification notification) {
notificationTypes.add(notification.runtimeType);
return false;
},
));
await tester.flingFrom(const Offset(0, 325), const Offset(0, -325), 200);
await tester.pumpAndSettle();
final List<Type> types = <Type>[
ScrollStartNotification,
UserScrollNotification,
...List<Type>.filled(5, ScrollUpdateNotification),
ScrollEndNotification,
UserScrollNotification,
];
expect(notificationTypes, types);
});
testWidgets('Do not crash when remove the tree during animation.', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/89214
await tester.pumpWidget(_boilerplate(
null,
onScrollNotification: (ScrollNotification notification) {
return false;
},
));
await tester.flingFrom(const Offset(0, 325), const Offset(0, 325), 200);
// The animation is running.
await tester.pumpWidget(const SizedBox.shrink());
expect(tester.takeException(), isNull);
});
for (final bool shouldAnimate in <bool>[true, false]) {
testWidgets('Can ${shouldAnimate ? 'animate' : 'jump'} to arbitrary positions', (WidgetTester tester) async {
const Key stackKey = ValueKey<String>('stack');
const Key containerKey = ValueKey<String>('container');
final DraggableScrollableController controller = DraggableScrollableController();
await tester.pumpWidget(_boilerplate(
null,
controller: controller,
stackKey: stackKey,
containerKey: containerKey,
));
await tester.pumpAndSettle();
final double screenHeight = tester.getSize(find.byKey(stackKey)).height;
// Use a local helper to animate so we can share code across a jumpTo test
// and an animateTo test.
void goTo(double size) => shouldAnimate
? controller.animateTo(size, duration: const Duration(milliseconds: 200), curve: Curves.linear)
: controller.jumpTo(size);
// If we're animating, pump will call four times, two of which are for the
// animation duration.
final int expectedPumpCount = shouldAnimate ? 4 : 2;
goTo(.6);
expect(await tester.pumpAndSettle(), expectedPumpCount);
expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(.6, precisionErrorTolerance),
);
expect(find.text('Item 1'), findsOneWidget);
expect(find.text('Item 20'), findsOneWidget);
expect(find.text('Item 70'), findsNothing);
goTo(.4);
expect(await tester.pumpAndSettle(), expectedPumpCount);
expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(.4, precisionErrorTolerance),
);
expect(find.text('Item 1'), findsOneWidget);
expect(find.text('Item 20'), findsNothing);
expect(find.text('Item 70'), findsNothing);
await tester.fling(find.text('Item 1'), Offset(0, -screenHeight), 100);
await tester.pumpAndSettle();
expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(1, precisionErrorTolerance),
);
expect(find.text('Item 1'), findsNothing);
expect(find.text('Item 20'), findsOneWidget);
expect(find.text('Item 70'), findsNothing);
// Programmatic control does not affect the inner scrollable's position.
goTo(.8);
expect(await tester.pumpAndSettle(), expectedPumpCount);
expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(.8, precisionErrorTolerance),
);
expect(find.text('Item 1'), findsNothing);
expect(find.text('Item 20'), findsOneWidget);
expect(find.text('Item 70'), findsNothing);
// Attempting to move to a size too big or too small instead moves to the
// min or max child size.
goTo(.5);
await tester.pumpAndSettle();
goTo(0);
// The animation was cut short by half, there should have been on less pumps
final int truncatedPumpCount = shouldAnimate ? expectedPumpCount - 1 : expectedPumpCount;
expect(await tester.pumpAndSettle(), truncatedPumpCount);
expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(.25, precisionErrorTolerance),
);
});
}
testWidgets('Can reuse a controller after the old controller is disposed', (WidgetTester tester) async {
const Key stackKey = ValueKey<String>('stack');
const Key containerKey = ValueKey<String>('container');
final DraggableScrollableController controller = DraggableScrollableController();
await tester.pumpWidget(_boilerplate(
null,
controller: controller,
stackKey: stackKey,
containerKey: containerKey,
));
await tester.pumpAndSettle();
// Pump a new sheet with the same controller. This will dispose of the old sheet first.
await tester.pumpWidget(_boilerplate(
null,
controller: controller,
stackKey: stackKey,
containerKey: containerKey,
));
await tester.pumpAndSettle();
final double screenHeight = tester.getSize(find.byKey(stackKey)).height;
controller.jumpTo(.6);
await tester.pumpAndSettle();
expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(.6, precisionErrorTolerance),
);
});
testWidgets('animateTo interrupts other animations', (WidgetTester tester) async {
const Key stackKey = ValueKey<String>('stack');
const Key containerKey = ValueKey<String>('container');
final DraggableScrollableController controller = DraggableScrollableController();
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: _boilerplate(
null,
controller: controller,
stackKey: stackKey,
containerKey: containerKey,
),
));
await tester.pumpAndSettle();
final double screenHeight = tester.getSize(find.byKey(stackKey)).height;
await tester.flingFrom(Offset(0, .5*screenHeight), Offset(0, -.5*screenHeight), 2000);
// Wait until `flinFrom` finished dragging, but before the scrollable goes ballistic.
await tester.pump(const Duration(seconds: 1));
expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(1, precisionErrorTolerance),
);
expect(find.text('Item 1'), findsOneWidget);
controller.animateTo(.9, duration: const Duration(milliseconds: 200), curve: Curves.linear);
await tester.pumpAndSettle();
expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(.9, precisionErrorTolerance),
);
// The ballistic animation should have been canceled so item 1 should still be visible.
expect(find.text('Item 1'), findsOneWidget);
});
testWidgets('Other animations interrupt animateTo', (WidgetTester tester) async {
const Key stackKey = ValueKey<String>('stack');
const Key containerKey = ValueKey<String>('container');
final DraggableScrollableController controller = DraggableScrollableController();
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: _boilerplate(
null,
controller: controller,
stackKey: stackKey,
containerKey: containerKey,
),
));
await tester.pumpAndSettle();
final double screenHeight = tester.getSize(find.byKey(stackKey)).height;
controller.animateTo(1, duration: const Duration(milliseconds: 200), curve: Curves.linear);
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(.75, precisionErrorTolerance),
);
// Interrupt animation and drag downward.
await tester.drag(find.text('Item 1'), Offset(0, .1 * screenHeight));
await tester.pumpAndSettle();
expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(.65, precisionErrorTolerance),
);
});
testWidgets('animateTo can be interrupted by other animateTo or jumpTo', (WidgetTester tester) async {
const Key stackKey = ValueKey<String>('stack');
const Key containerKey = ValueKey<String>('container');
final DraggableScrollableController controller = DraggableScrollableController();
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: _boilerplate(
null,
controller: controller,
stackKey: stackKey,
containerKey: containerKey,
),
));
await tester.pumpAndSettle();
final double screenHeight = tester.getSize(find.byKey(stackKey)).height;
controller.animateTo(1, duration: const Duration(milliseconds: 200), curve: Curves.linear);
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(.75, precisionErrorTolerance),
);
// Interrupt animation with a new `animateTo`.
controller.animateTo(.25, duration: const Duration(milliseconds: 200), curve: Curves.linear);
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(.5, precisionErrorTolerance),
);
// Interrupt animation with a jump.
controller.jumpTo(.6);
await tester.pumpAndSettle();
expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(.6, precisionErrorTolerance),
);
});
testWidgets('Can get size and pixels', (WidgetTester tester) async {
const Key stackKey = ValueKey<String>('stack');
const Key containerKey = ValueKey<String>('container');
final DraggableScrollableController controller = DraggableScrollableController();
await tester.pumpWidget(_boilerplate(
null,
controller: controller,
stackKey: stackKey,
containerKey: containerKey,
));
await tester.pumpAndSettle();
final double screenHeight = tester.getSize(find.byKey(stackKey)).height;
expect(controller.sizeToPixels(.25), .25*screenHeight);
expect(controller.pixelsToSize(.25*screenHeight), .25);
controller.animateTo(.6, duration: const Duration(milliseconds: 200), curve: Curves.linear);
await tester.pumpAndSettle();
expect(
tester.getSize(find.byKey(containerKey)).height / screenHeight,
closeTo(.6, precisionErrorTolerance),
);
expect(controller.size, closeTo(.6, precisionErrorTolerance));
expect(controller.pixels, closeTo(.6*screenHeight, precisionErrorTolerance));
await tester.drag(find.text('Item 5'), Offset(0, .2*screenHeight));
expect(controller.size, closeTo(.4, precisionErrorTolerance));
expect(controller.pixels, closeTo(.4*screenHeight, precisionErrorTolerance));
});
testWidgets('Cannot attach a controller to multiple sheets', (WidgetTester tester) async {
final DraggableScrollableController controller = DraggableScrollableController();
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
_boilerplate(
null,
controller: controller,
),
_boilerplate(
null,
controller: controller,
)
],
),
), null, EnginePhase.build);
expect(tester.takeException(), isAssertionError);
});
testWidgets('Invalid controller interactions throw assertion errors', (WidgetTester tester) async {
final DraggableScrollableController controller = DraggableScrollableController();
// Can't use a controller before attaching it.
expect(() => controller.jumpTo(.1), throwsAssertionError);
expect(() => controller.pixels, throwsAssertionError);
expect(() => controller.size, throwsAssertionError);
expect(() => controller.pixelsToSize(0), throwsAssertionError);
expect(() => controller.sizeToPixels(0), throwsAssertionError);
await tester.pumpWidget(_boilerplate(
null,
controller: controller,
));
// Can't jump or animate to invalid sizes.
expect(() => controller.jumpTo(-1), throwsAssertionError);
expect(() => controller.jumpTo(1.1), throwsAssertionError);
expect(
() => controller.animateTo(-1, duration: const Duration(milliseconds: 1), curve: Curves.linear),
throwsAssertionError,
);
expect(
() => controller.animateTo(1.1, duration: const Duration(milliseconds: 1), curve: Curves.linear),
throwsAssertionError,
);
// Can't use animateTo with a zero duration.
expect(() => controller.animateTo(.5, duration: Duration.zero, curve: Curves.linear), throwsAssertionError);
});
}