driver.dart 39.3 KB
Newer Older
1 2 3 4 5
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';
6
import 'dart:convert';
7
import 'dart:io';
8 9

import 'package:file/file.dart' as f;
10
import 'package:fuchsia_remote_debug_protocol/fuchsia_remote_debug_protocol.dart' as fuchsia;
11
import 'package:json_rpc_2/error_code.dart' as error_code;
12
import 'package:json_rpc_2/json_rpc_2.dart' as rpc;
13
import 'package:meta/meta.dart';
14
import 'package:path/path.dart' as p;
15 16
import 'package:vm_service_client/vm_service_client.dart';
import 'package:web_socket_channel/io.dart';
17

18 19 20
import '../common/error.dart';
import '../common/find.dart';
import '../common/frame_sync.dart';
21
import '../common/fuchsia_compat.dart';
22 23 24 25 26 27
import '../common/gesture.dart';
import '../common/health.dart';
import '../common/message.dart';
import '../common/render_tree.dart';
import '../common/request_data.dart';
import '../common/semantics.dart';
28
import '../common/text.dart';
29
import 'common.dart';
30
import 'timeline.dart';
31

32
/// Timeline stream identifier.
33
enum TimelineStream {
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
  /// A meta-identifier that instructs the Dart VM to record all streams.
  all,

  /// Marks events related to calls made via Dart's C API.
  api,

  /// Marks events from the Dart VM's JIT compiler.
  compiler,

  /// Marks events emitted using the `dart:developer` API.
  dart,

  /// Marks events from the Dart VM debugger.
  debugger,

  /// Marks events emitted using the `dart_tools_api.h` C API.
  embedder,

  /// Marks events from the garbage collector.
  gc,

  /// Marks events related to message passing between Dart isolates.
  isolate,

  /// Marks internal VM events.
  vm,
60 61
}

62
const List<TimelineStream> _defaultStreams = <TimelineStream>[TimelineStream.all];
63

64 65 66
/// How long to wait before showing a message saying that
/// things seem to be taking a long time.
const Duration _kUnusuallyLongTimeout = Duration(seconds: 5);
67 68 69

/// The amount of time we wait prior to making the next attempt to connect to
/// the VM service.
70
const Duration _kPauseBetweenReconnectAttempts = Duration(seconds: 1);
71

72
// See https://github.com/dart-lang/sdk/blob/master/runtime/vm/timeline.cc#L32
73
String _timelineStreamsToString(List<TimelineStream> streams) {
74
  final String contents = streams.map<String>((TimelineStream stream) {
75
    switch (stream) {
76 77 78 79 80 81 82 83 84
      case TimelineStream.all: return 'all';
      case TimelineStream.api: return 'API';
      case TimelineStream.compiler: return 'Compiler';
      case TimelineStream.dart: return 'Dart';
      case TimelineStream.debugger: return 'Debugger';
      case TimelineStream.embedder: return 'Embedder';
      case TimelineStream.gc: return 'GC';
      case TimelineStream.isolate: return 'Isolate';
      case TimelineStream.vm: return 'VM';
85
      default:
86
        throw 'Unknown timeline stream $stream';
87 88 89 90 91
    }
  }).join(', ');
  return '[$contents]';
}

92
final Logger _log = Logger('FlutterDriver');
93

94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114
Future<T> _warnIfSlow<T>({
  @required Future<T> future,
  @required Duration timeout,
  @required String message,
}) {
  assert(future != null);
  assert(timeout != null);
  assert(message != null);
  return future..timeout(timeout, onTimeout: () { _log.warning(message); });
}

Duration _maxDuration(Duration a, Duration b) {
  if (a == null)
    return b;
  if (b == null)
    return a;
  if (a > b)
    return a;
  return b;
}

115 116 117 118
/// A convenient accessor to frequently used finders.
///
/// Examples:
///
119
///     driver.tap(find.text('Save'));
120
///     driver.scroll(find.byValueKey(42));
121
const CommonFinders find = CommonFinders._();
122

123 124 125 126 127
/// Computes a value.
///
/// If computation is asynchronous, the function may return a [Future].
///
/// See also [FlutterDriver.waitFor].
128
typedef EvaluatorFunction = dynamic Function();
129

