Summary
Flutter currently lacks a first-class, deterministic way to validate animation curves and timings in widget tests.
The recommended approach relies on pixel/position assertions at fixed timestamps, which is brittle, device-dependent, and tightly coupled to layout details.
This proposal introduces a testing-oriented abstraction to:
- Record animation progress over time (
Animation<double>).
- Normalize samples into a time/value trace.
- Assert curve conformance statistically instead of via fixed coordinates.
- Enforce proper lifecycle cleanup (
dispose) to avoid test leaks.
The goal is not to change runtime animation behavior, only to improve test observability and correctness.
Motivation
Current Problems
-
No public API to observe animation progress
- Tests cannot access
Animation<double> without reaching into private state or widget internals.
- Developers often cast
State or extract controllers, which is fragile and non-generic.
-
Brittle assertions
- Tests assert layout positions (
dy, dx) at specific timestamps.
- Minor changes in layout, device size, or easing math break tests unrelated to animation intent.
-
No curve-level validation
- There is no way to assert that an animation follows
Curves.easeIn, easeOut, etc.
- Reverse curves (
reverseCurve) are especially hard to validate correctly.
-
Lifecycle hazards
- Attaching listeners during tests without guaranteed cleanup leads to flakiness and memory leaks across frames.
Example from the Framework
The following test demonstrates how animation customization is currently validated for showModalBottomSheet:
testWidgets('Modal bottom sheet animation can be customized', (WidgetTester tester) async {
final Key sheetKey = UniqueKey();
Widget buildWidget({AnimationStyle? sheetAnimationStyle}) {
return MaterialApp(
home: Scaffold(
body: Builder(
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
showModalBottomSheet<void>(
context: context,
sheetAnimationStyle: sheetAnimationStyle,
builder: (BuildContext context) {
return SizedBox.expand(
child: ColoredBox(
key: sheetKey,
color: Theme.of(context).colorScheme.primary,
child: FilledButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text('Close'),
),
),
);
},
);
},
child: const Text('X'),
);
},
),
),
);
}
// Test custom animation style.
await tester.pumpWidget(
buildWidget(
sheetAnimationStyle: const AnimationStyle(
duration: Duration(milliseconds: 800),
reverseDuration: Duration(milliseconds: 400),
),
),
);
await tester.tap(find.text('X'));
await tester.pump();
await tester.pump(const Duration(milliseconds: 400));
expect(tester.getTopLeft(find.byKey(sheetKey)).dy, closeTo(316.7, 0.1));
await tester.pump(const Duration(milliseconds: 400));
expect(tester.getTopLeft(find.byKey(sheetKey)).dy, equals(262.5));
await tester.tap(find.widgetWithText(FilledButton, 'Close'));
await tester.pump();
await tester.pump(const Duration(milliseconds: 200));
expect(tester.getTopLeft(find.byKey(sheetKey)).dy, closeTo(316.7, 0.1));
await tester.pump(const Duration(milliseconds: 200));
expect(tester.getTopLeft(find.byKey(sheetKey)).dy, equals(600.0));
// Test no animation style.
await tester.pumpWidget(buildWidget(sheetAnimationStyle: AnimationStyle.noAnimation));
await tester.pumpAndSettle();
await tester.tap(find.text('X'));
await tester.pump();
expect(tester.getTopLeft(find.byKey(sheetKey)).dy, equals(262.5));
await tester.tap(find.widgetWithText(FilledButton, 'Close'));
await tester.pump();
expect(find.byKey(sheetKey), findsNothing);
});
This test correctly verifies that animation durations are respected and that the sheet transitions between visible states. However, it also highlights several limitations of the current approach.
- Tests assert exact layout values at specific timestamps.
- Assertions depend on screen size, constraints, and layout behavior.
- The test cannot validate which curve is used, only the final geometry.
- Forward and reverse animations cannot be distinguished beyond timing.
- Small internal changes can invalidate the test without changing behavior.
In practice, this means tests validate effects of animations, not animation behavior itself.
Proposed Direction (Conceptual)
This proposal does not suggest changes to runtime animation behavior. Instead, it outlines potential improvements to test-only observability and validation.
1. Test-oriented animation observation
Introduce a test-only abstraction that allows observing animation progress over time in a normalized, implementation-agnostic way.
2. Generic animation association
Allow tests to associate a widget under test with the animation that drives it, without relying on private state or widget-specific knowledge.
3. Curve-based validation
Enable validation of animations based on sampled progress compared against an expected curve, rather than asserting fixed layout values at specific timestamps.
4. Lifecycle enforcement
Any animation observation mechanism should define and enforce a clear lifecycle, including explicit cleanup, to avoid cross-test interference.
Benefits
- Tests express animation intent rather than layout math.
- Reduced brittleness and flakiness in animation-heavy tests.
- Better validation coverage for curve and reverse-curve APIs.
- Improved confidence when evolving animation-related framework APIs.
Non-Goals
- No changes to runtime animation behavior.
- No exposure of animation controllers or internal state.
- No breaking changes to existing APIs.
Summary
Flutter currently lacks a first-class, deterministic way to validate animation curves and timings in widget tests.
The recommended approach relies on pixel/position assertions at fixed timestamps, which is brittle, device-dependent, and tightly coupled to layout details.
This proposal introduces a testing-oriented abstraction to:
Animation<double>).dispose) to avoid test leaks.The goal is not to change runtime animation behavior, only to improve test observability and correctness.
Motivation
Current Problems
No public API to observe animation progress
Animation<double>without reaching into private state or widget internals.Stateor extract controllers, which is fragile and non-generic.Brittle assertions
dy,dx) at specific timestamps.No curve-level validation
Curves.easeIn,easeOut, etc.reverseCurve) are especially hard to validate correctly.Lifecycle hazards
Example from the Framework
The following test demonstrates how animation customization is currently validated for
showModalBottomSheet:This test correctly verifies that animation durations are respected and that the sheet transitions between visible states. However, it also highlights several limitations of the current approach.
In practice, this means tests validate effects of animations, not animation behavior itself.
Proposed Direction (Conceptual)
This proposal does not suggest changes to runtime animation behavior. Instead, it outlines potential improvements to test-only observability and validation.
1. Test-oriented animation observation
Introduce a test-only abstraction that allows observing animation progress over time in a normalized, implementation-agnostic way.
2. Generic animation association
Allow tests to associate a widget under test with the animation that drives it, without relying on private state or widget-specific knowledge.
3. Curve-based validation
Enable validation of animations based on sampled progress compared against an expected curve, rather than asserting fixed layout values at specific timestamps.
4. Lifecycle enforcement
Any animation observation mechanism should define and enforce a clear lifecycle, including explicit cleanup, to avoid cross-test interference.
Benefits
Non-Goals