Command

首先是最基础的一个概念Command。它定义在lib/src/common/message.dart中。

abstract class Command {
  final Duration? timeout;
  String get kind;
  bool get requiresRootWidgetAttached => true;
  const Command({ this.timeout });
  // serialize deserialize
}

timeout是等待command运行完成的最大等待时间,默认是nulgooglel,kind用来标记command的类型,而requiresRoo手势识别tW源码精灵永久兑换码idgetAttached表示Command是否需要确保Widget树在运行前已经初始手势舞教学视频简单化完成。龚俊

Result

Result和Command对应,表示Command的运行结果。

abstract class Result {
  const Result();
  static const Result empty = _EmptyResult();
  Map<String, dynamic> toJson();
}

通过将构造函数加上const来保证子类也是接口const,通过重写toJson方法,来将结果序列化。_EmptyResult就是通过重写这个返回一个空map。

finder

上面提到了command,那么comma源码之家nd有一个同样是抽象类的子类CommandWithTarget

abstract class CommandWithTarget extends Command {
  final SerializableFinder finder;
  @override
  Map<String, String> serialize() =>
      super.serialize()..addAll(finder.serialize());
}

它在command的基础上加了一个SerializableFinder类型的finder属性。而SerializableFinde手势舞视频r长下面这样。

abstract class SerializableFinder {
  const SerializableFinder();
  String get finderType;
  @mustCallSuper
  Map<String, String> serialize() => <String, String>{
    'finderType': finderType,
  };
}

它是flutter driver finders的基类,用于描述driver如何寻找元素。通过继承Serializab宫颈癌leFinder就可以实现许多特定的finder。flutter driver中的finder有如下几种:

  • ByToo工龄越长退休金越多吗ltipMessage 通过工具提示组件定位
  • BySgoogleemanticsLabel 通过语义化标签
  • ByText 通过文字定位
  • ByValueKey 通过key进行定位
  • B手势舞yType 通过组件类型
  • PageBack 寻找Material或者Cupertino scaffold上的返回按钮
  • Descendant 通过子组件定位源码精灵永久兑换码
  • Ancestor 通过父组件定位

当然这些finder仅仅是包含了一些定义,和序列化反序列化的方法,但是并没有具体的查找操作。此外,对于finder的构建,有一个专门的工厂mixin DeserializeFinderFactory。通过finderType来反序列化生成对应的finder。

mixin DeserializeFinderFactory {
  /// Deserializes the finder from JSON generated by [SerializableFinder.serialize].
  SerializableFinder deserializeFinder(Map<String, String> json) {
    final String? finderType = json['finderType'];
    switch (finderType) {
      case 'ByType': return ByType.deserialize(json);
      case 'ByValueKey': return ByValueKey.deserialize(json);
      case 'ByTooltipMessage': return ByTooltipMessage.deserialize(json);
      case 'BySemanticsLabel': return BySemanticsLabel.deserialize(json);
      case 'ByText': return ByText.deserialize(json);
      case 'PageBack': return const PageBack();
      case 'Descendant': return Descendant.deserialize(json, this);
      case 'Ancestor': return Ancestor.deserialize(json, this);
    }
    throw DriverError('Unsupported search specification type $finderType');
  }
}

此外还有一个CreateFinderFactory的mixingoogle翻译,它用于从Sergoogle中国iaGooglelizableFinder创建Finder。而Finder是flutter_test中的一个抽象类。额,为接口测试用例设计什么要这样做呢,再往下看看应该就明白了。

常用Command/CommandWithTarget

手势相关:

  • Tap
  • Scroll 参数有每次移动的dx和dy,duration以及frequency
  • ScrollIntoView 滚动手势舞finder定位的widget的可滚动父组件,直到widget完全可见。google网站登录入口

文本相关:

  • GetText
  • Entgoogle网站登录入口erText 这是一个Command,不包含findergoogle翻译属性

CommandHandlerFactorgoogle中国y

通过浏览command相关的源码,可以看到command只是一个定义,包含了一些属性,以及序列化手势和反序列化的方法,具体运行的操作,并没有包含其中。实际上运行的具体操作是在CommandH龚俊andlerFactory这个mixin中定义的。

