3

bounty info: I'll accept your answer if:

  • isn't something along the line do this instead
  • the code sample is mostly unchanged
  • produce successful test, not just some quote from docs
  • doesn't need any extra package

[edit : 07/02/21] following Miyoyo#5957 on flutter community on discord @iapicca Convert widget position to global, get width height, add both, and see if the resulting bottom right position is on screen? and using the following answers as reference:

given the code sample below (also runnable on dartpad)

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

final _testKey = GlobalKey();
const _fabKey = ValueKey('fab');
final _onScreen = ValueNotifier<bool>(true);

void main() => runApp(_myApp);

const _myApp = MaterialApp(
  home: Scaffold(
    body: MyStage(),
    floatingActionButton: MyFAB(),
  ),
);

class MyFAB extends StatelessWidget {
  const MyFAB() : super(key: const ValueKey('MyFAB'));

  @override
  Widget build(BuildContext context) => FloatingActionButton(
        key: _fabKey,
        onPressed: () => _onScreen.value = !_onScreen.value,
      );
}

class MyStage extends StatelessWidget {
  const MyStage() : super(key: const ValueKey('MyStage'));

  @override
  Widget build(BuildContext context) => Stack(
        children: [
          ValueListenableBuilder(
            child: FlutterLogo(
              key: _testKey,
            ),
            valueListenable: _onScreen,
            builder: (context, isOnStage, child) => AnimatedPositioned(
              top: MediaQuery.of(context).size.height *
                  (_onScreen.value ? .5 : -1),
              child: child,
              duration: const Duration(milliseconds: 100),
            ),
          ),
        ],
      );
}

I want to test is the widget is off screen here's the test code so far

void main() {
  testWidgets('...', (tester) async {
    await tester.pumpWidget(_myApp);
    final rect = _testKey.currentContext.findRenderObject().paintBounds;

    expect(tester.getSize(find.byKey(_testKey)), rect.size,
        reason: 'size should match');

    final lowestPointBefore = rect.bottomRight.dy;
    print('lowest point **BEFORE** $lowestPointBefore ${DateTime.now()}');
    expect(lowestPointBefore > .0, true, reason: 'should be on-screen');

    await tester.tap(find.byKey(_fabKey));
    await tester.pump(const Duration(milliseconds: 300));
    final lowestPointAfter =
        _testKey.currentContext.findRenderObject().paintBounds.bottomRight.dy;

    print('lowest point **AFTER** $lowestPointAfter ${DateTime.now()}');
    expect(lowestPointAfter > .0, false, reason: 'should be off-screen');
  });
}


and the logs produced

00:03 +0: ...                                                                                                                                                                                               
lowest point **BEFORE** 24.0 2021-02-07 16:28:08.715558
lowest point **AFTER** 24.0 2021-02-07 16:28:08.850733
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following TestFailure object was thrown running a test:
  Expected: <false>
  Actual: <true>

When the exception was thrown, this was the stack:
#4      main.<anonymous closure> (file:///home/francesco/projects/issue/test/widget_test.dart:83:5)
<asynchronous suspension>
<asynchronous suspension>
(elided one frame from package:stack_trace)
...

This was caught by the test expectation on the following line:
  file:///home/francesco/projects/issue/test/widget_test.dart line 83
The test description was:
  ...
════════════════════════════════════════════════════════════════════════════════════════════════════
00:03 +0 -1: ... [E]                                                                                                                                                                                        
  Test failed. See exception logs above.
  The test description was: ...
  
00:03 +0 -1: Some tests failed.                                            

I'm not sure if my approach is correct and the time in the print suggest me that

lowest point **BEFORE** 24.0 2021-02-07 16:28:08.715558
lowest point **AFTER** 24.0 2021-02-07 16:28:08.850733

suggest me that await tester.pumpAndSettle(Duration(milliseconds: 300)); doesn't do what I think it does

Francesco Iapicca
  • 2,618
  • 5
  • 40
  • 85
  • Yes, strange that Rect of AnimatedPositioned widget is not updating after animation, same for Positioned widget which is inside that AnimatedPositioned widget. I have solution but it's without that Rect, Let me check for rect solution. – Parth Dave Feb 07 '21 at 18:46

3 Answers3

4

Problems are:

  1. We were trying to find the rect of FlutterLogo but FlutterLogo rect will remain same the parent AnimatedPositioned widget's location are actually changing.
  2. Even though we now start to check for AnimatedPositioned paintBounds it will still be the same as we are not changing width but the position it self.

Solution:

  1. Get the screen rect by topWidget for me it's Scaffold. (if we have different widgets like HomeScreen which contains FAB button we just need to find that rect)
  2. Before click I'm checking if fab button is on-screen or not
  3. Tap and pump the widget and let it settle.
  4. Search for widget rect and it will be out of the screen i.e. in our case -600

Added comments in the code it self