130 131
/// Drives a Flutter Application running in another process.
class FlutterDriver {
132 133 134
  /// Creates a driver that uses a connection provided by the given
  /// [_serviceClient], [_peer] and [_appIsolate].
  @visibleForTesting
135 136 137 138
  FlutterDriver.connectedTo(
    this._serviceClient,
    this._peer,
    this._appIsolate, {
139 140
    bool printCommunication = false,
    bool logCommunicationToFile = true,
141 142 143
  }) : _printCommunication = printCommunication,
       _logCommunicationToFile = logCommunicationToFile,
       _driverId = _nextDriverId++;
144

145 146 147 148
  static const String _flutterExtensionMethodName = 'ext.flutter.driver';
  static const String _setVMTimelineFlagsMethodName = '_setVMTimelineFlags';
  static const String _getVMTimelineMethodName = '_getVMTimeline';
  static const String _clearVMTimelineMethodName = '_clearVMTimeline';
149
  static const String _collectAllGarbageMethodName = '_collectAllGarbage';
150

151 152
  static int _nextDriverId = 0;

153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168
  // The additional blank line in the beginning is for _log.warning.
  static const String _kDebugWarning = '''

┏╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍┓
┇ ⚠    THIS BENCHMARK IS BEING RUN IN DEBUG MODE     ⚠  ┇
┡╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍┦
│                                                       │
│  Numbers obtained from a benchmark while asserts are  │
│  enabled will not accurately reflect the performance  │
│  that will be experienced by end users using release  ╎
│  builds. Benchmarks should be run using this command  ┆
│  line:  flutter drive --profile test_perf.dart        ┊
│                                                       ┊
└─────────────────────────────────────────────────╌┄┈  🐢
''';

169 170 171 172
  /// Connects to a Flutter application.
  ///
  /// Resumes the application if it is currently paused (e.g. at a breakpoint).
  ///
173
  /// `dartVmServiceUrl` is the URL to Dart observatory (a.k.a. VM service). If
174
  /// not specified, the URL specified by the `VM_SERVICE_URL` environment
175
  /// variable is used. One or the other must be specified.
176
  ///
177
  /// `printCommunication` determines whether the command communication between
178 179
  /// the test and the app should be printed to stdout.
  ///
180
  /// `logCommunicationToFile` determines whether the command communication
181
  /// between the test and the app should be logged to `flutter_driver_commands.log`.
182
  ///
183
  /// `isolateNumber` determines the specific isolate to connect to.
184
  /// If this is left as `null`, will connect to the first isolate found
185 186 187 188 189 190 191 192 193 194 195 196
  /// running on `dartVmServiceUrl`.
  ///
  /// `fuchsiaModuleTarget` specifies the pattern for determining which mod to
  /// control. When running on a Fuchsia device, either this or the environment
  /// variable `FUCHSIA_MODULE_TARGET` must be set (the environment variable is
  /// treated as a substring pattern). This field will be ignored if
  /// `isolateNumber` is set, as this is already enough information to connect
  /// to an isolate.
  ///
  /// The return value is a future. This method never times out, though it may
  /// fail (completing with an error). A timeout can be applied by the caller
  /// using [Future.timeout] if necessary.
197 198
  static Future<FlutterDriver> connect({
    String dartVmServiceUrl,
199 200
    bool printCommunication = false,
    bool logCommunicationToFile = true,
201
    int isolateNumber,
202
    Pattern fuchsiaModuleTarget,
203
  }) async {
204
    // If running on a Fuchsia device, connect to the first isolate whose name
205 206 207 208 209
    // matches FUCHSIA_MODULE_TARGET.
    //
    // If the user has already supplied an isolate number/URL to the Dart VM
    // service, then this won't be run as it is unnecessary.
    if (Platform.isFuchsia && isolateNumber == null) {
210 211 212
      // TODO(awdavies): Use something other than print. On fuchsia
      // `stderr`/`stdout` appear to have issues working correctly.
      flutterDriverLog.listen(print);
213 214
      fuchsiaModuleTarget ??= Platform.environment['FUCHSIA_MODULE_TARGET'];
      if (fuchsiaModuleTarget == null) {
215 216 217 218 219
        throw DriverError(
          'No Fuchsia module target has been specified.\n'
          'Please make sure to specify the FUCHSIA_MODULE_TARGET '
          'environment variable.'
        );
220 221 222 223 224 225 226 227 228 229 230 231
      }
      final fuchsia.FuchsiaRemoteConnection fuchsiaConnection =
          await FuchsiaCompat.connect();
      final List<fuchsia.IsolateRef> refs =
          await fuchsiaConnection.getMainIsolatesByPattern(fuchsiaModuleTarget);
      final fuchsia.IsolateRef ref = refs.first;
      isolateNumber = ref.number;
      dartVmServiceUrl = ref.dartVm.uri.toString();
      await fuchsiaConnection.stop();
      FuchsiaCompat.cleanup();
    }

232
    dartVmServiceUrl ??= Platform.environment['VM_SERVICE_URL'];
233 234

    if (dartVmServiceUrl == null) {
235
      throw DriverError(
236 237 238 239
        'Could not determine URL to connect to application.\n'
        'Either the VM_SERVICE_URL environment variable should be set, or an explicit '
        'URL should be provided to the FlutterDriver.connect() method.'
      );
240
    }
241

Josh Soref's avatar
Josh Soref committed
242
    // Connect to Dart VM services
243
    _log.info('Connecting to Flutter application at $dartVmServiceUrl');
244 245
    final VMServiceClientConnection connection =
        await vmServiceConnectFunction(dartVmServiceUrl);
246 247
    final VMServiceClient client = connection.client;
    final VM vm = await client.getVM();
248 249 250 251 252 253
    final VMIsolateRef isolateRef = isolateNumber ==
        null ? vm.isolates.first :
               vm.isolates.firstWhere(
                   (VMIsolateRef isolate) => isolate.number == isolateNumber);
    _log.trace('Isolate found with number: ${isolateRef.number}');

254
    VMIsolate isolate = await isolateRef.loadRunnable();
255

256
    // TODO(yjbanov): vm_service_client does not support "None" pause event yet.
257 258
    // It is currently reported as null, but we cannot rely on it because
    // eventually the event will be reported as a non-null object. For now,
259 260
    // list all the events we know about. Later we'll check for "None" event
    // explicitly.
261
    //
262 263 264 265 266 267 268
    // See: https://github.com/dart-lang/vm_service_client/issues/4
    if (isolate.pauseEvent is! VMPauseStartEvent &&
        isolate.pauseEvent is! VMPauseExitEvent &&
        isolate.pauseEvent is! VMPauseBreakpointEvent &&
        isolate.pauseEvent is! VMPauseExceptionEvent &&
        isolate.pauseEvent is! VMPauseInterruptedEvent &&
        isolate.pauseEvent is! VMResumeEvent) {
269
      isolate = await isolateRef.loadRunnable();
270 271
    }

272
    final FlutterDriver driver = FlutterDriver.connectedTo(
273 274 275
      client, connection.peer, isolate,
      printCommunication: printCommunication,
      logCommunicationToFile: logCommunicationToFile,
276
    );
277 278 279 280

    // Attempts to resume the isolate, but does not crash if it fails because
    // the isolate is already resumed. There could be a race with other tools,
    // such as a debugger, any of which could have resumed the isolate.
Ian Hickson's avatar
Ian Hickson committed
281
    Future<dynamic> resumeLeniently() {
282
      _log.trace('Attempting to resume isolate');
283 284
      return isolate.resume().catchError((dynamic e) {
        const int vmMustBePausedCode = 101;
285 286 287 288 289 290 291 292 293 294 295 296 297 298
        if (e is rpc.RpcException && e.code == vmMustBePausedCode) {
          // No biggie; something else must have resumed the isolate
          _log.warning(
            'Attempted to resume an already resumed isolate. This may happen '
            'when we lose a race with another tool (usually a debugger) that '
            'is connected to the same isolate.'
          );
        } else {
          // Failed to resume due to another reason. Fail hard.
          throw e;
        }
      });
    }

299
    /// Waits for a signal from the VM service that the extension is registered.
300
    /// Returns [_flutterExtensionMethodName]
301 302
    Future<String> waitForServiceExtension() {
      return isolate.onExtensionAdded.firstWhere((String extension) {
303
        return extension == _flutterExtensionMethodName;
304 305 306 307 308 309 310 311 312 313
      });
    }

    /// Tells the Dart VM Service to notify us about "Isolate" events.
    ///
    /// This is a workaround for an issue in package:vm_service_client, which
    /// subscribes to the "Isolate" stream lazily upon subscription, which
    /// results in lost events.
    ///
    /// Details: https://github.com/dart-lang/vm_service_client/issues/17
314
    Future<void> enableIsolateStreams() async {
315 316 317 318 319
      await connection.peer.sendRequest('streamListen', <String, String>{
        'streamId': 'Isolate',
      });
    }

320 321 322 323 324 325 326
    // Attempt to resume isolate if it was paused
    if (isolate.pauseEvent is VMPauseStartEvent) {
      _log.trace('Isolate is paused at start.');

      // If the isolate is paused at the start, e.g. via the --start-paused
      // option, then the VM service extension is not registered yet. Wait for
      // it to be registered.
327
      await enableIsolateStreams();
328
      final Future<String> whenServiceExtensionReady = waitForServiceExtension();
329
      final Future<dynamic> whenResumed = resumeLeniently();
330 331
      await whenResumed;

332 333 334 335 336 337 338 339 340
      _log.trace('Waiting for service extension');
      // We will never receive the extension event if the user does not
      // register it. If that happens, show a message but continue waiting.
      await _warnIfSlow<String>(
        future: whenServiceExtensionReady,
        timeout: _kUnusuallyLongTimeout,
        message: 'Flutter Driver extension is taking a long time to become available. '
                 'Ensure your test app (often "lib/main.dart") imports '
                 '"package:flutter_driver/driver_extension.dart" and '
341
                 'calls enableFlutterDriverExtension() as the first call in main().',
342
      );
343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359
    } else if (isolate.pauseEvent is VMPauseExitEvent ||
               isolate.pauseEvent is VMPauseBreakpointEvent ||
               isolate.pauseEvent is VMPauseExceptionEvent ||
               isolate.pauseEvent is VMPauseInterruptedEvent) {
      // If the isolate is paused for any other reason, assume the extension is
      // already there.
      _log.trace('Isolate is paused mid-flight.');
      await resumeLeniently();
    } else if (isolate.pauseEvent is VMResumeEvent) {
      _log.trace('Isolate is not paused. Assuming application is ready.');
    } else {
      _log.warning(
        'Unknown pause event type ${isolate.pauseEvent.runtimeType}. '
        'Assuming application is ready.'
      );
    }

360 361 362 363 364 365 366 367 368 369 370 371 372 373 374
    // Invoked checkHealth and try to fix delays in the registration of Service
    // extensions
    Future<Health> checkHealth() async {
      try {
        // At this point the service extension must be installed. Verify it.
        return await driver.checkHealth();
      } on rpc.RpcException catch (e) {
        if (e.code != error_code.METHOD_NOT_FOUND) {
          rethrow;
        }
        _log.trace(
          'Check Health failed, try to wait for the service extensions to be'
          'registered.'
        );
        await enableIsolateStreams();
375
        await waitForServiceExtension();
376 377 378 379 380
        return driver.checkHealth();
      }
    }

    final Health health = await checkHealth();
381
    if (health.status != HealthStatus.ok) {
382
      await client.close();
383
      throw DriverError('Flutter application health check failed.');
384 385 386 387 388 389
    }

    _log.info('Connected to Flutter application.');
    return driver;
  }

390 391
  /// The unique ID of this driver instance.
  final int _driverId;
392

393 394
  /// Client connected to the Dart VM running the Flutter application
  final VMServiceClient _serviceClient;
395

396 397
  /// JSON-RPC client useful for sending raw JSON requests.
  final rpc.Peer _peer;
398

399
  /// The main isolate hosting the Flutter application
400
  final VMIsolate _appIsolate;
401

402 403
  /// Whether to print communication between host and app to `stdout`.
  final bool _printCommunication;
404

405 406
  /// Whether to log communication between host and app to `flutter_driver_commands.log`.
  final bool _logCommunicationToFile;
407 408

