【问题标题】:How to verify a widget is "offscreen"如何验证小部件是否“离屏”
【发布时间】:2021-05-08 19:10:33
【问题描述】:

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

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

[编辑:21 年 7 月 2 日] 在 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? 上关注 Miyoyo#5957,并使用以下答案作为参考:

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

我要测试的是小部件是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)); 没有做我认为的事情

【问题讨论】:

  • 是的,奇怪的是 AnimatedPositioned 小部件的 Rect 在动画之后没有更新,对于位于 AnimatedPositioned 小部件内部的定位小部件也是如此。我有解决方案,但没有那个 Rect,让我检查一下 rect 解决方案。

标签: flutter dart testing widget-test-flutter


【解决方案1】:

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

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

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

【讨论】:

  • 我不确定这对我有什么帮助,你能告诉我一个实现或至少是伪代码
  • 我已经有一段时间没有写 Flutter 测试了,所以如果我写了那将是非常可怕的。但是,我看到您编辑了原始帖子并提出了建议。该建议与我的建议基本相同。 Miyoyo 说要获取小部件的位置和宽度和高度,然后确定这个位置+宽度是否会显示在屏幕上。我的回答会做同样的事情,但使用一个函数 offSetToReveal,我相信它会做类似的事情并返回显示对象所需的偏移量。
  • 我的更新代码示例不起作用,如果您知道如何解决,请随时修复它,它肯定会计数
【解决方案2】:

问题是:

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

解决方案:

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

在自己的代码中添加了 cmets

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)

【讨论】:

  • 谢谢@ParthDave 正是我想要的!
  • 很高兴为您提供帮助@FrancescoIapicca
【解决方案3】:

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

问题中提到的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');
    });
  });
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-06-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-01-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多