blob: 9777747c1ca0212421bead72200491641af01ba1 [file] [log] [blame]
// Copyright 2018 The Chromium 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:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('Basic floating action button locations', () {
testWidgets('still animates motion when the floating action button is null', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame(fab: null, location: null));
expect(find.byType(FloatingActionButton), findsNothing);
expect(tester.binding.transientCallbackCount, 0);
await tester.pumpWidget(buildFrame(fab: null, location: FloatingActionButtonLocation.endFloat));
expect(find.byType(FloatingActionButton), findsNothing);
expect(tester.binding.transientCallbackCount, greaterThan(0));
await tester.pumpWidget(buildFrame(fab: null, location: FloatingActionButtonLocation.centerFloat));
expect(find.byType(FloatingActionButton), findsNothing);
expect(tester.binding.transientCallbackCount, greaterThan(0));
});
testWidgets('moves fab from center to end and back', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame(location: FloatingActionButtonLocation.endFloat));
expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(756.0, 356.0));
expect(tester.binding.transientCallbackCount, 0);
await tester.pumpWidget(buildFrame(location: FloatingActionButtonLocation.centerFloat));
expect(tester.binding.transientCallbackCount, greaterThan(0));
await tester.pumpAndSettle();
expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(400.0, 356.0));
expect(tester.binding.transientCallbackCount, 0);
await tester.pumpWidget(buildFrame(location: FloatingActionButtonLocation.endFloat));
expect(tester.binding.transientCallbackCount, greaterThan(0));
await tester.pumpAndSettle();
expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(756.0, 356.0));
expect(tester.binding.transientCallbackCount, 0);
});
testWidgets('moves to and from custom-defined positions', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame(location: const _StartTopFloatingActionButtonLocation()));
expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(44.0, 56.0));
await tester.pumpWidget(buildFrame(location: FloatingActionButtonLocation.centerFloat));
expect(tester.binding.transientCallbackCount, greaterThan(0));
await tester.pumpAndSettle();
expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(400.0, 356.0));
expect(tester.binding.transientCallbackCount, 0);
await tester.pumpWidget(buildFrame(location: const _StartTopFloatingActionButtonLocation()));
expect(tester.binding.transientCallbackCount, greaterThan(0));
await tester.pumpAndSettle();
expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(44.0, 56.0));
expect(tester.binding.transientCallbackCount, 0);
});
group('interrupts in-progress animations without jumps', () {
_GeometryListener geometryListener;
ScaffoldGeometry geometry;
_GeometryListenerState listenerState;
Size previousRect;
Iterable<double> previousRotations;
// The maximum amounts we expect the fab width and height to change
// during one step of a transition.
const double maxDeltaWidth = 12.5;
const double maxDeltaHeight = 12.5;
// The maximum amounts we expect the fab icon to rotate during one step
// of a transition.
const double maxDeltaRotation = 0.09;
// We'll listen to the Scaffold's geometry for any 'jumps' to detect
// changes in the size and rotation of the fab.
void setupListener(WidgetTester tester) {
// Measure the delta in width and height of the fab, and check that it never grows
// by more than the expected maximum deltas.
void check() {
geometry = listenerState.cache.value;
final Size currentRect = geometry.floatingActionButtonArea?.size;
// Measure the delta in width and height of the rect, and check that
// it never grows by more than a safe amount.
if (previousRect != null && currentRect != null) {
final double deltaWidth = currentRect.width - previousRect.width;
final double deltaHeight = currentRect.height - previousRect.height;
expect(
deltaWidth.abs(),
lessThanOrEqualTo(maxDeltaWidth),
reason: "The Floating Action Button's width should not change "
'faster than $maxDeltaWidth per animation step.\n'
'Prevous rect: $previousRect, current rect: $currentRect',
);
expect(
deltaHeight.abs(),
lessThanOrEqualTo(maxDeltaHeight),
reason: "The Floating Action Button's width should not change "
'faster than $maxDeltaHeight per animation step.\n'
'Prevous rect: $previousRect, current rect: $currentRect',
);
}
previousRect = currentRect;
// Measure the delta in rotation.
// Check that it never grows by more than a safe amount.
//
// Note that there may be multiple transitions all active at
// the same time. We are concerned only with the closest one.
final Iterable<RotationTransition> rotationTransitions = tester.widgetList(
find.byType(RotationTransition),
);
final Iterable<double> currentRotations = rotationTransitions.map(
(RotationTransition t) => t.turns.value);
if (previousRotations != null && previousRotations.isNotEmpty
&& currentRotations != null && currentRotations.isNotEmpty
&& previousRect != null && currentRect != null) {
final List<double> deltas = <double>[];
for (double currentRotation in currentRotations) {
double minDelta;
for (double previousRotation in previousRotations) {
final double delta = (previousRotation - currentRotation).abs();
minDelta ??= delta;
minDelta = min(delta, minDelta);
}
deltas.add(minDelta);
}
if (deltas.where((double delta) => delta < maxDeltaRotation).isEmpty) {
fail("The Floating Action Button's rotation should not change "
'faster than $maxDeltaRotation per animation step.\n'
'Detected deltas were: $deltas\n'
'Previous values: $previousRotations, current values: $currentRotations\n'
'Prevous rect: $previousRect, current rect: $currentRect',);
}
}
previousRotations = currentRotations;
}
listenerState = tester.state(find.byType(_GeometryListener));
listenerState.geometryListenable.addListener(check);
}
setUp(() {
// We create the geometry listener here, but it can only be set up
// after it is pumped into the widget tree and a tester is
// available.
geometryListener = _GeometryListener();
geometry = null;
listenerState = null;
previousRect = null;
previousRotations = null;
});
testWidgets('moving the fab to centerFloat', (WidgetTester tester) async {
// Create a scaffold with the fab at endFloat
await tester.pumpWidget(buildFrame(location: FloatingActionButtonLocation.endFloat, listener: geometryListener));
setupListener(tester);
// Move the fab to centerFloat'
await tester.pumpWidget(buildFrame(location: FloatingActionButtonLocation.centerFloat, listener: geometryListener));
await tester.pumpAndSettle();
});
testWidgets('interrupting motion towards the StartTop location.', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame(location: FloatingActionButtonLocation.centerFloat, listener: geometryListener));
setupListener(tester);
// Move the fab to the top start after creating the fab.
await tester.pumpWidget(buildFrame(location: const _StartTopFloatingActionButtonLocation(), listener: geometryListener));
await tester.pump(kFloatingActionButtonSegue ~/ 2);
// Interrupt motion to move to the end float
await tester.pumpWidget(buildFrame(location: FloatingActionButtonLocation.endFloat, listener: geometryListener));
await tester.pumpAndSettle();
});
testWidgets('interrupting entrance to remove the fab.', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame(fab: null, location: FloatingActionButtonLocation.centerFloat, listener: geometryListener));
setupListener(tester);
// Animate the fab in.
await tester.pumpWidget(buildFrame(location: FloatingActionButtonLocation.endFloat, listener: geometryListener));
await tester.pump(kFloatingActionButtonSegue ~/ 2);
// Remove the fab.
await tester.pumpWidget(
buildFrame(
fab: null,
location: FloatingActionButtonLocation.endFloat,
listener: geometryListener,
),
);
await tester.pumpAndSettle();
});
testWidgets('interrupting entrance of a new fab.', (WidgetTester tester) async {
await tester.pumpWidget(
buildFrame(
fab: null,
location: FloatingActionButtonLocation.endFloat,
listener: geometryListener,
),
);
setupListener(tester);
// Bring in a new fab.
await tester.pumpWidget(buildFrame(location: FloatingActionButtonLocation.centerFloat, listener: geometryListener));
await tester.pump(kFloatingActionButtonSegue ~/ 2);
// Interrupt motion to move the fab.
await tester.pumpWidget(
buildFrame(
location: FloatingActionButtonLocation.endFloat,
listener: geometryListener,
),
);
await tester.pumpAndSettle();
});
});
});
testWidgets('Docked floating action button locations', (WidgetTester tester) async {
await tester.pumpWidget(
buildFrame(
location: FloatingActionButtonLocation.endDocked,
bab: const SizedBox(height: 100.0),
viewInsets: EdgeInsets.zero,
),
);
// Scaffold 800x600, FAB is 56x56, BAB is 800x100, FAB's center is
// at the top of the BAB.
expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(756.0, 500.0));
await tester.pumpWidget(
buildFrame(
location: FloatingActionButtonLocation.centerDocked,
bab: const SizedBox(height: 100.0),
viewInsets: EdgeInsets.zero,
),
);
await tester.pumpAndSettle();
expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(400.0, 500.0));
await tester.pumpWidget(
buildFrame(
location: FloatingActionButtonLocation.endDocked,
bab: const SizedBox(height: 100.0),
viewInsets: EdgeInsets.zero,
),
);
await tester.pumpAndSettle();
expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(756.0, 500.0));
});
testWidgets('Docked floating action button locations: no BAB, small BAB', (WidgetTester tester) async {
await tester.pumpWidget(
buildFrame(
location: FloatingActionButtonLocation.endDocked,
viewInsets: EdgeInsets.zero,
),
);
expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(756.0, 572.0));
await tester.pumpWidget(
buildFrame(
location: FloatingActionButtonLocation.endDocked,
bab: const SizedBox(height: 16.0),
viewInsets: EdgeInsets.zero,
),
);
expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(756.0, 572.0));
});
testWidgets('Mini-start-top floating action button location', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
appBar: AppBar(),
floatingActionButton: FloatingActionButton(onPressed: () { }, mini: true),
floatingActionButtonLocation: FloatingActionButtonLocation.miniStartTop,
body: Column(
children: const <Widget>[
ListTile(
leading: CircleAvatar(),
),
],
),
),
),
);
expect(tester.getCenter(find.byType(FloatingActionButton)).dx, tester.getCenter(find.byType(CircleAvatar)).dx);
expect(tester.getCenter(find.byType(FloatingActionButton)).dy, kToolbarHeight);
});
testWidgets('Start-top floating action button location LTR', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
appBar: AppBar(),
floatingActionButton: const FloatingActionButton(onPressed: null),
floatingActionButtonLocation: FloatingActionButtonLocation.startTop,
),
),
);
expect(tester.getRect(find.byType(FloatingActionButton)), Rect.fromLTWH(16.0, 28.0, 56.0, 56.0));
});
testWidgets('End-top floating action button location RTL', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl,
child: Scaffold(
appBar: AppBar(),
floatingActionButton: const FloatingActionButton(onPressed: null),
floatingActionButtonLocation: FloatingActionButtonLocation.endTop,
),
),
),
);
expect(tester.getRect(find.byType(FloatingActionButton)), Rect.fromLTWH(16.0, 28.0, 56.0, 56.0));
});
testWidgets('Start-top floating action button location RTL', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl,
child: Scaffold(
appBar: AppBar(),
floatingActionButton: const FloatingActionButton(onPressed: null),
floatingActionButtonLocation: FloatingActionButtonLocation.startTop,
),
),
),
);
expect(tester.getRect(find.byType(FloatingActionButton)), Rect.fromLTWH(800.0 - 56.0 - 16.0, 28.0, 56.0, 56.0));
});
testWidgets('End-top floating action button location LTR', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
appBar: AppBar(),
floatingActionButton: const FloatingActionButton(onPressed: null),
floatingActionButtonLocation: FloatingActionButtonLocation.endTop,
),
),
);
expect(tester.getRect(find.byType(FloatingActionButton)), Rect.fromLTWH(800.0 - 56.0 - 16.0, 28.0, 56.0, 56.0));
});
}
class _GeometryListener extends StatefulWidget {
@override
State createState() => _GeometryListenerState();
}
class _GeometryListenerState extends State<_GeometryListener> {
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: cache
);
}
int numNotifications = 0;
ValueListenable<ScaffoldGeometry> geometryListenable;
_GeometryCachePainter cache;
@override
void didChangeDependencies() {
super.didChangeDependencies();
final ValueListenable<ScaffoldGeometry> newListenable = Scaffold.geometryOf(context);
if (geometryListenable == newListenable)
return;
if (geometryListenable != null)
geometryListenable.removeListener(onGeometryChanged);
geometryListenable = newListenable;
geometryListenable.addListener(onGeometryChanged);
cache = _GeometryCachePainter(geometryListenable);
}
void onGeometryChanged() {
numNotifications += 1;
}
}
// The Scaffold.geometryOf() value is only available at paint time.
// To fetch it for the tests we implement this CustomPainter that just
// caches the ScaffoldGeometry value in its paint method.
class _GeometryCachePainter extends CustomPainter {
_GeometryCachePainter(this.geometryListenable) : super(repaint: geometryListenable);
final ValueListenable<ScaffoldGeometry> geometryListenable;
ScaffoldGeometry value;
@override
void paint(Canvas canvas, Size size) {
value = geometryListenable.value;
}
@override
bool shouldRepaint(_GeometryCachePainter oldDelegate) {
return true;
}
}
Widget buildFrame({
FloatingActionButton fab = const FloatingActionButton(
onPressed: null,
child: Text('1'),
),
FloatingActionButtonLocation location,
_GeometryListener listener,
TextDirection textDirection = TextDirection.ltr,
EdgeInsets viewInsets = const EdgeInsets.only(bottom: 200.0),
Widget bab,
}) {
return Localizations(
locale: const Locale('en', 'us'),
delegates: const <LocalizationsDelegate<dynamic>>[
DefaultWidgetsLocalizations.delegate,
DefaultMaterialLocalizations.delegate,
],
child: Directionality(
textDirection: textDirection,
child: MediaQuery(
data: MediaQueryData(viewInsets: viewInsets),
child: Scaffold(
appBar: AppBar(title: const Text('FabLocation Test')),
floatingActionButtonLocation: location,
floatingActionButton: fab,
bottomNavigationBar: bab,
body: listener,
),
),
));
}
class _StartTopFloatingActionButtonLocation extends FloatingActionButtonLocation {
const _StartTopFloatingActionButtonLocation();
@override
Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) {
double fabX;
assert(scaffoldGeometry.textDirection != null);
switch (scaffoldGeometry.textDirection) {
case TextDirection.rtl:
final double startPadding = kFloatingActionButtonMargin + scaffoldGeometry.minInsets.right;
fabX = scaffoldGeometry.scaffoldSize.width - scaffoldGeometry.floatingActionButtonSize.width - startPadding;
break;
case TextDirection.ltr:
final double startPadding = kFloatingActionButtonMargin + scaffoldGeometry.minInsets.left;
fabX = startPadding;
break;
}
final double fabY = scaffoldGeometry.contentTop - (scaffoldGeometry.floatingActionButtonSize.height / 2.0);
return Offset(fabX, fabY);
}
}