【问题标题】:Visualize taps/gestures for flutter-test (or flutter-driver)可视化用于颤振测试(或颤振驱动程序)的敲击/手势
【发布时间】:2020-10-23 22:49:49
【问题描述】:

当使用flutter_driver / flutter_test 时,我们会通过await tap() 之类的操作来模拟用户行为。但是,我想查看模拟器屏幕上的点击位置。可能吗?谢谢!

【问题讨论】:

  • 从你设备的开发者选项中你可以打开show taps它通常在Android Q开发者选项的input部分
  • @dev-aentgs 似乎颤振驱动程序使用了一些内部机制(例如命中测试)而不是模拟“真实用户”点击。因此恕我直言,这可能行不通?
  • 您究竟为什么要这样做/您想要实现什么。我可以用一种方法来满足您的要求,但这可能不是您想要的。
  • @creativecreatorormaybenot 这可以在许多情况下使用。例如,通过这样做,我们可以很容易地看到测试人员正在点击什么,并且更容易调试。
  • 视觉效果就是我想要的。我有一个秘诀,可以将 GitHub 动作中的 flutter_driver 测试记录到视频中,并将它们作为工件上传。这会让我看到测试何时失败,因为点击未命中

标签: flutter dart flutter-test


【解决方案1】:

我对此的想法(因为 Flutter Driver 和小部件测试不使用真正的点击)是在 Flutter 级别记录点击,即使用 Flutter 命中测试。

我将向您展示一个 widget,您可以将您的应用封装到 visualizecapture all水龙头。我写了a complete widget for this

演示

这是将小部件包装在默认模板演示应用程序周围时的结果:

实施

我们想要做的很简单:react 以我们的小部件大小(整个应用程序是我们的子应用程序)对 所有 点按事件。
然而,它带来了一个小挑战:GestureDetector e.g.对它们做出反应后,不会让点击通过。因此,如果我们使用TapGestureRecognizer,我们要么无法对点击我们应用中的按钮的点击做出反应我们将无法点击按钮(只会看到我们的指示)。

因此,我们需要使用我们的自己的渲染对象来完成这项工作。当您熟悉时,这并不是一项艰巨的任务 - RenderProxyBox 正是我们需要的抽象 :)

捕捉命中事件

通过覆盖hitTest,我们可以确保我们始终记录点击:

@override
bool hitTest(BoxHitTestResult result, {required Offset position}) {
  if (!size.contains(position)) return false;
  // We always want to add a hit test entry for ourselves as we want to react
  // to each and every hit event.
  result.add(BoxHitTestEntry(this, position));
  return hitTestChildren(result, position: position);
}

现在,我们可以使用handleEvent 来记录命中事件并可视化它们:

@override
void handleEvent(PointerEvent event, covariant HitTestEntry entry) {
  // We do not want to interfere in the gesture arena, which is why we are not
  // using regular tap recognizers. Instead, we handle it ourselves and always
  // react to the hit events (ignoring the gesture arena).
  if (event is PointerDownEvent) {
    // Record the global position.
    recordTap(event.position);

    // Visualize local position.
    visualizeTap(event.localPosition);
  }
}

可视化

我不会告诉你细节(完整代码在最后):我决定为每个记录的命中创建一个AnimationController,并将其与本地位置一起存储。

由于我们使用的是RenderProxyBox,我们可以在动画控制器触发时调用markNeedsPaint,然后为所有记录的点击绘制一个圆圈:

@override
void paint(PaintingContext context, Offset offset) {
  context.paintChild(child!, offset);

  final canvas = context.canvas;
  for (final tap in _recordedTaps) {
    drawTap(canvas, tap);
  }
}

代码

当然,我浏览了实现的大部分部分,因为您可以通读它们:)
代码应该是直截了当的,因为我概述了我使用的概念。

您可以找到the full source code here

用法

用法很简单:

TapRecorder(
  child: YourApp(),
)

即使在我的示例实现中,您也可以配置点击圈颜色、大小、持续时间等:

/// These are the parameters for the visualization of the recorded taps.
const _tapRadius = 15.0,
    _tapDuration = Duration(milliseconds: 420),
    _tapColor = Colors.white,
    _shadowColor = Colors.black,
    _shadowElevation = 2.0;

