extension.dart 25.5 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';
6

7
import 'package:meta/meta.dart';
8
import 'package:flutter/cupertino.dart';
9
import 'package:flutter/foundation.dart';
10
import 'package:flutter/gestures.dart';
11
import 'package:flutter/material.dart';
12
import 'package:flutter/rendering.dart' show RendererBinding, SemanticsHandle;
13
import 'package:flutter/scheduler.dart';
14
import 'package:flutter/semantics.dart';
15
import 'package:flutter/services.dart';
16
import 'package:flutter/widgets.dart';
17
import 'package:flutter_test/flutter_test.dart';
18

19
import '../common/diagnostics_tree.dart';
20 21 22
import '../common/error.dart';
import '../common/find.dart';
import '../common/frame_sync.dart';
23
import '../common/geometry.dart';
24 25
import '../common/gesture.dart';
import '../common/health.dart';
26
import '../common/layer_tree.dart';
27 28 29 30
import '../common/message.dart';
import '../common/render_tree.dart';
import '../common/request_data.dart';
import '../common/semantics.dart';
31
import '../common/text.dart';
32
import '../common/wait.dart';
33
import '_io_extension.dart' if (dart.library.html) '_web_extension.dart';
34
import 'wait_conditions.dart';
35

36 37
const String _extensionMethodName = 'driver';
const String _extensionMethod = 'ext.flutter.$_extensionMethodName';
38

39 40 41 42
/// Signature for the handler passed to [enableFlutterDriverExtension].
///
/// Messages are described in string form and should return a [Future] which
/// eventually completes to a string response.
43
typedef DataHandler = Future<String> Function(String message);
44

45
class _DriverBinding extends BindingBase with ServicesBinding, SchedulerBinding, GestureBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding {
46
  _DriverBinding(this._handler, this._silenceErrors);
47 48

  final DataHandler _handler;
49
  final bool _silenceErrors;
50

51 52 53
  @override
  void initServiceExtensions() {
    super.initServiceExtensions();
54
    final FlutterDriverExtension extension = FlutterDriverExtension(_handler, _silenceErrors);
55 56
    registerServiceExtension(
      name: _extensionMethodName,
57
      callback: extension.call,
58
    );
59 60 61
    if (kIsWeb) {
      registerWebServiceExtension(extension.call);
    }
62
  }
63 64 65 66 67

  @override
  BinaryMessenger createBinaryMessenger() {
    return TestDefaultBinaryMessenger(super.createBinaryMessenger());
  }
68
}
69 70 71 72 73 74 75 76

/// Enables Flutter Driver VM service extension.
///
/// This extension is required for tests that use `package:flutter_driver` to
/// drive applications from a separate process.
///
/// Call this function prior to running your application, e.g. before you call
/// `runApp`.
77 78 79
///
/// Optionally you can pass a [DataHandler] callback. It will be called if the
/// test calls [FlutterDriver.requestData].
80
///
Kent Boogaart's avatar
Kent Boogaart committed
81
/// `silenceErrors` will prevent exceptions from being logged. This is useful
82
/// for tests where exceptions are expected. Defaults to false. Any errors
83
/// will still be returned in the `response` field of the result JSON along
84 85
/// with an `isError` boolean.
void enableFlutterDriverExtension({ DataHandler handler, bool silenceErrors = false }) {
86
  assert(WidgetsBinding.instance == null);
87
  _DriverBinding(handler, silenceErrors);
88
  assert(WidgetsBinding.instance is _DriverBinding);
89 90
}

91
/// Signature for functions that handle a command and return a result.
92
typedef CommandHandlerCallback = Future<Result> Function(Command c);
93

94
/// Signature for functions that deserialize a JSON map to a command object.
95
typedef CommandDeserializerCallback = Command Function(Map<String, String> params);
96

97 98
/// Signature for functions that run the given finder and return the [Element]
/// found, if any, or null otherwise.
99
typedef FinderConstructor = Finder Function(SerializableFinder finder);
100

