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

FlareControls never initializes in unit test & FlareActor never displays #160

Closed
ravenblackx opened this issue Sep 26, 2019 · 13 comments
Closed
Assignees

Comments

@ravenblackx
Copy link

import 'dart:async';

import 'package:flare_flutter/flare_actor.dart';
import 'package:flare_flutter/flare_controls.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

class DemoControls extends FlareControls {
  Completer completer;
  DemoControls(this.completer);

  @override
  void initialize(artboard) {
    super.initialize(artboard);
    print('initialized');
    completer.complete();
  }
}

void main() {
  Completer completer;
  setUp(() {
    completer = Completer();
  });

  testWidgets('Flare initialize failure', (tester) async {
    await tester.pumpWidget(MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        backgroundColor: Colors.transparent,
        body: Container(
          width: 500,
          height: 500,
          child: FlareActor(
            'assets/something.flr',
            alignment: Alignment.center,
            fit: BoxFit.contain,
            controller: DemoControls(completer),
          ),
        ),
      ),
    ));
    print('created');
    for (int i = 0; i < 100; i++) {
      await tester.pump();
    }
    print('pumped');
    for (int i = 0; i < 100; i++) {
      await tester.pump(Duration(milliseconds: 100));
    }
    print('time-pumped');

    // With the following line uncommented the test blocks forever.
    // await completer.future;

    // This is blank, of course, because the actor is not initialized.
    await expectLater(find.byType(Scaffold).first,
        matchesGoldenFile('some_golden_file'));
    print('golden-checked');

    // I did have one example where you could await completer.future here
    // and it would work, but in this example it still blocks forever.
    print('done');

    // Even without blocking, the test fails like:
    // Test Flare initialize failure failed: ══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞═════════════════
    // The following assertion was thrown running a test:
    // A Timer is still pending even after the widget tree was disposed.
  });
}

// The order of the output from the test is:
// created
// pumped
// time-pumped
// [some golden file comparison output]
// golden-checked
// done
// initialized  *** this happens only when the test starts closing! ***
// Pending timers: [details of the unresolved timers] [already mentioned in bug #123]
@ravenblackx
Copy link
Author

Ah, I found the circumstances under which the completer completes before the test ends - if you pump after the golden test, then the initialize call is finally provoked. But as you can see in the given example, 200 pumps, 100 of them advancing time, before the golden test, avail nothing.

@jkurtw
Copy link
Collaborator

jkurtw commented Oct 4, 2019

/cc @luigi-rosso

@luigi-rosso luigi-rosso self-assigned this Oct 4, 2019
@luigi-rosso
Copy link
Contributor

Looks like this is related to flutter/flutter#24703

We use compute to perform the loading of a Flare file. Compute futures never complete in the testing environment. I'm looking into implementing a workaround using kDebugMode.

@luigi-rosso
Copy link
Contributor

Try updating to flare_flutter: 1.5.12.

Then call FlareTesting.setup(); before running any tests. Here's a working example (you'll need a valid .flr file):

import 'dart:async';

import 'package:flare_flutter/flare_actor.dart';
import 'package:flare_flutter/flare_testing.dart';
import 'package:flare_flutter/flare_controls.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

class DemoControls extends FlareControls {
  Completer completer;
  DemoControls(this.completer);

  @override
  void initialize(artboard) {
    super.initialize(artboard);
    print('initialized');
    completer.complete();
  }
}

void main() {
  FlareTesting.setup();
  Completer completer;
  setUp(() {
    completer = Completer();
  });

  testWidgets('Flare initialize failure', (tester) async {
    await tester.pumpWidget(MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        backgroundColor: Colors.transparent,
        body: Container(
          width: 500,
          height: 500,
          child: FlareActor(
            'assets/Filip.flr',
            alignment: Alignment.center,
            fit: BoxFit.contain,
            controller: DemoControls(completer),
          ),
        ),
      ),
    ));

    await tester.pumpAndSettle();

    /// This actually works in so far as it makes it outputs the print
    /// statement, but the rest of the test oddly locks up.
    // await completer.future;
    // print("awaited completer...");

    // This is blank, of course, because the actor is not initialized.
    await expectLater(
        find.byType(Scaffold).first, matchesGoldenFile('some_golden_file.png'));
    print('golden-checked');

    print('done');
  });
}

Note that I also had to remove await completer.future; Even though it was working (it would pass the await), it somehow stalled the rest of the framework after that.

@ravenblackx
Copy link
Author

Thanks, that helped significantly.
Followon surprise, after starting an animation and zero-duration pumping a bunch of times,

await tester.pump(Duration(milliseconds: 400));
await tester.pump();  // Even many times this changes nothing.
await expectLater(find.byType(Scaffold).first, matchesGoldenFile('no_animation_happened'));

The animation has not advanced, whereas

await tester.pump(Duration(milliseconds: 1));
await tester.pump(Duration(milliseconds: 400));
await expectLater(find.byType(Scaffold).first, matchesGoldenFile('some_animation_happened'));

The animation has moved on as expected, even though in both cases it should be 400ms having passed.

It seems like the animation controller's timer doesn't initialize until the first actual time increment, so the first frame worth of time passing is lost.

@luigi-rosso
Copy link
Contributor

I suspect this has to do with the load process being asynchronous. The controller only initializes once the file is loaded. Your strategy with the completer seems like the best way to do it: wait for the initialize and then pump 400 milliseconds. I couldn't diagnose why simply awaiting the completer would cause subsequent code to lockup. I'll do some further testing to see if I can advance an animation a specific amount of time via the testing framework.