  Future<Map<String, dynamic>> _sendCommand(Command command) async {
409
    Map<String, dynamic> response;
410
    try {
411
      final Map<String, String> serialized = command.serialize();
412
      _logCommunication('>>> $serialized');
413 414 415 416 417 418 419 420
      final Future<Map<String, dynamic>> future = _appIsolate.invokeExtension(
        _flutterExtensionMethodName,
        serialized,
      ).then<Map<String, dynamic>>((Object value) => value);
      response = await _warnIfSlow<Map<String, dynamic>>(
        future: future,
        timeout: _maxDuration(command.timeout, _kUnusuallyLongTimeout),
        message: '${command.kind} message is taking a long time to complete...',
421
      );
422
      _logCommunication('<<< $response');
423
    } catch (error, stackTrace) {
424
      throw DriverError(
425 426
        'Failed to fulfill ${command.runtimeType} due to remote error',
        error,
427
        stackTrace,
428 429
      );
    }
430
    if (response['isError'])
431
      throw DriverError('Error in Flutter application: ${response['response']}');
432
    return response['response'];
433 434
  }

435
  void _logCommunication(String message) {
436 437 438
    if (_printCommunication)
      _log.info(message);
    if (_logCommunicationToFile) {
439
      final f.File file = fs.file(p.join(testOutputsDirectory, 'flutter_driver_commands_$_driverId.log'));
440
      file.createSync(recursive: true); // no-op if file exists
441
      file.writeAsStringSync('${DateTime.now()} $message\n', mode: f.FileMode.append, flush: true);
442 443 444
    }
  }

445
  /// Checks the status of the Flutter Driver extension.
446
  Future<Health> checkHealth({ Duration timeout }) async {
447
    return Health.fromJson(await _sendCommand(GetHealth(timeout: timeout)));
448 449
  }

450
  /// Returns a dump of the render tree.
451
  Future<RenderTree> getRenderTree({ Duration timeout }) async {
452
    return RenderTree.fromJson(await _sendCommand(GetRenderTree(timeout: timeout)));
453 454
  }

455
  /// Taps at the center of the widget located by [finder].
456
  Future<void> tap(SerializableFinder finder, { Duration timeout }) async {
457
    await _sendCommand(Tap(finder, timeout: timeout));
458
  }
459

460
  /// Waits until [finder] locates the target.
461
  Future<void> waitFor(SerializableFinder finder, { Duration timeout }) async {
462
    await _sendCommand(WaitFor(finder, timeout: timeout));
463 464
  }

465
  /// Waits until [finder] can no longer locate the target.
466
  Future<void> waitForAbsent(SerializableFinder finder, { Duration timeout }) async {
467
    await _sendCommand(WaitForAbsent(finder, timeout: timeout));
468 469
  }

470 471 472 473
  /// Waits until there are no more transient callbacks in the queue.
  ///
  /// Use this method when you need to wait for the moment when the application
  /// becomes "stable", for example, prior to taking a [screenshot].
474
  Future<void> waitUntilNoTransientCallbacks({ Duration timeout }) async {
475
    await _sendCommand(WaitUntilNoTransientCallbacks(timeout: timeout));
476 477
  }

478 479 480 481 482 483 484 485 486
  /// Tell the driver to perform a scrolling action.
  ///
  /// A scrolling action begins with a "pointer down" event, which commonly maps
  /// to finger press on the touch screen or mouse button press. A series of
  /// "pointer move" events follow. The action is completed by a "pointer up"
  /// event.
  ///
  /// [dx] and [dy] specify the total offset for the entire scrolling action.
  ///
487
  /// [duration] specifies the length of the action.
488 489 490
  ///
  /// The move events are generated at a given [frequency] in Hz (or events per
  /// second). It defaults to 60Hz.
491 492
  Future<void> scroll(SerializableFinder finder, double dx, double dy, Duration duration, { int frequency = 60, Duration timeout }) async {
    await _sendCommand(Scroll(finder, dx, dy, duration, frequency, timeout: timeout));
493 494
  }

495 496
  /// Scrolls the Scrollable ancestor of the widget located by [finder]
  /// until the widget is completely visible.
497 498 499 500 501
  ///
  /// If the widget located by [finder] is contained by a scrolling widget
  /// that lazily creates its children, like [ListView] or [CustomScrollView],
  /// then this method may fail because [finder] doesn't actually exist.
  /// The [scrollUntilVisible] method can be used in this case.
502 503
  Future<void> scrollIntoView(SerializableFinder finder, { double alignment = 0.0, Duration timeout }) async {
    await _sendCommand(ScrollIntoView(finder, alignment: alignment, timeout: timeout));
504 505
  }

506 507 508 509 510
  /// Repeatedly [scroll] the widget located by [scrollable] by [dxScroll] and
  /// [dyScroll] until [item] is visible, and then use [scrollIntoView] to
  /// ensure the item's final position matches [alignment].
  ///
  /// The [scrollable] must locate the scrolling widget that contains [item].
511
  /// Typically `find.byType('ListView')` or `find.byType('CustomScrollView')`.
512
  ///
513
  /// At least one of [dxScroll] and [dyScroll] must be non-zero.
514 515 516 517
  ///
  /// If [item] is below the currently visible items, then specify a negative
  /// value for [dyScroll] that's a small enough increment to expose [item]
  /// without potentially scrolling it up and completely out of view. Similarly
518
  /// if [item] is above, then specify a positive value for [dyScroll].
519
  ///
520
  /// If [item] is to the right of the currently visible items, then
521 522
  /// specify a negative value for [dxScroll] that's a small enough increment to
  /// expose [item] without potentially scrolling it up and completely out of
523
  /// view. Similarly if [item] is to the left, then specify a positive value
524 525 526
  /// for [dyScroll].
  ///
  /// The [timeout] value should be long enough to accommodate as many scrolls
527
  /// as needed to bring an item into view. The default is to not time out.
528 529 530
  Future<void> scrollUntilVisible(
    SerializableFinder scrollable,
    SerializableFinder item, {
531 532 533
    double alignment = 0.0,
    double dxScroll = 0.0,
    double dyScroll = 0.0,
534
    Duration timeout,
535 536 537 538 539 540 541 542
  }) async {
    assert(scrollable != null);
    assert(item != null);
    assert(alignment != null);
    assert(dxScroll != null);
    assert(dyScroll != null);
    assert(dxScroll != 0.0 || dyScroll != 0.0);

543 544 545 546
    // Kick off an (unawaited) waitFor that will complete when the item we're
    // looking for finally scrolls onscreen. We add an initial pause to give it
    // the chance to complete if the item is already onscreen; if not, scroll
    // repeatedly until we either find the item or time out.
547
    bool isVisible = false;
548 549
    waitFor(item, timeout: timeout).then<void>((_) { isVisible = true; });
    await Future<void>.delayed(const Duration(milliseconds: 500));
550 551
    while (!isVisible) {
      await scroll(scrollable, dxScroll, dyScroll, const Duration(milliseconds: 100));
552
      await Future<void>.delayed(const Duration(milliseconds: 500));
553 554 555 556 557
    }

    return scrollIntoView(item, alignment: alignment);
  }

558
  /// Returns the text in the `Text` widget located by [finder].
559
  Future<String> getText(SerializableFinder finder, { Duration timeout }) async {
560
    return GetTextResult.fromJson(await _sendCommand(GetText(finder, timeout: timeout))).text;
561 562
  }

563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579
  /// Enters `text` into the currently focused text input, such as the
  /// [EditableText] widget.
  ///
  /// This method does not use the operating system keyboard to enter text.
  /// Instead it emulates text entry by sending events identical to those sent
  /// by the operating system keyboard (the "TextInputClient.updateEditingState"
  /// method channel call).
  ///
  /// Generally the behavior is dependent on the implementation of the widget
  /// receiving the input. Usually, editable widgets, such as [EditableText] and
  /// those built on top of it would replace the currently entered text with the
  /// provided `text`.
  ///
  /// It is assumed that the widget receiving text input is focused prior to
  /// calling this method. Typically, a test would activate a widget, e.g. using
  /// [tap], then call this method.
  ///
580 581 582
  /// For this method to work, text emulation must be enabled (see
  /// [setTextEntryEmulation]). Text emulation is enabled by default.
  ///
583 584 585 586
  /// Example:
  ///
  /// ```dart
  /// test('enters text in a text field', () async {
587 588 589 590 591 592
  ///   var textField = find.byValueKey('enter-text-field');
  ///   await driver.tap(textField);  // acquire focus
  ///   await driver.enterText('Hello!');  // enter text
  ///   await driver.waitFor(find.text('Hello!'));  // verify text appears on UI
  ///   await driver.enterText('World!');  // enter another piece of text
  ///   await driver.waitFor(find.text('World!'));  // verify new text appears
593 594
  /// });
  /// ```
595
  Future<void> enterText(String text, { Duration timeout }) async {
596
    await _sendCommand(EnterText(text, timeout: timeout));
597 598
  }

599 600
  /// Configures text entry emulation.
  ///
601
  /// If `enabled` is true, enables text entry emulation via [enterText]. If
602 603 604 605 606 607
  /// `enabled` is false, disables it. By default text entry emulation is
  /// enabled.
  ///
  /// When disabled, [enterText] will fail with a [DriverError]. When an
  /// [EditableText] is focused, the operating system's configured keyboard
  /// method is invoked, such as an on-screen keyboard on a phone or a tablet.
608
  ///
609 610
  /// When enabled, the operating system's configured keyboard will not be
  /// invoked when the widget is focused, as the [SystemChannels.textInput]
611
  /// channel will be mocked out.
612
  Future<void> setTextEntryEmulation({ @required bool enabled, Duration timeout }) async {
613
    assert(enabled != null);
614
    await _sendCommand(SetTextEntryEmulation(enabled, timeout: timeout));
615 616
  }

617 618
  /// Sends a string and returns a string.
  ///
619 620 621 622
  /// This enables generic communication between the driver and the application.
  /// It's expected that the application has registered a [DataHandler]
  /// callback in [enableFlutterDriverExtension] that can successfully handle
  /// these requests.
623
  Future<String> requestData(String message, { Duration timeout }) async {
624
    return RequestDataResult.fromJson(await _sendCommand(RequestData(message, timeout: timeout))).message;
625 626
  }

627 628
  /// Turns semantics on or off in the Flutter app under test.
  ///
629
  /// Returns true when the call actually changed the state from on to off or
630
  /// vice versa.
631
  Future<bool> setSemantics(bool enabled, { Duration timeout }) async {
632
    final SetSemanticsResult result = SetSemanticsResult.fromJson(await _sendCommand(SetSemantics(enabled, timeout: timeout)));
633 634 635
    return result.changedState;
  }

636 637
  /// Retrieves the semantics node id for the object returned by `finder`, or
  /// the nearest ancestor with a semantics node.
638
  ///
639 640
  /// Throws an error if `finder` returns multiple elements or a semantics
  /// node is not found.
641
  ///
642 643
  /// Semantics must be enabled to use this method, either using a platform
  /// specific shell command or [setSemantics].
644
  Future<int> getSemanticsId(SerializableFinder finder, { Duration timeout }) async {
645
    final Map<String, dynamic> jsonResponse = await _sendCommand(GetSemanticsId(finder, timeout: timeout));
646 647 648 649
    final GetSemanticsIdResult result = GetSemanticsIdResult.fromJson(jsonResponse);
    return result.id;
  }

650 651 652 653
  /// Take a screenshot.
  ///
  /// The image will be returned as a PNG.
  Future<List<int>> screenshot() async {
654 655 656 657 658 659 660
    // HACK: this artificial delay here is to deal with a race between the
    //       driver script and the GPU thread. The issue is that driver API
    //       synchronizes with the framework based on transient callbacks, which
    //       are out of sync with the GPU thread. Here's the timeline of events
    //       in ASCII art:
    //
    //       -------------------------------------------------------------------
661
    //       Without this delay:
662 663 664 665 666 667 668 669 670 671
    //       -------------------------------------------------------------------
    //       UI    : <-- build -->
    //       GPU   :               <-- rasterize -->
    //       Gap   :              | random |
    //       Driver:                        <-- screenshot -->
    //
    //       In the diagram above, the gap is the time between the last driver
    //       action taken, such as a `tap()`, and the subsequent call to
    //       `screenshot()`. The gap is random because it is determined by the
    //       unpredictable network communication between the driver process and
672 673 674 675
    //       the application. If this gap is too short, which it typically will
    //       be, the screenshot is taken before the GPU thread is done
    //       rasterizing the frame, so the screenshot of the previous frame is
    //       taken, which is wrong.
676 677
    //
    //       -------------------------------------------------------------------
678
    //       With this delay, if we're lucky:
679 680 681 682 683 684 685 686 687
    //       -------------------------------------------------------------------
    //       UI    : <-- build -->
    //       GPU   :               <-- rasterize -->
    //       Gap   :              |    2 seconds or more   |
    //       Driver:                                        <-- screenshot -->
    //
    //       The two-second gap should be long enough for the GPU thread to
    //       finish rasterizing the frame, but not longer than necessary to keep
    //       driver tests as fast a possible.
688 689 690 691 692 693 694 695 696 697 698 699
    //
    //       -------------------------------------------------------------------
    //       With this delay, if we're not lucky:
    //       -------------------------------------------------------------------
    //       UI    : <-- build -->
    //       GPU   :               <-- rasterize randomly slow today -->
    //       Gap   :              |    2 seconds or more   |
    //       Driver:                                        <-- screenshot -->
    //
    //       In practice, sometimes the device gets really busy for a while and
    //       even two seconds isn't enough, which means that this is still racy
    //       and a source of flakes.
700
    await Future<void>.delayed(const Duration(seconds: 2));
701

702
    final Map<String, dynamic> result = await _peer.sendRequest('_flutter.screenshot');
703
    return base64.decode(result['screenshot']);
704 705
  }

706 707
  /// Returns the Flags set in the Dart VM as JSON.
  ///
708 709
  /// See the complete documentation for [the `getFlagList` Dart VM service
  /// method][getFlagList].
710 711 712 713 714 715 716 717 718 719 720 721 722 723 724
  ///
  /// Example return value:
  ///
  ///     [
  ///       {
  ///         "name": "timeline_recorder",
  ///         "comment": "Select the timeline recorder used. Valid values: ring, endless, startup, and systrace.",
  ///         "modified": false,
  ///         "_flagType": "String",
  ///         "valueAsString": "ring"
  ///       },
  ///       ...
  ///     ]
  ///
  /// [getFlagList]: https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md#getflaglist
725 726
  Future<List<Map<String, dynamic>>> getVmFlags() async {
    final Map<String, dynamic> result = await _peer.sendRequest('getFlagList');
727 728 729
    return result != null
        ? result['flags'].cast<Map<String,dynamic>>()
        : const <Map<String, dynamic>>[];
730 731
  }

732
  /// Starts recording performance traces.
733 734 735 736
  ///
  /// The `timeout` argument causes a warning to be displayed to the user if the
  /// operation exceeds the specified timeout; it does not actually cancel the
  /// operation.
737
  Future<void> startTracing({
738
    List<TimelineStream> streams = _defaultStreams,
739
    Duration timeout = _kUnusuallyLongTimeout,
740
  }) async {
741
    assert(streams != null && streams.isNotEmpty);
742
    assert(timeout != null);
743
    try {
744 745
      await _warnIfSlow<void>(
        future: _peer.sendRequest(_setVMTimelineFlagsMethodName, <String, String>{
746
          'recordedStreams': _timelineStreamsToString(streams),
747 748 749 750
        }),
        timeout: timeout,
        message: 'VM is taking an unusually long time to respond to being told to start tracing...',
      );
751
    } catch (error, stackTrace) {
752
      throw DriverError(
753 754
        'Failed to start tracing due to remote error',
        error,
755
        stackTrace,
756 757 758 759
      );
    }
  }

760
  /// Stops recording performance traces and downloads the timeline.
761 762 763 764 765 766 767 768
  ///
  /// The `timeout` argument causes a warning to be displayed to the user if the
  /// operation exceeds the specified timeout; it does not actually cancel the
  /// operation.
  Future<Timeline> stopTracingAndDownloadTimeline({
    Duration timeout = _kUnusuallyLongTimeout,
  }) async {
    assert(timeout != null);
769
    try {
770 771 772 773 774
      await _warnIfSlow<void>(
        future: _peer.sendRequest(_setVMTimelineFlagsMethodName, <String, String>{'recordedStreams': '[]'}),
        timeout: timeout,
        message: 'VM is taking an unusually long time to respond to being told to stop tracing...',
      );
775
      return Timeline.fromJson(await _peer.sendRequest(_getVMTimelineMethodName));
776
    } catch (error, stackTrace) {
777
      throw DriverError(
778
        'Failed to stop tracing due to remote error',
779
        error,
780
        stackTrace,
781
      );
782 783 784
    }
  }

785 786 787 788 789 790 791 792 793 794
  Future<bool> _isPrecompiledMode() async {
    final List<Map<String, dynamic>> flags = await getVmFlags();
    for(Map<String, dynamic> flag in flags) {
      if (flag['name'] == 'precompiled_mode') {
        return flag['valueAsString'] == 'true';
      }
    }
    return false;
  }

795 796 797 798 799 800
  /// Runs [action] and outputs a performance trace for it.
  ///
  /// Waits for the `Future` returned by [action] to complete prior to stopping
  /// the trace.
  ///
  /// This is merely a convenience wrapper on top of [startTracing] and
801
  /// [stopTracingAndDownloadTimeline].
802 803 804
  ///
  /// [streams] limits the recorded timeline event streams to only the ones
  /// listed. By default, all streams are recorded.
805 806 807 808
  ///
  /// If [retainPriorEvents] is true, retains events recorded prior to calling
  /// [action]. Otherwise, prior events are cleared before calling [action]. By
  /// default, prior events are cleared.
809 810 811
  ///
  /// If this is run in debug mode, a warning message will be printed to suggest
  /// running the benchmark in profile mode instead.
812 813
  Future<Timeline> traceAction(
    Future<dynamic> action(), {
814 815
    List<TimelineStream> streams = _defaultStreams,
    bool retainPriorEvents = false,
816 817 818 819
  }) async {
    if (!retainPriorEvents) {
      await clearTimeline();
    }
820
    await startTracing(streams: streams);
821
    await action();
822 823 824 825

    if (!(await _isPrecompiledMode())) {
      _log.warning(_kDebugWarning);
    }
826
    return stopTracingAndDownloadTimeline();
827 828
  }

829
  /// Clears all timeline events recorded up until now.
830 831 832 833 834
  ///
  /// The `timeout` argument causes a warning to be displayed to the user if the
  /// operation exceeds the specified timeout; it does not actually cancel the
  /// operation.
  Future<void> clearTimeline({
835
    Duration timeout = _kUnusuallyLongTimeout,
836 837
  }) async {
    assert(timeout != null);
838
    try {
839 840 841 842 843
      await _warnIfSlow<void>(
        future: _peer.sendRequest(_clearVMTimelineMethodName, <String, String>{}),
        timeout: timeout,
        message: 'VM is taking an unusually long time to respond to being told to clear its timeline buffer...',
      );
844
    } catch (error, stackTrace) {
845
      throw DriverError(
846 847 848 849 850 851 852
        'Failed to clear event timeline due to remote error',
        error,
        stackTrace,
      );
    }
  }

853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868
  /// [action] will be executed with the frame sync mechanism disabled.
  ///
  /// By default, Flutter Driver waits until there is no pending frame scheduled
  /// in the app under test before executing an action. This mechanism is called
  /// "frame sync". It greatly reduces flakiness because Flutter Driver will not
  /// execute an action while the app under test is undergoing a transition.
  ///
  /// Having said that, sometimes it is necessary to disable the frame sync
  /// mechanism (e.g. if there is an ongoing animation in the app, it will
  /// never reach a state where there are no pending frames scheduled and the
  /// action will time out). For these cases, the sync mechanism can be disabled
  /// by wrapping the actions to be performed by this [runUnsynchronized] method.
  ///
  /// With frame sync disabled, its the responsibility of the test author to
  /// ensure that no action is performed while the app is undergoing a
  /// transition to avoid flakiness.
869
  Future<T> runUnsynchronized<T>(Future<T> action(), { Duration timeout }) async {
870
    await _sendCommand(SetFrameSync(false, timeout: timeout));
871
    T result;
872 873 874
    try {
      result = await action();
    } finally {
875
      await _sendCommand(SetFrameSync(true, timeout: timeout));
876 877 878 879
    }
    return result;
  }

880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895
  /// Force a garbage collection run in the VM.
  Future<void> forceGC() async {
    try {
      await _peer
          .sendRequest(_collectAllGarbageMethodName, <String, String>{
            'isolateId': 'isolates/${_appIsolate.numberAsString}',
          });
    } catch (error, stackTrace) {
      throw DriverError(
        'Failed to force a GC due to remote error',
        error,
        stackTrace,
      );
    }
  }

896 897 898
  /// Closes the underlying connection to the VM service.
  ///
  /// Returns a [Future] that fires once the connection has been closed.
899
  Future<void> close() async {
900
    // Don't leak vm_service_client-specific objects, if any
901 902 903
    await _serviceClient.close();
    await _peer.close();
  }
904
}
yjbanov's avatar
yjbanov committed
905