testWidgets('...', (tester) async {
    await tester.pumpWidget(MyApp);
    //check screen width height - here I'm checking for scaffold but you can put some other logic for screen size or parent widget type
    Rect screenRect = tester.getRect(find.byType(Scaffold));
    print("screenRect: $screenRect");

    //checking previous position of the widget - on our case we are animating widget position via AnimatedPositioned
    // which in itself is a statefulwidget and has Positioned widget inside
    //also if we have multiple widgets of same type give them uniqueKey
    AnimatedPositioned widget =
        tester.firstWidget(find.byType(AnimatedPositioned));
    double topPosition = widget.top;
    print(widget);
    print("AnimatedPositioned topPosition: $topPosition}");
    expect(
        screenRect.bottom > topPosition && screenRect.top < topPosition, true,
        reason: 'should be on-screen');

    //click button to animate the widget and wait
    await tester.tap(find.byKey(fabKey));
    //this will wait for animation to settle or call pump after duration
    await tester.pumpAndSettle(const Duration(milliseconds: 300));

    //check after position of the widget
    AnimatedPositioned afterAnimationWidget =
        tester.firstWidget(find.byType(AnimatedPositioned));

    double afterAnimationTopPosition = afterAnimationWidget.top;
    Rect animatedWidgetRect = tester.getRect(find.byType(AnimatedPositioned));
    print("rect of widget : $animatedWidgetRect");
    expect(
        screenRect.bottom > afterAnimationTopPosition &&
            screenRect.top < afterAnimationTopPosition,
        false,
        reason: 'should be off-screen');
  });

Note: replaced _ from code as it was hiding the object from test file.

Output:

screenRect: Rect.fromLTRB(0.0, 0.0, 800.0, 600.0)
fab clicked
rect of widget : Rect.fromLTRB(0.0, -600.0, 24.0, -576.0)
Parth Dave
  • 2,096
  • 13
  • 20
0

I found this answer (in particular the code inside the onNotification), which kind of does what (I think) you want. It finds the RenderObject using the current context of the key. Afterwards it finds the RenderAbstractViewport using this RenderObject, and checks the offSetToReveal. Using this offset you can determine whether the current RenderObject is being displayed or not (using a simple comparison).

I'm not a 100% sure this will work / is what you want, but hopefully it can push you in the right direction.

Also (even though you stated you didn't want any external package), on the same question someone recommended this package, which can be useful for others having the same problem but who are open to using an external package.

Jasper
  • 302
  • 3
  • 11
  • I'm not sure how this would help me, can you show me an implementation or at least pseudo-code – Francesco Iapicca Feb 07 '21 at 09:47
  • I haven't written a flutter test in a while, so it would be pretty horrendous if I did. However, I see that you edited your original post with a suggestion. That suggestion basically does the same as my suggestion. Miyoyo said to get the widget position & width & height and then determine if this position + width would be showing on screen. My answer would do the same but use a function offSetToReveal which I believe does something similar and returns an offset that would be needed to reveal the object. – Jasper Feb 07 '21 at 12:41
  • my update code sample doesn't work, feel free to fix it if you know how to, it would definitely count – Francesco Iapicca Feb 07 '21 at 13:36
0

I want to thank @parth-dave for his answer, that I happily reward with the bounty

and Miyoyo referenced in the question

I want to offer my own implementation built on his approach

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

// !! uncomment tge line below to run as test app
// void main() => runApp(_myApp);

class Keys {
  static final subject = UniqueKey();
  static final parent = UniqueKey();
  static final trigger = UniqueKey();
}

final _onScreen = ValueNotifier<bool>(true);

Widget get app => MaterialApp(
      home: Scaffold(
        key: Keys.parent,
        body: MyStage(),
        floatingActionButton: MyFAB(),
      ),
    );

class MyFAB extends StatelessWidget {
  const MyFAB() : super(key: const ValueKey('MyFAB'));

  @override
  Widget build(BuildContext context) => FloatingActionButton(
        key: Keys.trigger,
        onPressed: () => _onScreen.value = !_onScreen.value,
      );
}

class MyStage extends StatelessWidget {
  const MyStage() : super(key: const ValueKey('MyStage'));

  @override
  Widget build(BuildContext context) => Stack(
        children: [
          ValueListenableBuilder(
            child: FlutterLogo(
              key: Keys.subject,
            ),
            valueListenable: _onScreen,
            builder: (context, isOnStage, child) => AnimatedPositioned(
              top: MediaQuery.of(context).size.height *
                  (_onScreen.value ? .5 : -1),
              child: child,
              duration: const Duration(milliseconds: 100),
            ),
          ),
        ],
      );
}

void main() {
  group('`AnimatedPositined` test', () {
    testWidgets(
        'WHEN no interaction with `trigger` THEN `subject` is ON SCREEN',
        (tester) async {
      await tester.pumpWidget(app);

      final parent = tester.getRect(find.byKey(Keys.parent));
      final subject = tester.getRect(find.byKey(Keys.subject));

      expect(parent.overlaps(subject), true, reason: 'should be ON-screen');
    });

    testWidgets('WHEN `trigger` tapped THEN `subject` is OFF SCREEN`',
        (tester) async {
      await tester.pumpWidget(app);

      await tester.tap(find.byKey(Keys.trigger));
      await tester.pumpAndSettle(const Duration(milliseconds: 300));

      final parent = tester.getRect(find.byKey(Keys.parent));
      final subject = tester.getRect(find.byKey(Keys.subject));

      expect(parent.overlaps(subject), false, reason: 'should be OFF-screen');
    });
  });
}

Francesco Iapicca
  • 2,618
  • 5
  • 40
  • 85