@luigi-rosso
Copy link
Contributor

luigi-rosso commented Oct 10, 2019

Your observations were correct, my assumption was wrong. The first pump effectively starts the rendering/animation process. This is due to how Flare schedules repainting and tracks elapsed time. This code and comments should help clarify:

import 'dart:async';

import 'package:flare_flutter/flare.dart';
import 'package:flare_flutter/flare_actor.dart';
import 'package:flare_flutter/flare_testing.dart';
import 'package:flare_flutter/flare_controls.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

class DemoControls extends FlareControls {
  Completer completer;
  DemoControls(this.completer);
  double _elapsedTime = 0.0;

  double get elapsedTime => _elapsedTime;

  @override
  void initialize(artboard) {
    super.initialize(artboard);
    print('initialized');
    completer.complete();
  }

  @override
  bool advance(FlutterActorArtboard artboard, double elapsed) {
    print("DemoControls advancing $elapsed seconds");
    _elapsedTime += elapsed;
    super.advance(artboard, elapsed);
    return true;
  }
}

void main() {
  FlareTesting.setup();
  Completer completer;
  DemoControls controls;
  setUp(() {
    completer = Completer();
    controls = DemoControls(completer);
  });

  testWidgets('Flare initialize failure', (tester) async {
    await tester.pumpWidget(MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        backgroundColor: Colors.transparent,
        body: Container(
          width: 500,
          height: 500,
          child: FlareActor(
            'assets/Filip.flr',
            alignment: Alignment.center,
            fit: BoxFit.contain,
            controller: controls,
          ),
        ),
      ),
    ));

    await tester.pump();

    // Wait for initialize. Doesn't make a
    // difference, you can comment out this
    // whole block and have the same results.
    print("Wait for completer");
    await tester.runAsync(() async {
      await completer.future;
    });

    print("First pump.");
    // No matter how long this first pump is,
    // the controller advances 0 seconds.
    // This is because of how Flare repaints and
    // measures time:
    // Scheduling a frame callback and then
    // computing elapsed time between previous
    // paint. So the first time it paints, it needs
    // to mark the first timestamp. So the first
    // elapsed value when starting/re-starting
    // playback is always 0.
    await tester.pump(Duration(milliseconds: 1));

    print("Second pump.");
    // Now that _lastFrameTime in FlareRenderBox is
    // set, time will elapse as expected.
    await tester.pump(Duration(milliseconds: 200));
    print("Controller elapsed: ${controls.elapsedTime}");

    // This is blank, of course, because the actor is not initialized.
    await expectLater(
        find.byType(Scaffold).first, matchesGoldenFile('some_golden_file.png'));
    print('golden-checked');

    print('done');
  });
}

Output should look similar to:

$ flutter test
00:02 +0: Flare initialize failure                                                                                                                                                                                                  
initialized
DemoControls advancing 0.0 seconds
Wait for completer
First pump.
DemoControls advancing 0.0 seconds
Second pump.
DemoControls advancing 0.2 seconds
Controller elapsed: 0.2
golden-checked
done
DemoControls advancing 0.0 seconds
00:02 +1: All tests passed!

Open to suggestions for how to improve this! We originally just used the frame scheduler, and let it schedule the next frame if necessary, but this made it necessary to carefully track when a widget was removed from the hierarchy and sometimes these calls would come in rapid succession when navigating (detach/re-attach) which would then need debouncing. The approach we settled on was to use the scheduler in response to a paint, as paint only happens when the widget is mounted.

@ravenblackx
Copy link
Author

ravenblackx commented Oct 10, 2019

I think if, in flare_render_box.dart, you made _lastFrameTime null for "hasn't been set yet" rather than using 0 for unset, then pump(no duration) would update it to zero, and pump(some duration) after that would then advance it by that duration, bringing its behavior in line with that of other animations.
ie.
replace double _lastFrameTime = 0.0; with double _lastFrameTime;
replace _lastFrameTime = 0; with _lastFrameTime = null;
replace double elapsedSeconds = _lastFrameTime == 0.0 ? 0.0 : t - _lastFrameTime; with double elapsedSeconds = _lastFrameTime == null ? 0.0 : t - _lastFrameTime;

Edit:
Also, while you're there, might as well replace
final double t = timestamp.inMicroseconds / Duration.microsecondsPerMillisecond / 1000.0;
with
final double t = timestamp.inMicroseconds.toDouble() / Duration.microsecondsPerSecond;

@andrewpmoore
Copy link

andrewpmoore commented May 19, 2020

We've found that by running test and wrapping them in runUnsynchronized we can keep flare animations working without any obvious issues

      test('flare animation test', () async {
        await driver.runUnsynchronized(() async {
                 //get things here
        }, );

      });

This basically means the find won't wait until there's no frame left to render before trying to find the object and therefore it doesn't timeout when it's busy always running a rive animation (or any other type of animation)

@ravenblackx
Copy link
Author

I think that's a solution to a different problem. Good to know though!

@ravenblackx
Copy link
Author

It looks like the zero-duration pump not starting the animation has been fixed - I suspect the timer behavior has been changed significantly since another timer-related issue was also fixed.

@ravenblackx
Copy link
Author

No, my mistake, the animation not starting until after the first time-advance is still an issue, I just happened to be touching it in a test where the animation was completing a full cycle. But I'll open it as a separate issue, because the title of this one has nothing to do with it.

@ravenblackx
Copy link
Author

Created issue #262 for this.

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

4 participants