906
/// Encapsulates connection information to an instance of a Flutter application.
907
@visibleForTesting
908
class VMServiceClientConnection {
909 910 911
  /// Creates an instance of this class given a [client] and a [peer].
  VMServiceClientConnection(this.client, this.peer);

912 913 914 915 916 917 918 919 920 921
  /// Use this for structured access to the VM service's public APIs.
  final VMServiceClient client;

  /// Use this to make arbitrary raw JSON-RPC calls.
  ///
  /// This object allows reaching into private VM service APIs. Use with
  /// caution.
  final rpc.Peer peer;
}

yjbanov's avatar
yjbanov committed
922
/// A function that connects to a Dart VM service given the [url].
923
typedef VMServiceConnectFunction = Future<VMServiceClientConnection> Function(String url);
yjbanov's avatar
yjbanov committed
924 925 926 927 928 929 930 931 932 933 934 935

/// The connection function used by [FlutterDriver.connect].
///
/// Overwrite this function if you require a custom method for connecting to
/// the VM service.
VMServiceConnectFunction vmServiceConnectFunction = _waitAndConnect;

/// Restores [vmServiceConnectFunction] to its default value.
void restoreVmServiceConnectFunction() {
  vmServiceConnectFunction = _waitAndConnect;
}

936 937 938 939 940
void _unhandledJsonRpcError(dynamic error, dynamic stack) {
  _log.trace('Unhandled RPC error:\n$error\n$stack');
  assert(false);
}

