2

赏金信息:如果出现以下情况,我会接受您的回答:

  • 不是顺其自然do this instead
  • 代码示例大部分未更改
  • 产生成功的测试,而不仅仅是文档中的一些引用
  • 不需要任何额外的包

[编辑:07/02/21]在不和谐的颤振社区上 关注 Miyoyo#5957@iapicca Convert widget position to global, get width height, add both, and see if the resulting bottom right position is on screen?并使用以下答案作为参考:

给出下面的代码示例(也可以在 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),
            ),
          ),
        ],
      );
}

我要测试的是小部件是off screen 这里的测试代码到目前为止

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


和产生的日志

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.                                            

我不确定我的方法是否正确,打印中的时间告诉我

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

建议我 await tester.pumpAndSettle(Duration(milliseconds: 300)); 不要做我认为的事情

4

3 回答 3

3

问题是:

  1. 我们试图找到 FlutterLogo 的矩形,但 FlutterLogo 矩形将保持不变,父 AnimatedPositioned 小部件的位置实际上正在改变。
  2. 即使我们现在开始检查 AnimatedPositioned paintBounds 它仍然是一样的,因为我们没有改变宽度而是改变它自己的位置。

解决方案:

  1. 对我来说,通过 topWidget 获取屏幕矩形,它是 Scaffold。(如果我们有不同的小部件,比如 HomeScreen 包含 FAB 按钮,我们只需要找到那个矩形)
  2. 在单击之前,我正在检查 fab 按钮是否在屏幕上
  3. 点击并泵送小部件,让它安定下来。
  4. 搜索小部件 rect 它将不在屏幕上,即在我们的例子中为 -600

在它自己的代码中添加了注释

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

注意:从代码中替换了 _,因为它从测试文件中隐藏了对象。

输出:

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)
于 2021-02-07T19:14:22.810 回答
0

我找到了这个答案(特别是 onNotification 中的代码),哪种做(我认为)你想要的。它使用键的当前上下文找到 RenderObject。之后,它使用此 RenderObject 找到 RenderAbstractViewport,并检查offSetToReveal。使用此偏移量,您可以确定当前的 RenderObject 是否正在显示(使用简单的比较)。

我不是 100% 确定这会奏效/是您想要的,但希望它可以将您推向正确的方向。

此外(即使您说您不想要任何外部包),在同一个问题上,有人推荐了这个包,这对于其他有同样问题但愿意使用外部包的人很有用。

于 2021-02-07T02:09:29.753 回答
0

我要感谢@parth-dave 的回答,我很乐意用赏金来奖励

Miyoyo在问题中引用

我想提供基于他的方法的我自己的实现

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

于 2021-02-08T09:31:29.727 回答