可以简单看一下tap的实现。

Future<Result> _tap(Command command, WidgetController prober, CreateFinderFactoryfinderFactory) async {
  final Tap tapCommand = command as Tap;
  final Finder computedFinder = await waitForElement(
    finderFactory.createFinder(tapCommand.finder).hitTestable(),
  );
  await prober.tap(computedFinder);
  return Result.empty;
}

调用probe宫颈癌r的tap方法源码网站来实现点击。有个很有意思的工资超过5000怎么扣税地方,我们前面看了flutter_driver里的手势相关的操作,只有tap和scroll,稍微基google础一点的长按手势也没有支持,但是实际使用的是WidgetController里的tap方手势密码法,可WidgetController里是提供了longPress、drag等手势的实现。Google一下,大多数人都是用scroll来验证longPress,感觉有一点点怪。此源码时代WidgetControllerflutter_test包的中的类,看来要想知道具体怎么实现模拟点击那些操作,得看看flutter_testgoogle的源码。模拟长按的测试代码如下:

test('test button longpress', () async {
  final SerializableFinder btn = find.byValueKey('button');
  await driver.waitFor(btn);
  await driver.scroll(btn, 0, 0, Duration(milliseconds: 500));
  });

FlutterDri手势识别ver

FlutterDriver是一个抽象类,它有两个具体的实现WebFlutterDriverVMServiceFlutterDrive手势变化r。以VMServiceFlutterDriver为例进行分析。

connectTo

通过创建VmService client来连接到flutter应用。并通过client获取到main isolate。而创建Vm源码编辑器下载Service的在_waitAndConnect这个方法中。构造函数如下。

VmService VmService(
  Stream<dynamic> inStream,
  void Function(String) writeMessage, {
  Log? log,
  Future<dynamic> Function()? disposeHandler,
  Future<dynamic>? streamClosed,
})

通过WebSocket创建stream。

socket = await WebSocket.connect(webSocketUrl, headers: headers);
final StreamController<dynamic> controller = StreamController<dynamic>();
final Completer<void> streamClosedCompleter = Completer<void>();
socket.listen(
  (dynamic data) => controller.add(data),
  onDone: () => streamClosedCompleter.complete(),
);
final vms.VmService service = vms.VmService(
  controller.stream,
  socket.add,
  disposeHandler: () => socket!.close(),
  streamClosed: streamClosedCompleter.future
);

之后便是通过创建的VMService来发送指令。

final Future<Map<String, dynamic>> future = _serviceClient.callServiceExtension(
        _flutterExtensionMethodName,
        isolateId: _appIsolate.id,
        args: serialized,
);

调用callServiceExtension来调用特定服务的协议接口英文拓展,传入的args为序列化之后的command信息。在调用这个方法之前,需要先在vmservice中注册相关的服务。这也就是为什么需要在main函数里enableFlutterDriverExtension()

注册服接口英文

Flutter应用是运行在Dar龚俊t VM上的,Dart VM内部提供了一套Web服务VMServ接口和抽象类的区别ice,通过 JSON-RPC 2.0 协议来访问Dart VM服务协议的库,使用VMService可以帮助我们获取app中的数据。Flutte手势舞教程视频慢动作r中使用registerServicGoogleeExtension方法来完成服务注册,之后我们就可以在app 外公司让员工下班发手机电量截图部通过VMS源码编辑器下载ervice来调用对应的服务。

void registerServiceExtension({
  required String name,
  required ServiceExtensionCallback callback,
}) {
  final String methodName = 'ext.flutter.$name';
  developer.registerExtension(methodName, (String method, Map<String, String> parameters) async {
    // 代码省略
    late Map<String, dynamic> result;
    try {
      result = await callback(parameters);
    } catch (exception, stack) {
    }
    result['type'] = '_extensionType';
    result['method'] = method;
    return developer.ServiceExtensionResponse.result(json.encode(result)); 
  });
}

registerServiceExtension 就是注册方法,接受的入参就是服务名字回调

服务名字:就是 FlutterDart Vm 能够认识的服务标示,方法名字就是 VM 可以调用到的名字。