101 102 103 104 105
/// The class that manages communication between a Flutter Driver test and the
/// application being remote-controlled, on the application side.
///
/// This is not normally used directly. It is instantiated automatically when
/// calling [enableFlutterDriverExtension].
106 107
@visibleForTesting
class FlutterDriverExtension {
108
  /// Creates an object to manage a Flutter Driver connection.
109
  FlutterDriverExtension(this._requestDataHandler, this._silenceErrors) {
110 111
    _testTextInput.register();

112
    _commandHandlers.addAll(<String, CommandHandlerCallback>{
113
      'get_health': _getHealth,
114
      'get_layer_tree': _getLayerTree,
115
      'get_render_tree': _getRenderTree,
116
      'enter_text': _enterText,
117
      'get_text': _getText,
118
      'request_data': _requestData,
119 120
      'scroll': _scroll,
      'scrollIntoView': _scrollIntoView,
121 122
      'set_frame_sync': _setFrameSync,
      'set_semantics': _setSemantics,
123
      'set_text_entry_emulation': _setTextEntryEmulation,
124
      'tap': _tap,
125
      'waitFor': _waitFor,
126
      'waitForAbsent': _waitForAbsent,
127 128 129 130
      'waitForCondition': _waitForCondition,
      'waitUntilNoTransientCallbacks': _waitUntilNoTransientCallbacks, // ignore: deprecated_member_use_from_same_package
      'waitUntilNoPendingFrame': _waitUntilNoPendingFrame, // ignore: deprecated_member_use_from_same_package
      'waitUntilFirstFrameRasterized': _waitUntilFirstFrameRasterized, // ignore: deprecated_member_use_from_same_package
131
      'get_semantics_id': _getSemanticsId,
132
      'get_offset': _getOffset,
133
      'get_diagnostics_tree': _getDiagnosticsTree,
134
    });
135

136
    _commandDeserializers.addAll(<String, CommandDeserializerCallback>{
137
      'get_health': (Map<String, String> params) => GetHealth.deserialize(params),
138
      'get_layer_tree': (Map<String, String> params) => GetLayerTree.deserialize(params),
139 140 141 142 143 144 145 146 147 148 149 150
      'get_render_tree': (Map<String, String> params) => GetRenderTree.deserialize(params),
      'enter_text': (Map<String, String> params) => EnterText.deserialize(params),
      'get_text': (Map<String, String> params) => GetText.deserialize(params),
      'request_data': (Map<String, String> params) => RequestData.deserialize(params),
      'scroll': (Map<String, String> params) => Scroll.deserialize(params),
      'scrollIntoView': (Map<String, String> params) => ScrollIntoView.deserialize(params),
      'set_frame_sync': (Map<String, String> params) => SetFrameSync.deserialize(params),
      'set_semantics': (Map<String, String> params) => SetSemantics.deserialize(params),
      'set_text_entry_emulation': (Map<String, String> params) => SetTextEntryEmulation.deserialize(params),
      'tap': (Map<String, String> params) => Tap.deserialize(params),
      'waitFor': (Map<String, String> params) => WaitFor.deserialize(params),
      'waitForAbsent': (Map<String, String> params) => WaitForAbsent.deserialize(params),
151 152 153 154
      'waitForCondition': (Map<String, String> params) => WaitForCondition.deserialize(params),
      'waitUntilNoTransientCallbacks': (Map<String, String> params) => WaitUntilNoTransientCallbacks.deserialize(params), // ignore: deprecated_member_use_from_same_package
      'waitUntilNoPendingFrame': (Map<String, String> params) => WaitUntilNoPendingFrame.deserialize(params), // ignore: deprecated_member_use_from_same_package
      'waitUntilFirstFrameRasterized': (Map<String, String> params) => WaitUntilFirstFrameRasterized.deserialize(params), // ignore: deprecated_member_use_from_same_package
155
      'get_semantics_id': (Map<String, String> params) => GetSemanticsId.deserialize(params),
156
      'get_offset': (Map<String, String> params) => GetOffset.deserialize(params),
157
      'get_diagnostics_tree': (Map<String, String> params) => GetDiagnosticsTree.deserialize(params),
158
    });
159

160
    _finders.addAll(<String, FinderConstructor>{
161 162 163 164 165
      'ByText': (SerializableFinder finder) => _createByTextFinder(finder as ByText),
      'ByTooltipMessage': (SerializableFinder finder) => _createByTooltipMessageFinder(finder as ByTooltipMessage),
      'BySemanticsLabel': (SerializableFinder finder) => _createBySemanticsLabelFinder(finder as BySemanticsLabel),
      'ByValueKey': (SerializableFinder finder) => _createByValueKeyFinder(finder as ByValueKey),
      'ByType': (SerializableFinder finder) => _createByTypeFinder(finder as ByType),
166
      'PageBack': (SerializableFinder finder) => _createPageBackFinder(),
167 168
      'Ancestor': (SerializableFinder finder) => _createAncestorFinder(finder as Ancestor),
      'Descendant': (SerializableFinder finder) => _createDescendantFinder(finder as Descendant),
169
    });
170 171
  }

172 173
  final TestTextInput _testTextInput = TestTextInput();

174
  final DataHandler _requestDataHandler;
175
  final bool _silenceErrors;
176

177 178 179
  void _log(String message) {
    driverLog('FlutterDriverExtension', message);
  }
180

181
  final WidgetController _prober = LiveWidgetController(WidgetsBinding.instance);
182 183
  final Map<String, CommandHandlerCallback> _commandHandlers = <String, CommandHandlerCallback>{};
  final Map<String, CommandDeserializerCallback> _commandDeserializers = <String, CommandDeserializerCallback>{};
184
  final Map<String, FinderConstructor> _finders = <String, FinderConstructor>{};
185

186 187 188 189
  /// With [_frameSync] enabled, Flutter Driver will wait to perform an action
  /// until there are no pending frames in the app under test.
  bool _frameSync = true;

190 191 192 193 194 195 196 197 198 199
  /// Processes a driver command configured by [params] and returns a result
  /// as an arbitrary JSON object.
  ///
  /// [params] must contain key "command" whose value is a string that
  /// identifies the kind of the command and its corresponding
  /// [CommandDeserializerCallback]. Other keys and values are specific to the
  /// concrete implementation of [Command] and [CommandDeserializerCallback].
  ///
  /// The returned JSON is command specific. Generally the caller deserializes
  /// the result into a subclass of [Result], but that's not strictly required.
200
  @visibleForTesting
201
  Future<Map<String, dynamic>> call(Map<String, String> params) async {
202
    final String commandKind = params['command'];
203
    try {
204 205
      final CommandHandlerCallback commandHandler = _commandHandlers[commandKind];
      final CommandDeserializerCallback commandDeserializer =
206
          _commandDeserializers[commandKind];
207 208
      if (commandHandler == null || commandDeserializer == null)
        throw 'Extension $_extensionMethod does not support command $commandKind';
209
      final Command command = commandDeserializer(params);
210 211
      assert(WidgetsBinding.instance.isRootWidgetAttached || !command.requiresRootWidgetAttached,
          'No root widget is attached; have you remembered to call runApp()?');
212 213 214 215
      Future<Result> responseFuture = commandHandler(command);
      if (command.timeout != null)
        responseFuture = responseFuture.timeout(command.timeout);
      final Result response = await responseFuture;
216
      return _makeResponse(response?.toJson());
217
    } on TimeoutException catch (error, stackTrace) {
218 219 220
      final String message = 'Timeout while executing $commandKind: $error\n$stackTrace';
      _log(message);
      return _makeResponse(message, isError: true);
221
    } catch (error, stackTrace) {
222
      final String message = 'Uncaught extension error while executing $commandKind: $error\n$stackTrace';
223
      if (!_silenceErrors)
224 225
        _log(message);
      return _makeResponse(message, isError: true);
226 227 228
    }
  }

229
  Map<String, dynamic> _makeResponse(dynamic response, { bool isError = false }) {
230 231 232 233 234 235
    return <String, dynamic>{
      'isError': isError,
      'response': response,
    };
  }

236
  Future<Health> _getHealth(Command command) async => const Health(HealthStatus.ok);
237

238 239 240 241
  Future<LayerTree> _getLayerTree(Command command) async {
    return LayerTree(RendererBinding.instance?.renderView?.debugLayer?.toStringDeep());
  }

242
  Future<RenderTree> _getRenderTree(Command command) async {
243
    return RenderTree(RendererBinding.instance?.renderView?.toStringDeep());
244 245
  }

246
  // This can be used to wait for the first frame being rasterized during app launch.
247 248 249 250
  @Deprecated(
    'This method has been deprecated in favor of _waitForCondition. '
    'This feature was deprecated after v1.9.3.'
  )
251 252 253 254 255
  Future<Result> _waitUntilFirstFrameRasterized(Command command) async {
    await WidgetsBinding.instance.waitUntilFirstFrameRasterized;
    return null;
  }

256
  // Waits until at the end of a frame the provided [condition] is [true].
257
  Future<void> _waitUntilFrame(bool condition(), [ Completer<void> completer ]) {
258
    completer ??= Completer<void>();
259 260 261 262 263 264 265 266 267 268
    if (!condition()) {
      SchedulerBinding.instance.addPostFrameCallback((Duration timestamp) {
        _waitUntilFrame(condition, completer);
      });
    } else {
      completer.complete();
    }
    return completer.future;
  }

269
  /// Runs `finder` repeatedly until it finds one or more [Element]s.
270 271 272
  Future<Finder> _waitForElement(Finder finder) async {
    if (_frameSync)
      await _waitUntilFrame(() => SchedulerBinding.instance.transientCallbackCount == 0);
273

274
    await _waitUntilFrame(() => finder.evaluate().isNotEmpty);
275 276 277 278 279

    if (_frameSync)
      await _waitUntilFrame(() => SchedulerBinding.instance.transientCallbackCount == 0);

    return finder;
280 281
  }

282 283 284 285 286
  /// Runs `finder` repeatedly until it finds zero [Element]s.
  Future<Finder> _waitForAbsentElement(Finder finder) async {
    if (_frameSync)
      await _waitUntilFrame(() => SchedulerBinding.instance.transientCallbackCount == 0);

287
    await _waitUntilFrame(() => finder.evaluate().isEmpty);
288 289 290 291 292 293 294

    if (_frameSync)
      await _waitUntilFrame(() => SchedulerBinding.instance.transientCallbackCount == 0);

    return finder;
  }

295 296
  Finder _createByTextFinder(ByText arguments) {
    return find.text(arguments.text);
297 298
  }

299 300
  Finder _createByTooltipMessageFinder(ByTooltipMessage arguments) {
    return find.byElementPredicate((Element element) {
301
      final Widget widget = element.widget;
302 303 304
      if (widget is Tooltip)
        return widget.message == arguments.text;
      return false;
305
    }, description: 'widget with text tooltip "${arguments.text}"');
306 307
  }

308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323
  Finder _createBySemanticsLabelFinder(BySemanticsLabel arguments) {
    return find.byElementPredicate((Element element) {
      if (element is! RenderObjectElement) {
        return false;
      }
      final String semanticsLabel = element.renderObject?.debugSemantics?.label;
      if (semanticsLabel == null) {
        return false;
      }
      final Pattern label = arguments.label;
      return label is RegExp
          ? label.hasMatch(semanticsLabel)
          : label == semanticsLabel;
    }, description: 'widget with semantic label "${arguments.label}"');
  }

324
  Finder _createByValueKeyFinder(ByValueKey arguments) {
325 326
    switch (arguments.keyValueType) {
      case 'int':
327
        return find.byKey(ValueKey<int>(arguments.keyValue as int));
328
      case 'String':
329
        return find.byKey(ValueKey<String>(arguments.keyValue as String));
330 331 332
      default:
        throw 'Unsupported ByValueKey type: ${arguments.keyValueType}';
    }
333 334
  }

335 336 337 338 339 340
  Finder _createByTypeFinder(ByType arguments) {
    return find.byElementPredicate((Element element) {
      return element.widget.runtimeType.toString() == arguments.type;
    }, description: 'widget with runtimeType "${arguments.type}"');
  }

341 342 343 344 345 346 347 348 349 350 351
  Finder _createPageBackFinder() {
    return find.byElementPredicate((Element element) {
      final Widget widget = element.widget;
      if (widget is Tooltip)
        return widget.message == 'Back';
      if (widget is CupertinoNavigationBarBackButton)
        return true;
      return false;
    }, description: 'Material or Cupertino back button');
  }

352
  Finder _createAncestorFinder(Ancestor arguments) {
353
    final Finder finder = find.ancestor(
354 355 356 357
      of: _createFinder(arguments.of),
      matching: _createFinder(arguments.matching),
      matchRoot: arguments.matchRoot,
    );
358
    return arguments.firstMatchOnly ? finder.first : finder;
359 360 361
  }

