• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2016 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:async';
6
7import 'package:flutter/semantics.dart';
8import 'package:meta/meta.dart';
9
10import 'package:flutter/cupertino.dart';
11import 'package:flutter/foundation.dart';
12import 'package:flutter/gestures.dart';
13import 'package:flutter/material.dart';
14import 'package:flutter/rendering.dart' show RendererBinding, SemanticsHandle;
15import 'package:flutter/scheduler.dart';
16import 'package:flutter/services.dart';
17import 'package:flutter/widgets.dart';
18import 'package:flutter_test/flutter_test.dart';
19
20import '../common/diagnostics_tree.dart';
21import '../common/error.dart';
22import '../common/find.dart';
23import '../common/frame_sync.dart';
24import '../common/geometry.dart';
25import '../common/gesture.dart';
26import '../common/health.dart';
27import '../common/message.dart';
28import '../common/render_tree.dart';
29import '../common/request_data.dart';
30import '../common/semantics.dart';
31import '../common/text.dart';
32
33const String _extensionMethodName = 'driver';
34const String _extensionMethod = 'ext.flutter.$_extensionMethodName';
35
36/// Signature for the handler passed to [enableFlutterDriverExtension].
37///
38/// Messages are described in string form and should return a [Future] which
39/// eventually completes to a string response.
40typedef DataHandler = Future<String> Function(String message);
41
42class _DriverBinding extends BindingBase with ServicesBinding, SchedulerBinding, GestureBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding {
43  _DriverBinding(this._handler, this._silenceErrors);
44
45  final DataHandler _handler;
46  final bool _silenceErrors;
47
48  @override
49  void initServiceExtensions() {
50    super.initServiceExtensions();
51    final FlutterDriverExtension extension = FlutterDriverExtension(_handler, _silenceErrors);
52    registerServiceExtension(
53      name: _extensionMethodName,
54      callback: extension.call,
55    );
56  }
57}
58
59/// Enables Flutter Driver VM service extension.
60///
61/// This extension is required for tests that use `package:flutter_driver` to
62/// drive applications from a separate process.
63///
64/// Call this function prior to running your application, e.g. before you call
65/// `runApp`.
66///
67/// Optionally you can pass a [DataHandler] callback. It will be called if the
68/// test calls [FlutterDriver.requestData].
69///
70/// `silenceErrors` will prevent exceptions from being logged. This is useful
71/// for tests where exceptions are expected. Defaults to false. Any errors
72/// will still be returned in the `response` field of the result json along
73/// with an `isError` boolean.
74void enableFlutterDriverExtension({ DataHandler handler, bool silenceErrors = false }) {
75  assert(WidgetsBinding.instance == null);
76  _DriverBinding(handler, silenceErrors);
77  assert(WidgetsBinding.instance is _DriverBinding);
78}
79
80/// Signature for functions that handle a command and return a result.
81typedef CommandHandlerCallback = Future<Result> Function(Command c);
82
83/// Signature for functions that deserialize a JSON map to a command object.
84typedef CommandDeserializerCallback = Command Function(Map<String, String> params);
85
86/// Signature for functions that run the given finder and return the [Element]
87/// found, if any, or null otherwise.
88typedef FinderConstructor = Finder Function(SerializableFinder finder);
89
90/// The class that manages communication between a Flutter Driver test and the
91/// application being remote-controlled, on the application side.
92///
93/// This is not normally used directly. It is instantiated automatically when
94/// calling [enableFlutterDriverExtension].
95@visibleForTesting
96class FlutterDriverExtension {
97  /// Creates an object to manage a Flutter Driver connection.
98  FlutterDriverExtension(this._requestDataHandler, this._silenceErrors) {
99    _testTextInput.register();
100
101    _commandHandlers.addAll(<String, CommandHandlerCallback>{
102      'get_health': _getHealth,
103      'get_render_tree': _getRenderTree,
104      'enter_text': _enterText,
105      'get_text': _getText,
106      'request_data': _requestData,
107      'scroll': _scroll,
108      'scrollIntoView': _scrollIntoView,
109      'set_frame_sync': _setFrameSync,
110      'set_semantics': _setSemantics,
111      'set_text_entry_emulation': _setTextEntryEmulation,
112      'tap': _tap,
113      'waitFor': _waitFor,
114      'waitForAbsent': _waitForAbsent,
115      'waitUntilNoTransientCallbacks': _waitUntilNoTransientCallbacks,
116      'waitUntilNoPendingFrame': _waitUntilNoPendingFrame,
117      'waitUntilFirstFrameRasterized': _waitUntilFirstFrameRasterized,
118      'get_semantics_id': _getSemanticsId,
119      'get_offset': _getOffset,
120      'get_diagnostics_tree': _getDiagnosticsTree,
121    });
122
123    _commandDeserializers.addAll(<String, CommandDeserializerCallback>{
124      'get_health': (Map<String, String> params) => GetHealth.deserialize(params),
125      'get_render_tree': (Map<String, String> params) => GetRenderTree.deserialize(params),
126      'enter_text': (Map<String, String> params) => EnterText.deserialize(params),
127      'get_text': (Map<String, String> params) => GetText.deserialize(params),
128      'request_data': (Map<String, String> params) => RequestData.deserialize(params),
129      'scroll': (Map<String, String> params) => Scroll.deserialize(params),
130      'scrollIntoView': (Map<String, String> params) => ScrollIntoView.deserialize(params),
131      'set_frame_sync': (Map<String, String> params) => SetFrameSync.deserialize(params),
132      'set_semantics': (Map<String, String> params) => SetSemantics.deserialize(params),
133      'set_text_entry_emulation': (Map<String, String> params) => SetTextEntryEmulation.deserialize(params),
134      'tap': (Map<String, String> params) => Tap.deserialize(params),
135      'waitFor': (Map<String, String> params) => WaitFor.deserialize(params),
136      'waitForAbsent': (Map<String, String> params) => WaitForAbsent.deserialize(params),
137      'waitUntilNoTransientCallbacks': (Map<String, String> params) => WaitUntilNoTransientCallbacks.deserialize(params),
138      'waitUntilNoPendingFrame': (Map<String, String> params) => WaitUntilNoPendingFrame.deserialize(params),
139      'waitUntilFirstFrameRasterized': (Map<String, String> params) => WaitUntilFirstFrameRasterized.deserialize(params),
140      'get_semantics_id': (Map<String, String> params) => GetSemanticsId.deserialize(params),
141      'get_offset': (Map<String, String> params) => GetOffset.deserialize(params),
142      'get_diagnostics_tree': (Map<String, String> params) => GetDiagnosticsTree.deserialize(params),
143    });
144
145    _finders.addAll(<String, FinderConstructor>{
146      'ByText': (SerializableFinder finder) => _createByTextFinder(finder),
147      'ByTooltipMessage': (SerializableFinder finder) => _createByTooltipMessageFinder(finder),
148      'BySemanticsLabel': (SerializableFinder finder) => _createBySemanticsLabelFinder(finder),
149      'ByValueKey': (SerializableFinder finder) => _createByValueKeyFinder(finder),
150      'ByType': (SerializableFinder finder) => _createByTypeFinder(finder),
151      'PageBack': (SerializableFinder finder) => _createPageBackFinder(),
152      'Ancestor': (SerializableFinder finder) => _createAncestorFinder(finder),
153      'Descendant': (SerializableFinder finder) => _createDescendantFinder(finder),
154    });
155  }
156
157  final TestTextInput _testTextInput = TestTextInput();
158
159  final DataHandler _requestDataHandler;
160  final bool _silenceErrors;
161
162  static final Logger _log = Logger('FlutterDriverExtension');
163
164  final WidgetController _prober = LiveWidgetController(WidgetsBinding.instance);
165  final Map<String, CommandHandlerCallback> _commandHandlers = <String, CommandHandlerCallback>{};
166  final Map<String, CommandDeserializerCallback> _commandDeserializers = <String, CommandDeserializerCallback>{};
167  final Map<String, FinderConstructor> _finders = <String, FinderConstructor>{};
168
169  /// With [_frameSync] enabled, Flutter Driver will wait to perform an action
170  /// until there are no pending frames in the app under test.
171  bool _frameSync = true;
172
173  /// Processes a driver command configured by [params] and returns a result
174  /// as an arbitrary JSON object.
175  ///
176  /// [params] must contain key "command" whose value is a string that
177  /// identifies the kind of the command and its corresponding
178  /// [CommandDeserializerCallback]. Other keys and values are specific to the
179  /// concrete implementation of [Command] and [CommandDeserializerCallback].
180  ///
181  /// The returned JSON is command specific. Generally the caller deserializes
182  /// the result into a subclass of [Result], but that's not strictly required.
183  @visibleForTesting
184  Future<Map<String, dynamic>> call(Map<String, String> params) async {
185    final String commandKind = params['command'];
186    try {
187      final CommandHandlerCallback commandHandler = _commandHandlers[commandKind];
188      final CommandDeserializerCallback commandDeserializer =
189          _commandDeserializers[commandKind];
190      if (commandHandler == null || commandDeserializer == null)
191        throw 'Extension $_extensionMethod does not support command $commandKind';
192      final Command command = commandDeserializer(params);
193      assert(WidgetsBinding.instance.isRootWidgetAttached || !command.requiresRootWidgetAttached,
194          'No root widget is attached; have you remembered to call runApp()?');
195      Future<Result> responseFuture = commandHandler(command);
196      if (command.timeout != null)
197        responseFuture = responseFuture.timeout(command.timeout);
198      final Result response = await responseFuture;
199      return _makeResponse(response?.toJson());
200    } on TimeoutException catch (error, stackTrace) {
201      final String msg = 'Timeout while executing $commandKind: $error\n$stackTrace';
202      _log.error(msg);
203      return _makeResponse(msg, isError: true);
204    } catch (error, stackTrace) {
205      final String msg = 'Uncaught extension error while executing $commandKind: $error\n$stackTrace';
206      if (!_silenceErrors)
207        _log.error(msg);
208      return _makeResponse(msg, isError: true);
209    }
210  }
211
212  Map<String, dynamic> _makeResponse(dynamic response, { bool isError = false }) {
213    return <String, dynamic>{
214      'isError': isError,
215      'response': response,
216    };
217  }
218
219  Future<Health> _getHealth(Command command) async => const Health(HealthStatus.ok);
220
221  Future<RenderTree> _getRenderTree(Command command) async {
222    return RenderTree(RendererBinding.instance?.renderView?.toStringDeep());
223  }
224
225  // This can be used to wait for the first frame being rasterized during app launch.
226  Future<Result> _waitUntilFirstFrameRasterized(Command command) async {
227    await WidgetsBinding.instance.waitUntilFirstFrameRasterized;
228    return null;
229  }
230
231  // Waits until at the end of a frame the provided [condition] is [true].
232  Future<void> _waitUntilFrame(bool condition(), [ Completer<void> completer ]) {
233    completer ??= Completer<void>();
234    if (!condition()) {
235      SchedulerBinding.instance.addPostFrameCallback((Duration timestamp) {
236        _waitUntilFrame(condition, completer);
237      });
238    } else {
239      completer.complete();
240    }
241    return completer.future;
242  }
243
244  /// Runs `finder` repeatedly until it finds one or more [Element]s.
245  Future<Finder> _waitForElement(Finder finder) async {
246    if (_frameSync)
247      await _waitUntilFrame(() => SchedulerBinding.instance.transientCallbackCount == 0);
248
249    await _waitUntilFrame(() => finder.evaluate().isNotEmpty);
250
251    if (_frameSync)
252      await _waitUntilFrame(() => SchedulerBinding.instance.transientCallbackCount == 0);
253
254    return finder;
255  }
256
257  /// Runs `finder` repeatedly until it finds zero [Element]s.
258  Future<Finder> _waitForAbsentElement(Finder finder) async {
259    if (_frameSync)
260      await _waitUntilFrame(() => SchedulerBinding.instance.transientCallbackCount == 0);
261
262    await _waitUntilFrame(() => finder.evaluate().isEmpty);
263
264    if (_frameSync)
265      await _waitUntilFrame(() => SchedulerBinding.instance.transientCallbackCount == 0);
266
267    return finder;
268  }
269
270  Finder _createByTextFinder(ByText arguments) {
271    return find.text(arguments.text);
272  }
273
274  Finder _createByTooltipMessageFinder(ByTooltipMessage arguments) {
275    return find.byElementPredicate((Element element) {
276      final Widget widget = element.widget;
277      if (widget is Tooltip)
278        return widget.message == arguments.text;
279      return false;
280    }, description: 'widget with text tooltip "${arguments.text}"');
281  }
282
283  Finder _createBySemanticsLabelFinder(BySemanticsLabel arguments) {
284    return find.byElementPredicate((Element element) {
285      if (element is! RenderObjectElement) {
286        return false;
287      }
288      final String semanticsLabel = element.renderObject?.debugSemantics?.label;
289      if (semanticsLabel == null) {
290        return false;
291      }
292      final Pattern label = arguments.label;
293      return label is RegExp
294          ? label.hasMatch(semanticsLabel)
295          : label == semanticsLabel;
296    }, description: 'widget with semantic label "${arguments.label}"');
297  }
298
299  Finder _createByValueKeyFinder(ByValueKey arguments) {
300    switch (arguments.keyValueType) {
301      case 'int':
302        return find.byKey(ValueKey<int>(arguments.keyValue));
303      case 'String':
304        return find.byKey(ValueKey<String>(arguments.keyValue));
305      default:
306        throw 'Unsupported ByValueKey type: ${arguments.keyValueType}';
307    }
308  }
309
310  Finder _createByTypeFinder(ByType arguments) {
311    return find.byElementPredicate((Element element) {
312      return element.widget.runtimeType.toString() == arguments.type;
313    }, description: 'widget with runtimeType "${arguments.type}"');
314  }
315
316  Finder _createPageBackFinder() {
317    return find.byElementPredicate((Element element) {
318      final Widget widget = element.widget;
319      if (widget is Tooltip)
320        return widget.message == 'Back';
321      if (widget is CupertinoNavigationBarBackButton)
322        return true;
323      return false;
324    }, description: 'Material or Cupertino back button');
325  }
326
327  Finder _createAncestorFinder(Ancestor arguments) {
328    return find.ancestor(
329      of: _createFinder(arguments.of),
330      matching: _createFinder(arguments.matching),
331      matchRoot: arguments.matchRoot,
332    );
333  }
334
335  Finder _createDescendantFinder(Descendant arguments) {
336    return find.descendant(
337      of: _createFinder(arguments.of),
338      matching: _createFinder(arguments.matching),
339      matchRoot: arguments.matchRoot,
340    );
341  }
342
343  Finder _createFinder(SerializableFinder finder) {
344    final FinderConstructor constructor = _finders[finder.finderType];
345
346    if (constructor == null)
347      throw 'Unsupported finder type: ${finder.finderType}';
348
349    return constructor(finder);
350  }
351
352  Future<TapResult> _tap(Command command) async {
353    final Tap tapCommand = command;
354    final Finder computedFinder = await _waitForElement(
355      _createFinder(tapCommand.finder).hitTestable()
356    );
357    await _prober.tap(computedFinder);
358    return const TapResult();
359  }
360
361  Future<WaitForResult> _waitFor(Command command) async {
362    final WaitFor waitForCommand = command;
363    await _waitForElement(_createFinder(waitForCommand.finder));
364    return const WaitForResult();
365  }
366
367  Future<WaitForAbsentResult> _waitForAbsent(Command command) async {
368    final WaitForAbsent waitForAbsentCommand = command;
369    await _waitForAbsentElement(_createFinder(waitForAbsentCommand.finder));
370    return const WaitForAbsentResult();
371  }
372
373  Future<Result> _waitUntilNoTransientCallbacks(Command command) async {
374    if (SchedulerBinding.instance.transientCallbackCount != 0)
375      await _waitUntilFrame(() => SchedulerBinding.instance.transientCallbackCount == 0);
376    return null;
377  }
378
379  /// Returns a future that waits until no pending frame is scheduled (frame is synced).
380  ///
381  /// Specifically, it checks:
382  /// * Whether the count of transient callbacks is zero.
383  /// * Whether there's no pending request for scheduling a new frame.
384  ///
385  /// We consider the frame is synced when both conditions are met.
386  ///
387  /// This method relies on a Flutter Driver mechanism called "frame sync",
388  /// which waits for transient animations to finish. Persistent animations will
389  /// cause this to wait forever.
390  ///
391  /// If a test needs to interact with the app while animations are running, it
392  /// should avoid this method and instead disable the frame sync using
393  /// `set_frame_sync` method. See [FlutterDriver.runUnsynchronized] for more
394  /// details on how to do this. Note, disabling frame sync will require the
395  /// test author to use some other method to avoid flakiness.
396  Future<Result> _waitUntilNoPendingFrame(Command command) async {
397    await _waitUntilFrame(() {
398      return SchedulerBinding.instance.transientCallbackCount == 0
399          && !SchedulerBinding.instance.hasScheduledFrame;
400    });
401    return null;
402  }
403
404  Future<GetSemanticsIdResult> _getSemanticsId(Command command) async {
405    final GetSemanticsId semanticsCommand = command;
406    final Finder target = await _waitForElement(_createFinder(semanticsCommand.finder));
407    final Element element = target.evaluate().single;
408    RenderObject renderObject = element.renderObject;
409    SemanticsNode node;
410    while (renderObject != null && node == null) {
411      node = renderObject.debugSemantics;
412      renderObject = renderObject.parent;
413    }
414    if (node == null)
415      throw StateError('No semantics data found');
416    return GetSemanticsIdResult(node.id);
417  }
418
419  Future<GetOffsetResult> _getOffset(Command command) async {
420    final GetOffset getOffsetCommand = command;
421    final Finder finder = await _waitForElement(_createFinder(getOffsetCommand.finder));
422    final Element element = finder.evaluate().single;
423    final RenderBox box = element.renderObject;
424    Offset localPoint;
425    switch (getOffsetCommand.offsetType) {
426      case OffsetType.topLeft:
427        localPoint = Offset.zero;
428        break;
429      case OffsetType.topRight:
430        localPoint = box.size.topRight(Offset.zero);
431        break;
432      case OffsetType.bottomLeft:
433        localPoint = box.size.bottomLeft(Offset.zero);
434        break;
435      case OffsetType.bottomRight:
436        localPoint = box.size.bottomRight(Offset.zero);
437        break;
438      case OffsetType.center:
439        localPoint = box.size.center(Offset.zero);
440        break;
441    }
442    final Offset globalPoint = box.localToGlobal(localPoint);
443    return GetOffsetResult(dx: globalPoint.dx, dy: globalPoint.dy);
444  }
445
446  Future<DiagnosticsTreeResult> _getDiagnosticsTree(Command command) async {
447    final GetDiagnosticsTree diagnosticsCommand = command;
448    final Finder finder = await _waitForElement(_createFinder(diagnosticsCommand.finder));
449    final Element element = finder.evaluate().single;
450    DiagnosticsNode diagnosticsNode;
451    switch (diagnosticsCommand.diagnosticsType) {
452      case DiagnosticsType.renderObject:
453        diagnosticsNode = element.renderObject.toDiagnosticsNode();
454        break;
455      case DiagnosticsType.widget:
456        diagnosticsNode = element.toDiagnosticsNode();
457        break;
458    }
459    return DiagnosticsTreeResult(diagnosticsNode.toJsonMap(DiagnosticsSerializationDelegate(
460      subtreeDepth: diagnosticsCommand.subtreeDepth,
461      includeProperties: diagnosticsCommand.includeProperties,
462    )));
463  }
464
465  Future<ScrollResult> _scroll(Command command) async {
466    final Scroll scrollCommand = command;
467    final Finder target = await _waitForElement(_createFinder(scrollCommand.finder));
468    final int totalMoves = scrollCommand.duration.inMicroseconds * scrollCommand.frequency ~/ Duration.microsecondsPerSecond;
469    final Offset delta = Offset(scrollCommand.dx, scrollCommand.dy) / totalMoves.toDouble();
470    final Duration pause = scrollCommand.duration ~/ totalMoves;
471    final Offset startLocation = _prober.getCenter(target);
472    Offset currentLocation = startLocation;
473    final TestPointer pointer = TestPointer(1);
474    final HitTestResult hitTest = HitTestResult();
475
476    _prober.binding.hitTest(hitTest, startLocation);
477    _prober.binding.dispatchEvent(pointer.down(startLocation), hitTest);
478    await Future<void>.value(); // so that down and move don't happen in the same microtask
479    for (int moves = 0; moves < totalMoves; moves += 1) {
480      currentLocation = currentLocation + delta;
481      _prober.binding.dispatchEvent(pointer.move(currentLocation), hitTest);
482      await Future<void>.delayed(pause);
483    }
484    _prober.binding.dispatchEvent(pointer.up(), hitTest);
485
486    return const ScrollResult();
487  }
488
489  Future<ScrollResult> _scrollIntoView(Command command) async {
490    final ScrollIntoView scrollIntoViewCommand = command;
491    final Finder target = await _waitForElement(_createFinder(scrollIntoViewCommand.finder));
492    await Scrollable.ensureVisible(target.evaluate().single, duration: const Duration(milliseconds: 100), alignment: scrollIntoViewCommand.alignment ?? 0.0);
493    return const ScrollResult();
494  }
495
496  Future<GetTextResult> _getText(Command command) async {
497    final GetText getTextCommand = command;
498    final Finder target = await _waitForElement(_createFinder(getTextCommand.finder));
499    // TODO(yjbanov): support more ways to read text
500    final Text text = target.evaluate().single.widget;
501    return GetTextResult(text.data);
502  }
503
504  Future<SetTextEntryEmulationResult> _setTextEntryEmulation(Command command) async {
505    final SetTextEntryEmulation setTextEntryEmulationCommand = command;
506    if (setTextEntryEmulationCommand.enabled) {
507      _testTextInput.register();
508    } else {
509      _testTextInput.unregister();
510    }
511    return const SetTextEntryEmulationResult();
512  }
513
514  Future<EnterTextResult> _enterText(Command command) async {
515    if (!_testTextInput.isRegistered) {
516      throw 'Unable to fulfill `FlutterDriver.enterText`. Text emulation is '
517            'disabled. You can enable it using `FlutterDriver.setTextEntryEmulation`.';
518    }
519    final EnterText enterTextCommand = command;
520    _testTextInput.enterText(enterTextCommand.text);
521    return const EnterTextResult();
522  }
523
524  Future<RequestDataResult> _requestData(Command command) async {
525    final RequestData requestDataCommand = command;
526    return RequestDataResult(_requestDataHandler == null ? 'No requestData Extension registered' : await _requestDataHandler(requestDataCommand.message));
527  }
528
529  Future<SetFrameSyncResult> _setFrameSync(Command command) async {
530    final SetFrameSync setFrameSyncCommand = command;
531    _frameSync = setFrameSyncCommand.enabled;
532    return const SetFrameSyncResult();
533  }
534
535  SemanticsHandle _semantics;
536  bool get _semanticsIsEnabled => RendererBinding.instance.pipelineOwner.semanticsOwner != null;
537
538  Future<SetSemanticsResult> _setSemantics(Command command) async {
539    final SetSemantics setSemanticsCommand = command;
540    final bool semanticsWasEnabled = _semanticsIsEnabled;
541    if (setSemanticsCommand.enabled && _semantics == null) {
542      _semantics = RendererBinding.instance.pipelineOwner.ensureSemantics();
543      if (!semanticsWasEnabled) {
544        // wait for the first frame where semantics is enabled.
545        final Completer<void> completer = Completer<void>();
546        SchedulerBinding.instance.addPostFrameCallback((Duration d) {
547          completer.complete();
548        });
549        await completer.future;
550      }
551    } else if (!setSemanticsCommand.enabled && _semantics != null) {
552      _semantics.dispose();
553      _semantics = null;
554    }
555    return SetSemanticsResult(semanticsWasEnabled != _semanticsIsEnabled);
556  }
557}
558