blob: 9b1a33bc9a9d29f5712ec80fd3fa2ca97177a158 [file] [log] [blame] [edit]
// 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/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('toString control test', (WidgetTester tester) async {
const Widget widget = FadeTransition(
opacity: kAlwaysCompleteAnimation,
child: Text('Ready', textDirection: TextDirection.ltr),
);
expect(widget.toString, isNot(throwsException));
});
group('DecoratedBoxTransition test', () {
final DecorationTween decorationTween = DecorationTween(
begin: BoxDecoration(
color: const Color(0xFFFFFFFF),
border: Border.all(
width: 4.0,
),
borderRadius: BorderRadius.zero,
boxShadow: const <BoxShadow>[
BoxShadow(
color: Color(0x66000000),
blurRadius: 10.0,
spreadRadius: 4.0,
),
],
),
end: BoxDecoration(
color: const Color(0xFF000000),
border: Border.all(
color: const Color(0xFF202020),
),
borderRadius: const BorderRadius.all(Radius.circular(10.0)),
// No shadow.
),
);
late AnimationController controller;
setUp(() {
controller = AnimationController(vsync: const TestVSync());
});
testWidgets('decoration test', (WidgetTester tester) async {
final DecoratedBoxTransition transitionUnderTest =
DecoratedBoxTransition(
decoration: decorationTween.animate(controller),
child: const Text(
"Doesn't matter",
textDirection: TextDirection.ltr,
),
);
await tester.pumpWidget(transitionUnderTest);
RenderDecoratedBox actualBox = tester.renderObject(find.byType(DecoratedBox));
BoxDecoration actualDecoration = actualBox.decoration as BoxDecoration;
expect(actualDecoration.color, const Color(0xFFFFFFFF));
expect(actualDecoration.boxShadow![0].blurRadius, 10.0);
expect(actualDecoration.boxShadow![0].spreadRadius, 4.0);
expect(actualDecoration.boxShadow![0].color, const Color(0x66000000));
controller.value = 0.5;
await tester.pump();
actualBox = tester.renderObject(find.byType(DecoratedBox));
actualDecoration = actualBox.decoration as BoxDecoration;
expect(actualDecoration.color, const Color(0xFF7F7F7F));
expect(actualDecoration.border, isA<Border>());
final Border border = actualDecoration.border! as Border;
expect(border.left.width, 2.5);
expect(border.left.style, BorderStyle.solid);
expect(border.left.color, const Color(0xFF101010));
expect(actualDecoration.borderRadius, const BorderRadius.all(Radius.circular(5.0)));
expect(actualDecoration.shape, BoxShape.rectangle);
expect(actualDecoration.boxShadow![0].blurRadius, 5.0);
expect(actualDecoration.boxShadow![0].spreadRadius, 2.0);
// Scaling a shadow doesn't change the color.
expect(actualDecoration.boxShadow![0].color, const Color(0x66000000));
controller.value = 1.0;
await tester.pump();
actualBox = tester.renderObject(find.byType(DecoratedBox));
actualDecoration = actualBox.decoration as BoxDecoration;
expect(actualDecoration.color, const Color(0xFF000000));
expect(actualDecoration.boxShadow, null);
});
testWidgets('animations work with curves test', (WidgetTester tester) async {
final Animation<Decoration> curvedDecorationAnimation =
decorationTween.animate(CurvedAnimation(
parent: controller,
curve: Curves.easeOut,
));
final DecoratedBoxTransition transitionUnderTest = DecoratedBoxTransition(
decoration: curvedDecorationAnimation,
position: DecorationPosition.foreground,
child: const Text(
"Doesn't matter",
textDirection: TextDirection.ltr,
),
);
await tester.pumpWidget(transitionUnderTest);
RenderDecoratedBox actualBox = tester.renderObject(find.byType(DecoratedBox));
BoxDecoration actualDecoration = actualBox.decoration as BoxDecoration;
expect(actualDecoration.color, const Color(0xFFFFFFFF));
expect(actualDecoration.boxShadow![0].blurRadius, 10.0);
expect(actualDecoration.boxShadow![0].spreadRadius, 4.0);
expect(actualDecoration.boxShadow![0].color, const Color(0x66000000));
controller.value = 0.5;
await tester.pump();
actualBox = tester.renderObject(find.byType(DecoratedBox));
actualDecoration = actualBox.decoration as BoxDecoration;
// Same as the test above but the values should be much closer to the
// tween's end values given the easeOut curve.
expect(actualDecoration.color, const Color(0xFF505050));
expect(actualDecoration.border, isA<Border>());
final Border border = actualDecoration.border! as Border;
expect(border.left.width, moreOrLessEquals(1.9, epsilon: 0.1));
expect(border.left.style, BorderStyle.solid);
expect(border.left.color, const Color(0xFF151515));
expect(actualDecoration.borderRadius!.resolve(TextDirection.ltr).topLeft.x, moreOrLessEquals(6.8, epsilon: 0.1));
expect(actualDecoration.shape, BoxShape.rectangle);
expect(actualDecoration.boxShadow![0].blurRadius, moreOrLessEquals(3.1, epsilon: 0.1));
expect(actualDecoration.boxShadow![0].spreadRadius, moreOrLessEquals(1.2, epsilon: 0.1));
// Scaling a shadow doesn't change the color.
expect(actualDecoration.boxShadow![0].color, const Color(0x66000000));
});
});
testWidgets('AlignTransition animates', (WidgetTester tester) async {
final AnimationController controller = AnimationController(vsync: const TestVSync());
final Animation<Alignment> alignmentTween = AlignmentTween(
begin: Alignment.centerLeft,
end: Alignment.bottomRight,
).animate(controller);
final Widget widget = AlignTransition(
alignment: alignmentTween,
child: const Text('Ready', textDirection: TextDirection.ltr),
);
await tester.pumpWidget(widget);
final RenderPositionedBox actualPositionedBox = tester.renderObject(find.byType(Align));
Alignment actualAlignment = actualPositionedBox.alignment as Alignment;
expect(actualAlignment, Alignment.centerLeft);
controller.value = 0.5;
await tester.pump();
actualAlignment = actualPositionedBox.alignment as Alignment;
expect(actualAlignment, const Alignment(0.0, 0.5));
});
testWidgets('RelativePositionedTransition animates', (WidgetTester tester) async {
final AnimationController controller = AnimationController(vsync: const TestVSync());
final Animation<Rect?> rectTween = RectTween(
begin: const Rect.fromLTWH(0, 0, 30, 40),
end: const Rect.fromLTWH(100, 200, 100, 200),
).animate(controller);
final Widget widget = Directionality(
textDirection: TextDirection.rtl,
child: Stack(
alignment: Alignment.centerLeft,
children: <Widget>[
RelativePositionedTransition(
size: const Size(200, 300),
rect: rectTween,
child: const Placeholder(),
),
],
),
);
await tester.pumpWidget(widget);
final Positioned actualPositioned = tester.widget(find.byType(Positioned));
final RenderBox renderBox = tester.renderObject(find.byType(Placeholder));
Rect actualRect = Rect.fromLTRB(
actualPositioned.left!,
actualPositioned.top!,
actualPositioned.right ?? 0.0,
actualPositioned.bottom ?? 0.0,
);
expect(actualRect, equals(const Rect.fromLTRB(0, 0, 170, 260)));
expect(renderBox.size, equals(const Size(630, 340)));
controller.value = 0.5;
await tester.pump();
actualRect = Rect.fromLTRB(
actualPositioned.left!,
actualPositioned.top!,
actualPositioned.right ?? 0.0,
actualPositioned.bottom ?? 0.0,
);
expect(actualRect, equals(const Rect.fromLTWH(0, 0, 170, 260)));
expect(renderBox.size, equals(const Size(665, 420)));
});
testWidgets('AlignTransition keeps width and height factors', (WidgetTester tester) async {
final AnimationController controller = AnimationController(vsync: const TestVSync());
final Animation<Alignment> alignmentTween = AlignmentTween(
begin: Alignment.centerLeft,
end: Alignment.bottomRight,
).animate(controller);
final Widget widget = AlignTransition(
alignment: alignmentTween,
widthFactor: 0.3,
heightFactor: 0.4,
child: const Text('Ready', textDirection: TextDirection.ltr),
);
await tester.pumpWidget(widget);
final Align actualAlign = tester.widget(find.byType(Align));
expect(actualAlign.widthFactor, 0.3);
expect(actualAlign.heightFactor, 0.4);
});
testWidgets('SizeTransition clamps negative size factors - vertical axis', (WidgetTester tester) async {
final AnimationController controller = AnimationController(vsync: const TestVSync());
final Animation<double> animation = Tween<double>(begin: -1.0, end: 1.0).animate(controller);
final Widget widget = Directionality(
textDirection: TextDirection.ltr,
child: SizeTransition(
sizeFactor: animation,
child: const Text('Ready'),
),
);
await tester.pumpWidget(widget);
final RenderPositionedBox actualPositionedBox = tester.renderObject(find.byType(Align));
expect(actualPositionedBox.heightFactor, 0.0);
controller.value = 0.0;
await tester.pump();
expect(actualPositionedBox.heightFactor, 0.0);
controller.value = 0.75;
await tester.pump();
expect(actualPositionedBox.heightFactor, 0.5);
controller.value = 1.0;
await tester.pump();
expect(actualPositionedBox.heightFactor, 1.0);
});
testWidgets('SizeTransition clamps negative size factors - horizontal axis', (WidgetTester tester) async {
final AnimationController controller = AnimationController(vsync: const TestVSync());
final Animation<double> animation = Tween<double>(begin: -1.0, end: 1.0).animate(controller);
final Widget widget = Directionality(
textDirection: TextDirection.ltr,
child: SizeTransition(
axis: Axis.horizontal,
sizeFactor: animation,
child: const Text('Ready'),
),
);
await tester.pumpWidget(widget);
final RenderPositionedBox actualPositionedBox = tester.renderObject(find.byType(Align));
expect(actualPositionedBox.widthFactor, 0.0);
controller.value = 0.0;
await tester.pump();
expect(actualPositionedBox.widthFactor, 0.0);
controller.value = 0.75;
await tester.pump();
expect(actualPositionedBox.widthFactor, 0.5);
controller.value = 1.0;
await tester.pump();
expect(actualPositionedBox.widthFactor, 1.0);
});
testWidgets('RotationTransition animates', (WidgetTester tester) async {
final AnimationController controller = AnimationController(vsync: const TestVSync());
final Widget widget = RotationTransition(
alignment: Alignment.topRight,
turns: controller,
child: const Text(
'Rotation',
textDirection: TextDirection.ltr,
),
);
await tester.pumpWidget(widget);
Transform actualRotatedBox = tester.widget(find.byType(Transform));
Matrix4 actualTurns = actualRotatedBox.transform;
expect(actualTurns, equals(Matrix4.rotationZ(0.0)));
controller.value = 0.5;
await tester.pump();
actualRotatedBox = tester.widget(find.byType(Transform));
actualTurns = actualRotatedBox.transform;
expect(actualTurns, Matrix4.fromList(<double>[
-1.0, 0.0, 0.0, 0.0,
0.0, -1.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0,
])..transpose());
controller.value = 0.75;
await tester.pump();
actualRotatedBox = tester.widget(find.byType(Transform));
actualTurns = actualRotatedBox.transform;
expect(actualTurns, Matrix4.fromList(<double>[
0.0, 1.0, 0.0, 0.0,
-1.0, 0.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0,
])..transpose());
});
testWidgets('RotationTransition maintains chosen alignment during animation', (WidgetTester tester) async {
final AnimationController controller = AnimationController(vsync: const TestVSync());
final Widget widget = RotationTransition(
alignment: Alignment.topRight,
turns: controller,
child: const Text('Rotation', textDirection: TextDirection.ltr),
);
await tester.pumpWidget(widget);
RotationTransition actualRotatedBox = tester.widget(find.byType(RotationTransition));
Alignment actualAlignment = actualRotatedBox.alignment;
expect(actualAlignment, Alignment.topRight);
controller.value = 0.5;
await tester.pump();
actualRotatedBox = tester.widget(find.byType(RotationTransition));
actualAlignment = actualRotatedBox.alignment;
expect(actualAlignment, Alignment.topRight);
});
group('FadeTransition', () {
double getOpacity(WidgetTester tester, String textValue) {
final FadeTransition opacityWidget = tester.widget<FadeTransition>(
find.ancestor(
of: find.text(textValue),
matching: find.byType(FadeTransition),
).first,
);
return opacityWidget.opacity.value;
}
testWidgets('animates', (WidgetTester tester) async {
final AnimationController controller = AnimationController(vsync: const TestVSync());
final Animation<double> animation = Tween<double>(begin: 0.0, end: 1.0).animate(controller);
final Widget widget = Directionality(
textDirection: TextDirection.ltr,
child: FadeTransition(
opacity: animation,
child: const Text('Fade In'),
),
);
await tester.pumpWidget(widget);
expect(getOpacity(tester, 'Fade In'), 0.0);
controller.value = 0.25;
await tester.pump();
expect(getOpacity(tester, 'Fade In'), 0.25);
controller.value = 0.5;
await tester.pump();
expect(getOpacity(tester, 'Fade In'), 0.5);
controller.value = 0.75;
await tester.pump();
expect(getOpacity(tester, 'Fade In'), 0.75);
controller.value = 1.0;
await tester.pump();
expect(getOpacity(tester, 'Fade In'), 1.0);
});
});
group('SliverFadeTransition', () {
double getOpacity(WidgetTester tester, String textValue) {
final SliverFadeTransition opacityWidget = tester.widget<SliverFadeTransition>(
find.ancestor(
of: find.text(textValue),
matching: find.byType(SliverFadeTransition),
).first,
);
return opacityWidget.opacity.value;
}
testWidgets('animates', (WidgetTester tester) async {
final AnimationController controller = AnimationController(vsync: const TestVSync());
final Animation<double> animation = Tween<double>(begin: 0.0, end: 1.0).animate(controller);
final Widget widget = Localizations(
locale: const Locale('en', 'us'),
delegates: const <LocalizationsDelegate<dynamic>>[
DefaultWidgetsLocalizations.delegate,
DefaultMaterialLocalizations.delegate,
],
child: Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: CustomScrollView(
slivers: <Widget>[
SliverFadeTransition(
opacity: animation,
sliver: const SliverToBoxAdapter(
child: Text('Fade In'),
),
),
],
),
),
),
);
await tester.pumpWidget(widget);
expect(getOpacity(tester, 'Fade In'), 0.0);
controller.value = 0.25;
await tester.pump();
expect(getOpacity(tester, 'Fade In'), 0.25);
controller.value = 0.5;
await tester.pump();
expect(getOpacity(tester, 'Fade In'), 0.5);
controller.value = 0.75;
await tester.pump();
expect(getOpacity(tester, 'Fade In'), 0.75);
controller.value = 1.0;
await tester.pump();
expect(getOpacity(tester, 'Fade In'), 1.0);
});
});
group('ScaleTransition', () {
testWidgets('uses ImageFilter when provided with FilterQuality argument', (WidgetTester tester) async {
final AnimationController controller = AnimationController(vsync: const TestVSync());
final Animation<double> animation = Tween<double>(begin: 0.0, end: 1.0).animate(controller);
final Widget widget = Directionality(
textDirection: TextDirection.ltr,
child: ScaleTransition(
scale: animation,
filterQuality: FilterQuality.none,
child: const Text('Scale Transition'),
),
);
await tester.pumpWidget(widget);
// Validate that expensive layer is not left in tree before animation has started.
expect(tester.layers, isNot(contains(isA<ImageFilterLayer>())));
controller.value = 0.25;
await tester.pump();
expect(tester.layers, contains(isA<ImageFilterLayer>().having(
(ImageFilterLayer layer) => layer.imageFilter.toString(),
'image filter',
startsWith('ImageFilter.matrix('),
)));
controller.value = 0.5;
await tester.pump();
expect(tester.layers, contains(isA<ImageFilterLayer>().having(
(ImageFilterLayer layer) => layer.imageFilter.toString(),
'image filter',
startsWith('ImageFilter.matrix('),
)));
controller.value = 0.75;
await tester.pump();
expect(tester.layers, contains(isA<ImageFilterLayer>().having(
(ImageFilterLayer layer) => layer.imageFilter.toString(),
'image filter',
startsWith('ImageFilter.matrix('),
)));
controller.value = 1;
await tester.pump();
// Validate that expensive layer is not left in tree after animation has finished.
expect(tester.layers, isNot(contains(isA<ImageFilterLayer>())));
});
});
group('RotationTransition', () {
testWidgets('uses ImageFilter when provided with FilterQuality argument', (WidgetTester tester) async {
final AnimationController controller = AnimationController(vsync: const TestVSync());
final Animation<double> animation = Tween<double>(begin: 0.0, end: 1.0).animate(controller);
final Widget widget = Directionality(
textDirection: TextDirection.ltr,
child: RotationTransition(
turns: animation,
filterQuality: FilterQuality.none,
child: const Text('Scale Transition'),
),
);
await tester.pumpWidget(widget);
// Validate that expensive layer is not left in tree before animation has started.
expect(tester.layers, isNot(contains(isA<ImageFilterLayer>())));
controller.value = 0.25;
await tester.pump();
expect(tester.layers, contains(isA<ImageFilterLayer>().having(
(ImageFilterLayer layer) => layer.imageFilter.toString(),
'image filter',
startsWith('ImageFilter.matrix('),
)));
controller.value = 0.5;
await tester.pump();
expect(tester.layers, contains(isA<ImageFilterLayer>().having(
(ImageFilterLayer layer) => layer.imageFilter.toString(),
'image filter',
startsWith('ImageFilter.matrix('),
)));
controller.value = 0.75;
await tester.pump();
expect(tester.layers, contains(isA<ImageFilterLayer>().having(
(ImageFilterLayer layer) => layer.imageFilter.toString(),
'image filter',
startsWith('ImageFilter.matrix('),
)));
controller.value = 1;
await tester.pump();
// Validate that expensive layer is not left in tree after animation has finished.
expect(tester.layers, isNot(contains(isA<ImageFilterLayer>())));
});
});
group('Builders', () {
testWidgets('AnimatedBuilder rebuilds when changed', (WidgetTester tester) async {
final GlobalKey<RedrawCounterState> redrawKey = GlobalKey<RedrawCounterState>();
final ChangeNotifier notifier = ChangeNotifier();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: AnimatedBuilder(
animation: notifier,
builder: (BuildContext context, Widget? child) {
return RedrawCounter(key: redrawKey, child: child);
},
),
),
);
expect(redrawKey.currentState!.redraws, equals(1));
await tester.pump();
expect(redrawKey.currentState!.redraws, equals(1));
notifier.notifyListeners();
await tester.pump();
expect(redrawKey.currentState!.redraws, equals(2));
// Pump a few more times to make sure that we don't rebuild unnecessarily.
await tester.pump();
await tester.pump();
expect(redrawKey.currentState!.redraws, equals(2));
});
testWidgets("AnimatedBuilder doesn't rebuild the child", (WidgetTester tester) async {
final GlobalKey<RedrawCounterState> redrawKey = GlobalKey<RedrawCounterState>();
final GlobalKey<RedrawCounterState> redrawKeyChild = GlobalKey<RedrawCounterState>();
final ChangeNotifier notifier = ChangeNotifier();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: AnimatedBuilder(
animation: notifier,
builder: (BuildContext context, Widget? child) {
return RedrawCounter(key: redrawKey, child: child);
},
child: RedrawCounter(key: redrawKeyChild),
),
),
);
expect(redrawKey.currentState!.redraws, equals(1));
expect(redrawKeyChild.currentState!.redraws, equals(1));
await tester.pump();
expect(redrawKey.currentState!.redraws, equals(1));
expect(redrawKeyChild.currentState!.redraws, equals(1));
notifier.notifyListeners();
await tester.pump();
expect(redrawKey.currentState!.redraws, equals(2));
expect(redrawKeyChild.currentState!.redraws, equals(1));
// Pump a few more times to make sure that we don't rebuild unnecessarily.
await tester.pump();
await tester.pump();
expect(redrawKey.currentState!.redraws, equals(2));
expect(redrawKeyChild.currentState!.redraws, equals(1));
});
testWidgets('ListenableBuilder rebuilds when changed', (WidgetTester tester) async {
final GlobalKey<RedrawCounterState> redrawKey = GlobalKey<RedrawCounterState>();
final ChangeNotifier notifier = ChangeNotifier();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ListenableBuilder(
listenable: notifier,
builder: (BuildContext context, Widget? child) {
return RedrawCounter(key: redrawKey, child: child);
},
),
),
);
expect(redrawKey.currentState!.redraws, equals(1));
await tester.pump();
expect(redrawKey.currentState!.redraws, equals(1));
notifier.notifyListeners();
await tester.pump();
expect(redrawKey.currentState!.redraws, equals(2));
// Pump a few more times to make sure that we don't rebuild unnecessarily.
await tester.pump();
await tester.pump();
expect(redrawKey.currentState!.redraws, equals(2));
});
testWidgets("ListenableBuilder doesn't rebuild the child", (WidgetTester tester) async {
final GlobalKey<RedrawCounterState> redrawKey = GlobalKey<RedrawCounterState>();
final GlobalKey<RedrawCounterState> redrawKeyChild = GlobalKey<RedrawCounterState>();
final ChangeNotifier notifier = ChangeNotifier();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ListenableBuilder(
listenable: notifier,
builder: (BuildContext context, Widget? child) {
return RedrawCounter(key: redrawKey, child: child);
},
child: RedrawCounter(key: redrawKeyChild),
),
),
);
expect(redrawKey.currentState!.redraws, equals(1));
expect(redrawKeyChild.currentState!.redraws, equals(1));
await tester.pump();
expect(redrawKey.currentState!.redraws, equals(1));
expect(redrawKeyChild.currentState!.redraws, equals(1));
notifier.notifyListeners();
await tester.pump();
expect(redrawKey.currentState!.redraws, equals(2));
expect(redrawKeyChild.currentState!.redraws, equals(1));
// Pump a few more times to make sure that we don't rebuild unnecessarily.
await tester.pump();
await tester.pump();
expect(redrawKey.currentState!.redraws, equals(2));
expect(redrawKeyChild.currentState!.redraws, equals(1));
});
});
}
class RedrawCounter extends StatefulWidget {
const RedrawCounter({ super.key, this.child });
final Widget? child;
@override
State<RedrawCounter> createState() => RedrawCounterState();
}
class RedrawCounterState extends State<RedrawCounter> {
int redraws = 0;
@override
Widget build(BuildContext context) {
redraws += 1;
return SizedBox(child: widget.child);
}
}