  Finder _createDescendantFinder(Descendant arguments) {
362
    final Finder finder = find.descendant(
363 364 365 366
      of: _createFinder(arguments.of),
      matching: _createFinder(arguments.matching),
      matchRoot: arguments.matchRoot,
    );
367
    return arguments.firstMatchOnly ? finder.first : finder;
368 369
  }

370
  Finder _createFinder(SerializableFinder finder) {
371
    final FinderConstructor constructor = _finders[finder.finderType];
372

373
    if (constructor == null)
374 375
      throw 'Unsupported finder type: ${finder.finderType}';

376
    return constructor(finder);
377 378
  }

379
  Future<TapResult> _tap(Command command) async {
380
    final Tap tapCommand = command as Tap;
381 382 383 384
    final Finder computedFinder = await _waitForElement(
      _createFinder(tapCommand.finder).hitTestable()
    );
    await _prober.tap(computedFinder);
385
    return const TapResult();
386 387
  }

388
  Future<WaitForResult> _waitFor(Command command) async {
389
    final WaitFor waitForCommand = command as WaitFor;
390
    await _waitForElement(_createFinder(waitForCommand.finder));
391
    return const WaitForResult();
392 393
  }

394
  Future<WaitForAbsentResult> _waitForAbsent(Command command) async {
395
    final WaitForAbsent waitForAbsentCommand = command as WaitForAbsent;
396
    await _waitForAbsentElement(_createFinder(waitForAbsentCommand.finder));
397
    return const WaitForAbsentResult();
398 399
  }

400 401
  Future<Result> _waitForCondition(Command command) async {
    assert(command != null);
402
    final WaitForCondition waitForConditionCommand = command as WaitForCondition;
403 404 405 406 407
    final WaitCondition condition = deserializeCondition(waitForConditionCommand.condition);
    await condition.wait();
    return null;
  }

408 409 410 411
  @Deprecated(
    'This method has been deprecated in favor of _waitForCondition. '
    'This feature was deprecated after v1.9.3.'
  )
412
  Future<Result> _waitUntilNoTransientCallbacks(Command command) async {
413 414
    if (SchedulerBinding.instance.transientCallbackCount != 0)
      await _waitUntilFrame(() => SchedulerBinding.instance.transientCallbackCount == 0);
415
    return null;
416 417
  }

418
  /// Returns a future that waits until no pending frame is scheduled (frame is synced).
419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434
  ///
  /// Specifically, it checks:
  /// * Whether the count of transient callbacks is zero.
  /// * Whether there's no pending request for scheduling a new frame.
  ///
  /// We consider the frame is synced when both conditions are met.
  ///
  /// This method relies on a Flutter Driver mechanism called "frame sync",
  /// which waits for transient animations to finish. Persistent animations will
  /// cause this to wait forever.
  ///
  /// If a test needs to interact with the app while animations are running, it
  /// should avoid this method and instead disable the frame sync using
  /// `set_frame_sync` method. See [FlutterDriver.runUnsynchronized] for more
  /// details on how to do this. Note, disabling frame sync will require the
  /// test author to use some other method to avoid flakiness.
435 436
  ///
  /// This method has been deprecated in favor of [_waitForCondition].
437 438 439 440
  @Deprecated(
    'This method has been deprecated in favor of _waitForCondition. '
    'This feature was deprecated after v1.9.3.'
  )
441
  Future<Result> _waitUntilNoPendingFrame(Command command) async {
442 443 444 445 446 447 448
    await _waitUntilFrame(() {
      return SchedulerBinding.instance.transientCallbackCount == 0
          && !SchedulerBinding.instance.hasScheduledFrame;
    });
    return null;
  }

449
  Future<GetSemanticsIdResult> _getSemanticsId(Command command) async {
450
    final GetSemanticsId semanticsCommand = command as GetSemanticsId;
451
    final Finder target = await _waitForElement(_createFinder(semanticsCommand.finder));
452 453 454 455 456
    final Iterable<Element> elements = target.evaluate();
    if (elements.length > 1) {
      throw StateError('Found more than one element with the same ID: $elements');
    }
    final Element element = elements.single;
457 458 459 460
    RenderObject renderObject = element.renderObject;
    SemanticsNode node;
    while (renderObject != null && node == null) {
      node = renderObject.debugSemantics;
461
      renderObject = renderObject.parent as RenderObject;
462 463
    }
    if (node == null)
464 465
      throw StateError('No semantics data found');
    return GetSemanticsIdResult(node.id);
466 467
  }

468
  Future<GetOffsetResult> _getOffset(Command command) async {
469
    final GetOffset getOffsetCommand = command as GetOffset;
470 471
    final Finder finder = await _waitForElement(_createFinder(getOffsetCommand.finder));
    final Element element = finder.evaluate().single;
472
    final RenderBox box = element.renderObject as RenderBox;
473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494
    Offset localPoint;
    switch (getOffsetCommand.offsetType) {
      case OffsetType.topLeft:
        localPoint = Offset.zero;
        break;
      case OffsetType.topRight:
        localPoint = box.size.topRight(Offset.zero);
        break;
      case OffsetType.bottomLeft:
        localPoint = box.size.bottomLeft(Offset.zero);
        break;
      case OffsetType.bottomRight:
        localPoint = box.size.bottomRight(Offset.zero);
        break;
      case OffsetType.center:
        localPoint = box.size.center(Offset.zero);
        break;
    }
    final Offset globalPoint = box.localToGlobal(localPoint);
    return GetOffsetResult(dx: globalPoint.dx, dy: globalPoint.dy);
  }

495
  Future<DiagnosticsTreeResult> _getDiagnosticsTree(Command command) async {
496
    final GetDiagnosticsTree diagnosticsCommand = command as GetDiagnosticsTree;
497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513
    final Finder finder = await _waitForElement(_createFinder(diagnosticsCommand.finder));
    final Element element = finder.evaluate().single;
    DiagnosticsNode diagnosticsNode;
    switch (diagnosticsCommand.diagnosticsType) {
      case DiagnosticsType.renderObject:
        diagnosticsNode = element.renderObject.toDiagnosticsNode();
        break;
      case DiagnosticsType.widget:
        diagnosticsNode = element.toDiagnosticsNode();
        break;
    }
    return DiagnosticsTreeResult(diagnosticsNode.toJsonMap(DiagnosticsSerializationDelegate(
      subtreeDepth: diagnosticsCommand.subtreeDepth,
      includeProperties: diagnosticsCommand.includeProperties,
    )));
  }

514
  Future<ScrollResult> _scroll(Command command) async {
515
    final Scroll scrollCommand = command as Scroll;
516
    final Finder target = await _waitForElement(_createFinder(scrollCommand.finder));
517
    final int totalMoves = scrollCommand.duration.inMicroseconds * scrollCommand.frequency ~/ Duration.microsecondsPerSecond;
518
    final Offset delta = Offset(scrollCommand.dx, scrollCommand.dy) / totalMoves.toDouble();
519
    final Duration pause = scrollCommand.duration ~/ totalMoves;
520 521
    final Offset startLocation = _prober.getCenter(target);
    Offset currentLocation = startLocation;
522 523
    final TestPointer pointer = TestPointer(1);
    final HitTestResult hitTest = HitTestResult();
524

525 526
    _prober.binding.hitTest(hitTest, startLocation);
    _prober.binding.dispatchEvent(pointer.down(startLocation), hitTest);
527
    await Future<void>.value(); // so that down and move don't happen in the same microtask
528
    for (int moves = 0; moves < totalMoves; moves += 1) {
529
      currentLocation = currentLocation + delta;
530
      _prober.binding.dispatchEvent(pointer.move(currentLocation), hitTest);
531
      await Future<void>.delayed(pause);
532
    }
533
    _prober.binding.dispatchEvent(pointer.up(), hitTest);
534

535
    return const ScrollResult();
536 537
  }

538
  Future<ScrollResult> _scrollIntoView(Command command) async {
539
    final ScrollIntoView scrollIntoViewCommand = command as ScrollIntoView;
540
    final Finder target = await _waitForElement(_createFinder(scrollIntoViewCommand.finder));
Adam Barth's avatar
Adam Barth committed
541
    await Scrollable.ensureVisible(target.evaluate().single, duration: const Duration(milliseconds: 100), alignment: scrollIntoViewCommand.alignment ?? 0.0);
542
    return const ScrollResult();
543 544
  }

545
  Future<GetTextResult> _getText(Command command) async {
546
    final GetText getTextCommand = command as GetText;
547
    final Finder target = await _waitForElement(_createFinder(getTextCommand.finder));
548
    // TODO(yjbanov): support more ways to read text
549
    final Text text = target.evaluate().single.widget as Text;
550
    return GetTextResult(text.data);
551
  }
552

553
  Future<SetTextEntryEmulationResult> _setTextEntryEmulation(Command command) async {
554
    final SetTextEntryEmulation setTextEntryEmulationCommand = command as SetTextEntryEmulation;
555 556 557 558 559
    if (setTextEntryEmulationCommand.enabled) {
      _testTextInput.register();
    } else {
      _testTextInput.unregister();
    }
560
    return const SetTextEntryEmulationResult();
561 562
  }

563
  Future<EnterTextResult> _enterText(Command command) async {
564 565 566 567
    if (!_testTextInput.isRegistered) {
      throw 'Unable to fulfill `FlutterDriver.enterText`. Text emulation is '
            'disabled. You can enable it using `FlutterDriver.setTextEntryEmulation`.';
    }
568
    final EnterText enterTextCommand = command as EnterText;
569
    _testTextInput.enterText(enterTextCommand.text);
570
    return const EnterTextResult();
571 572
  }

573
  Future<RequestDataResult> _requestData(Command command) async {
574
    final RequestData requestDataCommand = command as RequestData;
575
    return RequestDataResult(_requestDataHandler == null ? 'No requestData Extension registered' : await _requestDataHandler(requestDataCommand.message));
576 577
  }

578
  Future<SetFrameSyncResult> _setFrameSync(Command command) async {
579
    final SetFrameSync setFrameSyncCommand = command as SetFrameSync;
580
    _frameSync = setFrameSyncCommand.enabled;
581
    return const SetFrameSyncResult();
582
  }
583 584 585 586 587

  SemanticsHandle _semantics;
  bool get _semanticsIsEnabled => RendererBinding.instance.pipelineOwner.semanticsOwner != null;

  Future<SetSemanticsResult> _setSemantics(Command command) async {
588
    final SetSemantics setSemanticsCommand = command as SetSemantics;
589 590 591 592 593
    final bool semanticsWasEnabled = _semanticsIsEnabled;
    if (setSemanticsCommand.enabled && _semantics == null) {
      _semantics = RendererBinding.instance.pipelineOwner.ensureSemantics();
      if (!semanticsWasEnabled) {
        // wait for the first frame where semantics is enabled.
594
        final Completer<void> completer = Completer<void>();
595 596 597 598 599 600 601 602 603
        SchedulerBinding.instance.addPostFrameCallback((Duration d) {
          completer.complete();
        });
        await completer.future;
      }
    } else if (!setSemanticsCommand.enabled && _semantics != null) {
      _semantics.dispose();
      _semantics = null;
    }
604
    return SetSemanticsResult(semanticsWasEnabled != _semanticsIsEnabled);
605
  }
606
}