如果您愿意,可以将它们设为小部件参数。

测试

我希望可视化部分不辜负您的期望。

如果您想超越这一点,我确保水龙头是全局存储的:

/// List of the taps recorded by [TapRecorder].
///
/// This is only a make-shift solution of course. This will only be viable
/// when using a single [TapRecorder] because it is saved as a top-level
/// variable.
@visibleForTesting
final recordedTaps = <Offset>[];

您可以简单地访问测试中的列表来检查记录的点击:)

结束

我在实现这个过程中获得了很多乐趣,我希望它达到了您的期望。
该实现只是一个快速的临时实现,但是,我希望它可以为您提供将这个想法提升到一个好的水平所需的所有概念:)

【讨论】:

  • 看起来很棒!谢谢!我可以理解这个实现的想法:)
  • 干得好!赏金!我想知道通过颤振驱动器仪表包装器将其移植到这里会有多难
  • 您好,再次感谢您的回答!今天不仅需要显示点击事件,还需要显示所有内容(包括拖动、滚动等),因此我修改了您的解决方案并且效果很好(请参阅下面的答案)。
【解决方案2】:

这是我对@creativecreatorormaybenot 的修改。

在我最近的案例中,我不仅需要显示 tap 事件,还需要显示一切(包括拖动、滚动等)。所以我修改如下,效果很好:)

import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';

/// These are the parameters for the visualization of the recorded taps.
final _kTapRadius = 15.0, _kTapColor = Colors.grey[300]!, _kShadowColor = Colors.black, _kShadowElevation = 3.0;
const _kRemainAfterPointerUp = Duration(milliseconds: 100);

/// NOTE: refer to this answer for why use hitTest/handleEvent/etc https://stackoverflow.com/a/65067655
///
/// Widget that visualizes gestures.
///
/// It does not matter to this widget whether the child accepts the hit events.
/// Everything hitting the rect of the child will be recorded.
class GestureVisualizer extends SingleChildRenderObjectWidget {
  const GestureVisualizer({Key? key, required Widget child}) : super(child: child);

  @override
  RenderObject createRenderObject(BuildContext context) {
    return _RenderGestureVisualizer();
  }
}

class _RenderGestureVisualizer extends RenderProxyBox {
  /// key: pointer id, value: the info
  final _recordedPointerInfoMap = <int, _RecordedPointerInfo>{};

  @override
  void detach() {
    _recordedPointerInfoMap.clear();
    super.detach();
  }

  @override
  bool hitTest(BoxHitTestResult result, {required Offset position}) {
    if (!size.contains(position)) return false;
    // We always want to add a hit test entry for ourselves as we want to react
    // to each and every hit event.
    result.add(BoxHitTestEntry(this, position));
    return hitTestChildren(result, position: position);
  }

  @override
  void handleEvent(PointerEvent event, covariant HitTestEntry entry) {
    // We do not want to interfere in the gesture arena, which is why we are not
    // using regular tap recognizers. Instead, we handle it ourselves and always
    // react to the hit events (ignoring the gesture arena).

    // by experiment, sometimes we see PointerHoverEvent with pointer=0 strangely...
    if (event.pointer == 0) {
      return;
    }

    if (event is PointerUpEvent || event is PointerCancelEvent) {
      Future.delayed(_kRemainAfterPointerUp, () {
        _recordedPointerInfoMap.remove(event.pointer);
        markNeedsPaint();
      });
    } else {
      _recordedPointerInfoMap[event.pointer] = _RecordedPointerInfo(event.localPosition);
      markNeedsPaint();
    }
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    context.paintChild(child!, offset);

    final canvas = context.canvas;
    for (final info in _recordedPointerInfoMap.values) {
      final path = Path()..addOval(Rect.fromCircle(center: info.localPosition, radius: _kTapRadius));

      canvas.drawShadow(path, _kShadowColor, _kShadowElevation, true);
      canvas.drawPath(path, Paint()..color = _kTapColor);
    }
  }
}

class _RecordedPointerInfo {
  _RecordedPointerInfo(this.localPosition);

  final Offset localPosition;
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-10-23
    • 1970-01-01
    • 2020-12-04
    • 2021-02-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多