yjbanov's avatar
yjbanov committed
941 942
/// Waits for a real Dart VM service to become available, then connects using
/// the [VMServiceClient].
943
Future<VMServiceClientConnection> _waitAndConnect(String url) async {
944
  Uri uri = Uri.parse(url);
945 946 947 948 949 950
  final List<String> pathSegments = <String>[];
  // If there's an authentication code (default), we need to add it to our path.
  if (uri.pathSegments.isNotEmpty) {
    pathSegments.add(uri.pathSegments.first);
  }
  pathSegments.add('ws');
951
  if (uri.scheme == 'http')
952
    uri = uri.replace(scheme: 'ws', pathSegments: pathSegments);
953 954
  int attempts = 0;
  while (true) {
955 956 957
    WebSocket ws1;
    WebSocket ws2;
    try {
958 959
      ws1 = await WebSocket.connect(uri.toString());
      ws2 = await WebSocket.connect(uri.toString());
960 961
      return VMServiceClientConnection(
        VMServiceClient(IOWebSocketChannel(ws1).cast()),
962
        rpc.Peer(IOWebSocketChannel(ws2).cast(), onUnhandledError: _unhandledJsonRpcError)..listen(),
963
      );
964
    } catch (e) {
965 966
      await ws1?.close();
      await ws2?.close();
967 968 969 970
      if (attempts > 5)
        _log.warning('It is taking an unusually long time to connect to the VM...');
      attempts += 1;
      await Future<void>.delayed(_kPauseBetweenReconnectAttempts);
971
    }
yjbanov's avatar
yjbanov committed
972 973
  }
}
974 975 976 977 978

/// Provides convenient accessors to frequently used finders.
class CommonFinders {
  const CommonFinders._();

979
  /// Finds [Text] and [EditableText] widgets containing string equal to [text].
980
  SerializableFinder text(String text) => ByText(text);
981

982
  /// Finds widgets by [key]. Only [String] and [int] values can be used.
983
  SerializableFinder byValueKey(dynamic key) => ByValueKey(key);
984 985

  /// Finds widgets with a tooltip with the given [message].
986
  SerializableFinder byTooltip(String message) => ByTooltipMessage(message);
987

988 989 990
  /// Finds widgets with the given semantics [label].
  SerializableFinder bySemanticsLabel(Pattern label) => BySemanticsLabel(label);

991
  /// Finds widgets whose class name matches the given string.
992
  SerializableFinder byType(String type) => ByType(type);
993 994 995

  /// Finds the back button on a Material or Cupertino page's scaffold.
  SerializableFinder pageBack() => PageBack();
996
}