How to verify a widget is "offscreen"

Issue

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

Solution

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)

Answered By – Parth Dave

Answer Checked By – Gilberto Lyons (FlutterFixes Admin)

Leave a Reply

Your email address will not be published.