Flutter Driver源码分析
# 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
}
2
3
4
5
6
7
timeout
是等待Command运行完成的最大等待时间,默认是null,kind用来标记Command的类型,而requiresRootWidgetAttached
表示Command是否需要确保Widget树在运行前已经初始化完成。
# Result
Result和Command对应,表示Command的运行结果。
abstract class Result {
const Result();
static const Result empty = _EmptyResult();
Map<String, dynamic> toJson();
}
2
3
4
5
6
7
通过将构造函数加上const来保证子类也是const,通过重写toJson
方法,来将结果序列化。_EmptyResult
就是通过重写这个返回一个空map。
# finder
上面提到了command,那么command有一个同样是抽象类的子类CommandWithTarget
。
abstract class CommandWithTarget extends Command {
final SerializableFinder finder;
Map<String, String> serialize() =>
super.serialize()..addAll(finder.serialize());
}
2
3
4
5
6
7
8
它在command的基础上加了一个SerializableFinder
类型的finder属性。而SerializableFinder
长下面这样。
abstract class SerializableFinder {
const SerializableFinder();
String get finderType;
Map<String, String> serialize() => <String, String>{
'finderType': finderType,
};
}
2
3
4
5
6
7
8
9
10
11
它是flutter driver finders的基类,用于描述driver如何寻找元素。通过继承SerializableFinder
就可以实现许多特定的finder。flutter driver中的finder有如下几种:
- ByTooltipMessage 通过工具提示组件定位
- BySemanticsLabel 通过语义化标签
- ByText 通过文字定位
- ByValueKey 通过key进行定位
- ByType 通过组件类型
- 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');
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
此外还有一个CreateFinderFactory
的mixin,它用于从SerializableFinder
创建Finder
。而Finder
是flutter_test中的一个抽象类。额,为什么要这样做呢,再往下看看应该就明白了。
# 常用Command/CommandWithTarget
手势相关:
- Tap
- Scroll 参数有每次移动的dx和dy,duration以及frequency
- ScrollIntoView 滚动finder定位的widget的可滚动父组件,直到widget完全可见。
文本相关:
- GetText
- EnterText 这是一个Command,不包含finder属性
# CommandHandlerFactory
通过浏览Command相关的源码,可以看到Command只是一个定义,包含了一些属性,以及序列化和反序列化的方法,具体运行的操作,并没有包含其中。实际上运行的具体操作是在CommandHandlerFactory
这个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;
}
2
3
4
5
6
7
8
通过finderFactory创建flutter_test
中的Finder,传入waitForElement函数。
Future<Finder> waitForElement(Finder finder) async {
if (_frameSync)
await _waitUntilFrame(() => SchedulerBinding.instance!.transientCallbackCount == 0);
await _waitUntilFrame(() => finder.evaluate().isNotEmpty);
if (_frameSync)
await _waitUntilFrame(() => SchedulerBinding.instance!.transientCallbackCount == 0);
return finder;
}
2
3
4
5
6
7
8
9
10
11
waitForElement函数里面主要调用了_waitUntilFrame方法。
Future<void> _waitUntilFrame(bool Function() condition, [ Completer<void>? completer ]) {
completer ??= Completer<void>();
if (!condition()) {
SchedulerBinding.instance!.addPostFrameCallback((Duration timestamp) {
_waitUntilFrame(condition, completer);
});
} else {
completer.complete();
}
return completer.future;
}
2
3
4
5
6
7
8
9
10
11
这里通过使用Completer来生成一个Future,当bool闭包返回true将会完成这个Future,否则将会一直递归执行。
这一系列的操作都是为了确保能在当前Widget树中找到对应的控件。最后调用prober的tap方法来实现点击。有个很有意思的地方,我们前面看了flutter_driver里的手势相关的操作,只有tap和scroll,稍微基础一点的长按手势也没有支持,但是实际使用的是WidgetController
里的tap方法,可WidgetController
里是提供了longPress、drag等手势的实现。🤔Google一下,大多数人都是用scroll来验证longPress,感觉有一点点怪。此外WidgetController
是flutter_test
包的中的类,看来要想知道具体怎么实现模拟点击那些操作,得看看flutter_test
的源码。模拟长按的测试代码如下:
test('test button longpress', () async {
final SerializableFinder btn = find.byValueKey('button');
await driver.waitFor(btn);
await driver.scroll(btn, 0, 0, Duration(milliseconds: 500));
});
2
3
4
5
6
# FlutterDriver
FlutterDriver
是一个抽象类,它有两个具体的实现WebFlutterDriver
和VMServiceFlutterDriver
。以VMServiceFlutterDriver
为例进行分析。
# connectTo
通过创建VmService client来连接到flutter应用。并通过client获取到main isolate。而创建VmService的在_waitAndConnect
这个方法中。构造函数如下。
VmService VmService(
Stream<dynamic> inStream,
void Function(String) writeMessage, {
Log? log,
Future<dynamic> Function()? disposeHandler,
Future<dynamic>? streamClosed,
})
2
3
4
5
6
7
通过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
);
2
3
4
5
6
7
8
9
10
11
12
13
之后便是通过创建的VMService来发送指令。
final Future<Map<String, dynamic>> future = _serviceClient.callServiceExtension(
_flutterExtensionMethodName,
isolateId: _appIsolate.id,
args: serialized,
);
2
3
4
5
调用callServiceExtension
来调用特定服务的协议拓展,传入的args为序列化之后的Command信息。在调用这个方法之前,需要先在vmservice中注册相关的服务。这也就是为什么需要在main函数里enableFlutterDriverExtension()
。
# 注册服务
Flutter应用是运行在Dart VM上的,Dart VM内部提供了一套Web服务VMService,通过 JSON-RPC 2.0 (opens new window) 协议来访问Dart VM服务协议的库,使用VMService可以帮助我们获取app中的数据。Flutter中使用registerServiceExtension
方法来完成服务注册,之后我们就可以在app 外部通过VMService来调用对应的服务。
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));
});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
registerServiceExtension
就是注册方法,接受的入参就是服务名字 和 回调。
服务名字:就是 Flutter
和 Dart Vm
能够认识的服务标示,方法名字就是 VM 可以调用到的名字。
回调:就是 VM
调用服务名字时,Flutter 做出的反应
。
这里注意一点,我们传递的名字会被 包装成 ext.flutter.$名字
的形式。
注册会调用 developer
的 registerExtension
方法。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;
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);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
那么看代码可能有点迷惑。首先我们从它的父类入手。因为在调用子类的构造函数的时候,会调用父类的构造函数。下面是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();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
在构造函数中调用了initServiceExtensions
方法,这对应了_DriverBinding
中重写的initServiceExtensions
方法。并在此方法中进行服务注册,将FlutterDriverExtension
的call方法作为参数传入。在call方法中,根据params中的Command信息,调用handleCommand方法进行处理。而handleCommand方法是通过混入CommandHandlerFactory
得到的,并通过重写添加了对command的一些判断。
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);
}
2
3
4
5
6
7
8
9
这里的设计挺好的,可以借鉴一下,_commandExtensions
的存在提供了外部添加自定义Command的接口,也就是在FlutterDriverExtension
构造函数中的一个可选参数command,通过在此传入一些自定义的Command,可以实现自定义Command和对应处理方法的功能。
最后再提一嘴,在FlutterDriverExtension构造函数中还做了一件事,那就是registerTextInput
。有了它才可以输入框中进行输入操作。
# 总结一下
最后大概总结一下flutter_driver的原理。首先,在需要进行UI自动化测试的Flutter应用的main函数运行前,先向main isolate中注册我们的服务,也就是FlutterDriverExtension
中的call方法,用于处理Command。然后是通过websocket,创建vmservice的client,连接到我们的flutter应用。之后就是通过这个client来调用服务拓展,将Command传递过去。具体的操作都是flutter 应用结合flutter_test包来完成的。