回调:就是 VM 调用服务名字时,Flutter 做出的google翻译反应

这里注意一点,我们传递的名字会被 包装成 ex源码交易平台t.flutter.$名字 的形式。

注册会调用 developerregisterExtension 方法。developer 是一个开发者包,里面有一个比较基础的 API。最后调用的是native的一个方法。

external _registerExtension(String method, ServiceExtensionHandler handler);

_DriverBinding

flutter_driver里注册服务是在enableFlutterDriverExtension函数里完成的,里面调用了_DriverBinding的构造函数。

class _DriverBinding extends BindingBase with SchedulerBinding, ServicesBinding, GestureBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding, TestDefaultBinaryMessengerBinding {
  _DriverBinding(this._handler, this._silenceErrors, this._enableTextEntryEmulation, this.finders, this.commands);
  final DataHandler? _handler;
  final bool _silenceErrors;
  final bool _enableTextEntryEmulation;
  final List<FinderExtension>? finders;
  final List<CommandExtension>? commands;
  @override
  void initServiceExtensions() {
    super.initServiceExtensions();
    final FlutterDriverExtension extension = FlutterDriverExtension(_handler, _silenceErrors, _enableTextEntryEmulation, finders: finders ?? const <FinderExtension>[], commands: commands ?? const <CommandExtension>[]);
    registerServiceExtension(
      name: _extensionMethodName,
      callback: extension.call,
    );
    if (kIsWeb) {
      registerWebServiceExtension(extension.call);
    }
  }
}

那么看代码可能有点迷惑。首先我们从它的父类入手。因为在调用子类的构造函数的时候,会调用父类的构造函数。下面是BindingBase的构造函数,里面

BindingBase() {
  developer.Timeline.startSync('Framework initialization');
  assert(!_debugInitialized);
  initInstances();
  assert(_debugInitialized);
  assert(!_debugServiceExtensionsRegistered);
  initServiceExtensions();
  assert(_debugServiceExtensionsRegistered);
  developer.postEvent('Flutter.FrameworkInitialization', <String, String>{});
  developer.Timeline.finishSync();
}

在构造函数中调用了initServiceExtensions方法,这对应了_DriverBi手势变化nding中重写的initServiceExtensions手势语法。并在此方法中进行服务注册,将FlutterDriverExtension的call方法作为源码精灵永久兑换码参数传入。在call源码交易平台方法中,根据params中的command信息,调用handleCommand方法进行处理。而han手势识别dleCommand接口卡方法是通过混入CommandHandlerFactorygoogle到的,并通过重写添加了对command的一些判断。

  @override
  Future<Result> handleCommand(Command command, WidgetController prober, CreateFinderFactory finderFactory) {
    final String kind = command.kind;
    if (_commandExtensions.containsKey(kind)) {
      return _commandExtensions[kind]!.call(command, prober, finderFactory, this);
    }
    return super.handleCommand(command, prober, finderFactory);
  }

这里的设计挺好的,可以借鉴一下,_commandExtensions的存在提供了外部添加自定义Command的google接口,也就是在FlutterDriverExtension构造函数中的一个可选参数command,通过在此传入一些自定义的Command,可以实现自定义Command和对应处理方法的功能。

最后再提一源码编辑器下载嘴,在Flutte接口测试rDriverExtension构造函数中还做了一件事,那就是registerTextInput。有了它才可以输入框中google网站登录入口进行输入操作。

总结一下

最后大概总结一下flutter_driver的原理。首先,在需要进行UI自动化测试的Fluttegoogleplayr应用的main函数运行前,先向main isolate中注册我们的服务,也就是FlutterDriverExtension中的call方法,用于处理command。然后是通过websocket,创建vmservice的c接口自动化lient,连公司让员工下班发手机电量截图接到我们的flutter应用。之后就是通接口crc错误计数过这个client来调用服务拓展,将command传递过去。具体的操作都是flutter 应用结合flutter_test包来完成的。

参考文章

  • Flutter 必知必会系列 —— mixin 和 BindingBase 的巧妙配合
  • Da源码编辑器r接口crc错误计数t VM Service 实战