how do I test for a transition actually occuring

Issue

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,
    );
  }
}

dartpad example

what is an elegant way to test that on event received the "shakiness" actually occurs?
thank you

Solution

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 InheritedWidgets 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 🙂

Answered By – cameron1024

Answer Checked By – Clifford M. (FlutterFixes Volunteer)

Leave a Reply

Your email address will not be published. Required fields are marked *