given a widget that "shakes" on Stream event
class Shaker extends StatefulWidget {
final Widget child;
final Stream<void> stream;
final Duration duration;
final int loops;
const Shaker({
required this.child,
required this.stream,
this.duration = const Duration(milliseconds: 100),
this.loops = 2,
Key? key,
}) : super(key: key);
@override
State<Shaker> createState() => _ShakerState();
}
class _ShakerState extends State<Shaker> with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late final StreamSubscription _subscription;
late final Animation<Offset> _offsetAnimation;
@override
void initState() {
_controller = AnimationController(
vsync: this,
duration: widget.duration,
);
_subscription = widget.stream.listen((event) async {
for (var i = 0; i < widget.loops; ++i) {
await _controller.forward();
await _controller.reverse();
}
});
_offsetAnimation = Tween<Offset>(
begin: Offset.zero,
end: const Offset(.02, 0.0),
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.elasticIn,
));
super.initState();
}
@override
void dispose() {
_controller.dispose();
_subscription.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SlideTransition(
position: _offsetAnimation,
child: widget.child,
);
}
}
what is an elegant way to test that on event received the "shakiness" actually occurs?
thank you
In your widget test, you can use WidgetTester.pump()
and WidgetTester.pumpAndSettle()
to verify this behaviour.
pump()
simply renders a new frame from the framework, but pumpAndSettle()
continuously renders frames until there are no more scheduled frames, then returning the number of frames that were scheduled.
A simple (but less precise) test would be to make sure that your animation plays for the correct amount of frames. Something like:
const expectedFramesForShake = 10;
testWidgets('example', (tester) async {
final controller = StreamController<void>();
await tester.pumpWidget(Shaker(stream: controller.stream, /* other fields */));
controller.add(null); // cause a shake
final frames = await tester.pumpAndSettle();
expect(frames, expectedFramesForShake);
});
Note, you may have to wrap your widget in a MaterialApp
to set up the necessary InheritedWidget
s to make the test pass.
This verifies that the animation plays for approximately the right amount of time, but we can do better!
Instead of relying on pumpAndSettle()
to return the number of frames scheduled, we can inspect the widget tree after every frame.
// change this so it matches the values produced by the "correct" animation
const expectedOffsets = [0.0, 1.0, 3.0, 7.0,];
testWidgets('better example', (tester) async {
final controller = StreamController<void>();
await tester.pumpWidget(Shaker(stream: controller.stream, /* other fields */));
final offsets = <double>[]; // empty list to store results
controller.add(null);
do {
// pull the SlideTransition from the widget tree
final transition = tester.widget<SlideTransition>(find.byType<SlideTransition>);
// read the current value of the position animation
final horizontalOffset = transition.position.value.dx;
offsets.add(horizontalOffset);
// now request a new frame
await tester.pump();
// only loop while there are more frames scheduled
} while (tester.binding.hasScheduledFrame);
// compare the 2 arrays
expect(offsets, expectedOffsets);
});
This gives you certainty that the Shaker
is offsetting its child by exactly the right amount for each frame. However, this will make your tests quite brittle, and, for example, if you want to change the duration of your shake, it will change the value of offsets
.
P.S. Thanks for the dartpad example :-)