Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

request: pump more than one widget in sequence #141

Open
1 task done
pedroleiterocha opened this issue Jan 18, 2025 · 3 comments
Open
1 task done

request: pump more than one widget in sequence #141

pedroleiterocha opened this issue Jan 18, 2025 · 3 comments

Comments

@pedroleiterocha
Copy link

Is there an existing feature request for this?

  • I have searched the existing issues.

Command

pump one widget, and then another

Description

I've been trying to test a few animated widgets which produce an animation based on changes to its properties. They are very similar to Flutter's own animated widgets. Take AnimatedOpacity for example. At first, when AnimatedOpacity is built, its child is built with the specified opacity. Then, if it is built again with a different value for opacity, an animation is performed where the opacity level changes from the original to the new value.

Testing this widget would require me to pump it with one value for opacity and then pump it again with a different value. I've tried using goldenTest.pumpWidget with something like:

  pumpWidget: (tester, widget) {
    tester.pumpWidget(widget);
    tester.pumpWidget(AnimatedOpacity(opacity: newValue, child: (widget as AnimatedOpacity).child));
  },

But the widget received by pumpWidget is an Alchemist wrapper around AnimatedOpacity, and not just an AnimatedOpacity widget.

Reasoning

This is a basic type of animation used even by Flutter's own animated widgets. It would be nice to be able to test these with Alchemist.

Additional context and comments

Is there currently a way in which this could work with Alchemist as is? Or what kind of change would it make sense to implement in Alchemist to make tests such as this possible?

@Kirpal
Copy link
Collaborator

Kirpal commented Jan 21, 2025

Can you explain a little bit more of what the expected outcome would be? For example, if you ran your example with a starting opacity of 0 and an ending of 1, what opacity would you want captured in the golden image file?

My initial reaction is that alchemist isn't the best use case for this, because there can be slight platform differences in animation timing. For testing animations with alchemist I recommend trying the method I outlined here. Let me know if that works or if you think this issue can be handled in another way.

@pedroleiterocha
Copy link
Author

Can you explain a little bit more of what the expected outcome would be? For example, if you ran your example with a starting opacity of 0 and an ending of 1, what opacity would you want captured in the golden image file?

I guess I would like to capture a few golden image files at different states of the animation. I would like to use alchemist for a test where I need to pump two slightly different widgets, like the one below:

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  testWidgets('AnimatedOpacity', (tester) async {
    Future<void> pumpWidget(double opacity) => tester.pumpWidget(
          Center(
            child: DecoratedBox(
              decoration: const BoxDecoration(color: Colors.white),
              child: SizedBox(
                width: 100,
                height: 100,
                child: AnimatedOpacity(
                  opacity: opacity,
                  duration: const Duration(seconds: 2),
                  child: const Placeholder(),
                ),
              ),
            ),
          ),
        );

    await pumpWidget(0.3);
    await expectLater(
      find.byType(DecoratedBox),
      matchesGoldenFile('goldens/screenshot0.png'),
    );

    await pumpWidget(1);
    await expectLater(
      find.byType(DecoratedBox),
      matchesGoldenFile('goldens/screenshot1.png'),
    );

    await tester.pump(const Duration(seconds: 1));
    await expectLater(
      find.byType(DecoratedBox),
      matchesGoldenFile('goldens/screenshot2.png'),
    );

    await tester.pump(const Duration(seconds: 1));
    await expectLater(
      find.byType(DecoratedBox),
      matchesGoldenFile('goldens/screenshot3.png'),
    );
  });
}

But I would like to use Alchemist instead of Flutter's own matchesGoldenFile because Alchemist has a lot features which are important to me, like:

  • The ability to use dev/feat/tolerance to account for local and CI differences.
  • The platform and ci variants which Alchemist generates for me.
  • The fact that I don't need to wrap my widgets with Theme, Directionality or whatnot since Alchemist already does that or similar for me.

My initial reaction is that alchemist isn't the best use case for this, because there can be slight platform differences in animation timing. For testing animations with alchemist I recommend trying the method I outlined #134 (comment). Let me know if that works or if you think this issue can be handled in another way.

Your example may work, but it will involve more refactoring than I would like. Also, I will need to come up with another (non-golden) test to ensure that the outer widget is passing the correct animation values to the inner widget.

If there are no plans for a feature like this, I may try a different hack, where I wrap AnimatedOpacity with a stateful widget with a timer which switches the value of opacity, and then take the screenshots based on that timer.

@pedroleiterocha
Copy link
Author

I was able to use a timer to create a frame-by-frame golden test of an animation. I'll leave it here in case it's of use to anyone.

import 'dart:async';
import 'package:alchemist/alchemist.dart';
import 'package:flutter/material.dart';

void main() {
  const animationDuration = Duration(seconds: 10);
  const frames = 10;
  final frameDuration = animationDuration ~/ frames;

  AnimatedOpacity animatedOpacity(double opacity) => AnimatedOpacity(
        duration: animationDuration,
        opacity: opacity,
        child: const SizedBox(
          width: 100,
          height: 100,
          child: Placeholder(),
        ),
      );

  goldenTest(
    'AnimatedOpacity',
    fileName: 'animated_opacity',
    pumpBeforeTest: pumpNTimes(frames + 2, frameDuration),
    builder: () => GoldenTestGroup(
      children: [
        for (Duration d = animationDuration + const Duration(seconds: 1);
            d >= Duration.zero;
            d -= frameDuration)
          GoldenTestScenario(
            name: '${d.inSeconds}s',
            child: _DelayedSwitch(
              delay: d,
              first: animatedOpacity(0.3),
              second: animatedOpacity(1),
            ),
          ),
      ],
    ),
  );
}

class _DelayedSwitch extends StatefulWidget {
  const _DelayedSwitch({
    required this.delay,
    required this.first,
    required this.second,
  });

  final Duration delay;
  final Widget first;
  final Widget second;

  @override
  State<StatefulWidget> createState() => _DelayedSwitchState();
}

class _DelayedSwitchState extends State<_DelayedSwitch> {
  late Timer _timer;
  bool _delayElapsed = false;

  @override
  void initState() {
    super.initState();
    _delayElapsed = false;
    _timer = Timer(widget.delay, () => setState(() => _delayElapsed = true));
  }

  @override
  void dispose() {
    _timer.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return _delayElapsed ? widget.second : widget.first;
  }
}

It produces this image:
Image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants