driver.dart 49.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
import '../common/diagnostics_tree.dart';
19 20 21
import '../common/error.dart';
import '../common/find.dart';
import '../common/frame_sync.dart';
22
import '../common/fuchsia_compat.dart';
23
import '../common/geometry.dart';
24 25 26 27 28 29
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';
30
import '../common/text.dart';
31
import '../common/wait.dart';
32
import 'common.dart';
33
import 'timeline.dart';
34

35
/// Timeline stream identifier.
36
enum TimelineStream {
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
  /// 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,
63 64
}

65
const List<TimelineStream> _defaultStreams = <TimelineStream>[TimelineStream.all];
66

67 68
/// How long to wait before showing a message saying that
/// things seem to be taking a long time.
69 70
@visibleForTesting
const Duration kUnusuallyLongTimeout = Duration(seconds: 5);
71 72 73

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

76
// See https://github.com/dart-lang/sdk/blob/master/runtime/vm/timeline.cc#L32
77
String _timelineStreamsToString(List<TimelineStream> streams) {
78
  final String contents = streams.map<String>((TimelineStream stream) {
79
    switch (stream) {
80 81 82 83 84 85 86 87 88
      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';
89
      default:
90
        throw 'Unknown timeline stream $stream';
91 92 93 94 95
    }
  }).join(', ');
  return '[$contents]';
}

96
final Logger _log = Logger('FlutterDriver');
97

98 99 100 101 102 103 104 105
Future<T> _warnIfSlow<T>({
  @required Future<T> future,
  @required Duration timeout,
  @required String message,
}) {
  assert(future != null);
  assert(timeout != null);
  assert(message != null);
106
  return future..timeout(timeout, onTimeout: () { _log.warning(message); return null; });
107 108
}

109 110 111 112
/// A convenient accessor to frequently used finders.
///
/// Examples:
///
113
///     driver.tap(find.text('Save'));
114
///     driver.scroll(find.byValueKey(42));
115
const CommonFinders find = CommonFinders._();
116

117 118 119 120 121
/// Computes a value.
///
/// If computation is asynchronous, the function may return a [Future].
///
/// See also [FlutterDriver.waitFor].
122
typedef EvaluatorFunction = dynamic Function();
123

124 125
/// Drives a Flutter Application running in another process.
class FlutterDriver {
126
  /// Creates a driver that uses a connection provided by the given
127
  /// [serviceClient], [_peer] and [appIsolate].
128
  @visibleForTesting
129
  FlutterDriver.connectedTo(
130
    this.serviceClient,
131
    this._peer,
132
    this.appIsolate, {
133 134
    bool printCommunication = false,
    bool logCommunicationToFile = true,
135 136 137
  }) : _printCommunication = printCommunication,
       _logCommunicationToFile = logCommunicationToFile,
       _driverId = _nextDriverId++;
138

139
  static const String _flutterExtensionMethodName = 'ext.flutter.driver';
140 141 142
  static const String _setVMTimelineFlagsMethodName = 'setVMTimelineFlags';
  static const String _getVMTimelineMethodName = 'getVMTimeline';
  static const String _clearVMTimelineMethodName = 'clearVMTimeline';
143
  static const String _collectAllGarbageMethodName = '_collectAllGarbage';
144

145 146
  static int _nextDriverId = 0;

147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
  // 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        ┊
│                                                       ┊
└─────────────────────────────────────────────────╌┄┈  🐢
''';

163 164 165 166
  /// Connects to a Flutter application.
  ///
  /// Resumes the application if it is currently paused (e.g. at a breakpoint).
  ///
167
  /// `dartVmServiceUrl` is the URL to Dart observatory (a.k.a. VM service). If
168
  /// not specified, the URL specified by the `VM_SERVICE_URL` environment
169
  /// variable is used. One or the other must be specified.
170
  ///
171
  /// `printCommunication` determines whether the command communication between
172 173
  /// the test and the app should be printed to stdout.
  ///
174
  /// `logCommunicationToFile` determines whether the command communication
175
  /// between the test and the app should be logged to `flutter_driver_commands.log`.
176
  ///
177
  /// `isolateNumber` determines the specific isolate to connect to.
178
  /// If this is left as `null`, will connect to the first isolate found
179 180 181 182 183 184 185 186 187 188 189 190
  /// 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.
191 192
  static Future<FlutterDriver> connect({
    String dartVmServiceUrl,
193 194
    bool printCommunication = false,
    bool logCommunicationToFile = true,
195
    int isolateNumber,
196
    Pattern fuchsiaModuleTarget,
197
  }) async {
198
    // If running on a Fuchsia device, connect to the first isolate whose name
199 200 201 202 203
    // 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) {
204 205 206
      // TODO(awdavies): Use something other than print. On fuchsia
      // `stderr`/`stdout` appear to have issues working correctly.
      flutterDriverLog.listen(print);
207 208
      fuchsiaModuleTarget ??= Platform.environment['FUCHSIA_MODULE_TARGET'];
      if (fuchsiaModuleTarget == null) {
209 210 211 212 213
        throw DriverError(
          'No Fuchsia module target has been specified.\n'
          'Please make sure to specify the FUCHSIA_MODULE_TARGET '
          'environment variable.'
        );
214 215 216 217 218 219 220 221 222 223 224 225
      }
      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();
    }

226
    dartVmServiceUrl ??= Platform.environment['VM_SERVICE_URL'];
227 228

    if (dartVmServiceUrl == null) {
229
      throw DriverError(
230 231 232 233
        '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.'
      );
234
    }
235

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

248
    VMIsolate isolate = await isolateRef.loadRunnable();
249

250
    // TODO(yjbanov): vm_service_client does not support "None" pause event yet.
251 252
    // 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,
253 254
    // list all the events we know about. Later we'll check for "None" event
    // explicitly.
255
    //
256 257 258 259 260 261 262
    // 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) {
263
      isolate = await isolateRef.loadRunnable();
264 265
    }

266
    final FlutterDriver driver = FlutterDriver.connectedTo(
267 268 269
      client, connection.peer, isolate,
      printCommunication: printCommunication,
      logCommunicationToFile: logCommunicationToFile,
270
    );
271

272 273
    driver._dartVmReconnectUrl = dartVmServiceUrl;

274 275 276
    // 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
277
    Future<dynamic> resumeLeniently() {
278
      _log.trace('Attempting to resume isolate');
279 280
      return isolate.resume().catchError((dynamic e) {
        const int vmMustBePausedCode = 101;
281 282 283 284 285 286 287 288 289 290 291 292 293 294
        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;
        }
      });
    }

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

    /// 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
310
    Future<void> enableIsolateStreams() async {
311 312 313 314 315
      await connection.peer.sendRequest('streamListen', <String, String>{
        'streamId': 'Isolate',
      });
    }

316 317 318 319 320 321 322
    // 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.
323
      await enableIsolateStreams();
324
      final Future<String> whenServiceExtensionReady = waitForServiceExtension();
325
      final Future<dynamic> whenResumed = resumeLeniently();
326 327
      await whenResumed;

328 329 330 331 332
      _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,
333
        timeout: kUnusuallyLongTimeout,
334 335 336
        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 '
337
                 'calls enableFlutterDriverExtension() as the first call in main().',
338
      );
339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355
    } 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.'
      );
    }

356 357 358 359 360 361 362 363 364 365 366 367 368 369 370
    // 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();
371
        await waitForServiceExtension();
372 373 374 375 376
        return driver.checkHealth();
      }
    }

    final Health health = await checkHealth();
377
    if (health.status != HealthStatus.ok) {
378
      await client.close();
379
      throw DriverError('Flutter application health check failed.');
380 381 382 383 384 385
    }

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

386 387
  /// The unique ID of this driver instance.
  final int _driverId;
388

389
  /// Client connected to the Dart VM running the Flutter application
390 391 392 393 394 395
  ///
  /// You can use [VMServiceClient] to check VM version, flags and get
  /// notified when a new isolate has been instantiated. That could be
  /// useful if your application spawns multiple isolates that you
  /// would like to instrument.
  final VMServiceClient serviceClient;
396

397
  /// JSON-RPC client useful for sending raw JSON requests.
398 399 400 401 402 403 404 405 406 407 408 409 410 411 412
  rpc.Peer _peer;

  String _dartVmReconnectUrl;

  Future<void> _restorePeerConnectionIfNeeded() async {
    if (!_peer.isClosed || _dartVmReconnectUrl == null) {
      return;
    }

    _log.warning(
        'Peer connection is closed! Trying to restore the connection...'
    );

    final String webSocketUrl = _getWebSocketUrl(_dartVmReconnectUrl);
    final WebSocket ws = await WebSocket.connect(webSocketUrl);
413
    ws.done.whenComplete(() => _checkCloseCode(ws));
414 415 416 417 418
    _peer = rpc.Peer(
        IOWebSocketChannel(ws).cast(),
        onUnhandledError: _unhandledJsonRpcError,
    )..listen();
  }
419

420 421 422 423 424 425
  /// The main isolate hosting the Flutter application.
  ///
  /// If you used the [registerExtension] API to instrument your application,
  /// you can use this [VMIsolate] to call these extension methods via
  /// [invokeExtension].
  final VMIsolate appIsolate;
426

427 428
  /// Whether to print communication between host and app to `stdout`.
  final bool _printCommunication;
429

430 431
  /// Whether to log communication between host and app to `flutter_driver_commands.log`.
  final bool _logCommunicationToFile;
432 433

  Future<Map<String, dynamic>> _sendCommand(Command command) async {
434
    Map<String, dynamic> response;
435
    try {
436
      final Map<String, String> serialized = command.serialize();
437
      _logCommunication('>>> $serialized');
438
      final Future<Map<String, dynamic>> future = appIsolate.invokeExtension(
439 440 441 442 443
        _flutterExtensionMethodName,
        serialized,
      ).then<Map<String, dynamic>>((Object value) => value);
      response = await _warnIfSlow<Map<String, dynamic>>(
        future: future,
444
        timeout: command.timeout ?? kUnusuallyLongTimeout,
445
        message: '${command.kind} message is taking a long time to complete...',
446
      );
447
      _logCommunication('<<< $response');
448
    } catch (error, stackTrace) {
449
      throw DriverError(
450 451
        'Failed to fulfill ${command.runtimeType} due to remote error',
        error,
452
        stackTrace,
453 454
      );
    }
455
    if (response['isError'])
456
      throw DriverError('Error in Flutter application: ${response['response']}');
457
    return response['response'];
458 459
  }

460
  void _logCommunication(String message) {
461 462 463
    if (_printCommunication)
      _log.info(message);
    if (_logCommunicationToFile) {
464
      final f.File file = fs.file(p.join(testOutputsDirectory, 'flutter_driver_commands_$_driverId.log'));
465
      file.createSync(recursive: true); // no-op if file exists
466
      file.writeAsStringSync('${DateTime.now()} $message\n', mode: f.FileMode.append, flush: true);
467 468 469
    }
  }

470
  /// Checks the status of the Flutter Driver extension.
471
  Future<Health> checkHealth({ Duration timeout }) async {
472
    return Health.fromJson(await _sendCommand(GetHealth(timeout: timeout)));
473 474
  }

475
  /// Returns a dump of the render tree.
476
  Future<RenderTree> getRenderTree({ Duration timeout }) async {
477
    return RenderTree.fromJson(await _sendCommand(GetRenderTree(timeout: timeout)));
478 479
  }

480
  /// Taps at the center of the widget located by [finder].
481
  Future<void> tap(SerializableFinder finder, { Duration timeout }) async {
482
    await _sendCommand(Tap(finder, timeout: timeout));
483
  }
484

485
  /// Waits until [finder] locates the target.
486
  Future<void> waitFor(SerializableFinder finder, { Duration timeout }) async {
487
    await _sendCommand(WaitFor(finder, timeout: timeout));
488 489
  }

490
  /// Waits until [finder] can no longer locate the target.
491
  Future<void> waitForAbsent(SerializableFinder finder, { Duration timeout }) async {
492
    await _sendCommand(WaitForAbsent(finder, timeout: timeout));
493 494
  }

495 496 497 498 499
  /// Waits until the given [waitCondition] is satisfied.
  Future<void> waitForCondition(SerializableWaitCondition waitCondition, {Duration timeout}) async {
    await _sendCommand(WaitForCondition(waitCondition, timeout: timeout));
  }

500 501 502 503
  /// 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].
504
  Future<void> waitUntilNoTransientCallbacks({ Duration timeout }) async {
505
    await _sendCommand(WaitForCondition(const NoTransientCallbacks(), timeout: timeout));
506 507
  }

508 509 510 511 512
  /// Waits until the next [Window.onReportTimings] is called.
  ///
  /// Use this method to wait for the first frame to be rasterized during the
  /// app launch.
  Future<void> waitUntilFirstFrameRasterized() async {
513
    await _sendCommand(const WaitForCondition(FirstFrameRasterized()));
514 515
  }

516 517 518 519 520 521 522
  Future<DriverOffset> _getOffset(SerializableFinder finder, OffsetType type, { Duration timeout }) async {
    final GetOffset command = GetOffset(finder, type, timeout: timeout);
    final GetOffsetResult result = GetOffsetResult.fromJson(await _sendCommand(command));
    return DriverOffset(result.dx, result.dy);
  }

  /// Returns the point at the top left of the widget identified by `finder`.
523 524 525
  ///
  /// The offset is expressed in logical pixels and can be translated to
  /// device pixels via [Window.devicePixelRatio].
526 527 528 529 530
  Future<DriverOffset> getTopLeft(SerializableFinder finder, { Duration timeout }) async {
    return _getOffset(finder, OffsetType.topLeft, timeout: timeout);
  }

  /// Returns the point at the top right of the widget identified by `finder`.
531 532 533
  ///
  /// The offset is expressed in logical pixels and can be translated to
  /// device pixels via [Window.devicePixelRatio].
534 535 536 537 538
  Future<DriverOffset> getTopRight(SerializableFinder finder, { Duration timeout }) async {
    return _getOffset(finder, OffsetType.topRight, timeout: timeout);
  }

  /// Returns the point at the bottom left of the widget identified by `finder`.
539 540 541
  ///
  /// The offset is expressed in logical pixels and can be translated to
  /// device pixels via [Window.devicePixelRatio].
542 543 544 545 546
  Future<DriverOffset> getBottomLeft(SerializableFinder finder, { Duration timeout }) async {
    return _getOffset(finder, OffsetType.bottomLeft, timeout: timeout);
  }

  /// Returns the point at the bottom right of the widget identified by `finder`.
547 548 549
  ///
  /// The offset is expressed in logical pixels and can be translated to
  /// device pixels via [Window.devicePixelRatio].
550 551 552 553 554
  Future<DriverOffset> getBottomRight(SerializableFinder finder, { Duration timeout }) async {
    return _getOffset(finder, OffsetType.bottomRight, timeout: timeout);
  }

  /// Returns the point at the center of the widget identified by `finder`.
555 556 557
  ///
  /// The offset is expressed in logical pixels and can be translated to
  /// device pixels via [Window.devicePixelRatio].
558 559 560 561
  Future<DriverOffset> getCenter(SerializableFinder finder, { Duration timeout }) async {
    return _getOffset(finder, OffsetType.center, timeout: timeout);
  }

562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630
  /// Returns a JSON map of the [DiagnosticsNode] that is associated with the
  /// [RenderObject] identified by `finder`.
  ///
  /// The `subtreeDepth` argument controls how many layers of children will be
  /// included in the result. It defaults to zero, which means that no children
  /// of the [RenderObject] identified by `finder` will be part of the result.
  ///
  /// The `includeProperties` argument controls whether properties of the
  /// [DiagnosticsNode]s will be included in the result. It defaults to true.
  ///
  /// [RenderObject]s are responsible for positioning, layout, and painting on
  /// the screen, based on the configuration from a [Widget]. Callers that need
  /// information about size or position should use this method.
  ///
  /// A widget may indirectly create multiple [RenderObject]s, which each
  /// implement some aspect of the widget configuration. A 1:1 relationship
  /// should not be assumed.
  ///
  /// See also:
  ///
  ///  * [getWidgetDiagnostics], which gets the [DiagnosticsNode] of a [Widget].
  Future<Map<String, Object>> getRenderObjectDiagnostics(
      SerializableFinder finder, {
      int subtreeDepth = 0,
      bool includeProperties = true,
      Duration timeout,
  }) async {
    return _sendCommand(GetDiagnosticsTree(
      finder,
      DiagnosticsType.renderObject,
      subtreeDepth: subtreeDepth,
      includeProperties: includeProperties,
      timeout: timeout,
    ));
  }

  /// Returns a JSON map of the [DiagnosticsNode] that is associated with the
  /// [Widget] identified by `finder`.
  ///
  /// The `subtreeDepth` argument controls how many layers of children will be
  /// included in the result. It defaults to zero, which means that no children
  /// of the [Widget] identified by `finder` will be part of the result.
  ///
  /// The `includeProperties` argument controls whether properties of the
  /// [DiagnosticsNode]s will be included in the result. It defaults to true.
  ///
  /// [Widget]s describe configuration for the rendering tree. Individual
  /// widgets may create multiple [RenderObject]s to actually layout and paint
  /// the desired configuration.
  ///
  /// See also:
  ///
  ///  * [getRenderObjectDiagnostics], which gets the [DiagnosticsNode] of a
  ///    [RenderObject].
  Future<Map<String, Object>> getWidgetDiagnostics(
    SerializableFinder finder, {
    int subtreeDepth = 0,
    bool includeProperties = true,
    Duration timeout,
  }) async {
    return _sendCommand(GetDiagnosticsTree(
      finder,
      DiagnosticsType.renderObject,
      subtreeDepth: subtreeDepth,
      includeProperties: includeProperties,
      timeout: timeout,
    ));
  }

631 632 633 634 635 636 637 638 639
  /// 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.
  ///
640
  /// [duration] specifies the length of the action.
641 642 643
  ///
  /// The move events are generated at a given [frequency] in Hz (or events per
  /// second). It defaults to 60Hz.
644 645
  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));
646 647
  }

648 649
  /// Scrolls the Scrollable ancestor of the widget located by [finder]
  /// until the widget is completely visible.
650 651 652 653 654
  ///
  /// 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.
655 656
  Future<void> scrollIntoView(SerializableFinder finder, { double alignment = 0.0, Duration timeout }) async {
    await _sendCommand(ScrollIntoView(finder, alignment: alignment, timeout: timeout));
657 658
  }

659 660 661 662 663
  /// 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].
664
  /// Typically `find.byType('ListView')` or `find.byType('CustomScrollView')`.
665
  ///
666
  /// At least one of [dxScroll] and [dyScroll] must be non-zero.
667 668 669 670
  ///
  /// 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
671
  /// if [item] is above, then specify a positive value for [dyScroll].
672
  ///
673
  /// If [item] is to the right of the currently visible items, then
674 675
  /// specify a negative value for [dxScroll] that's a small enough increment to
  /// expose [item] without potentially scrolling it up and completely out of
676
  /// view. Similarly if [item] is to the left, then specify a positive value
677 678 679
  /// for [dyScroll].
  ///
  /// The [timeout] value should be long enough to accommodate as many scrolls
680
  /// as needed to bring an item into view. The default is to not time out.
681 682 683
  Future<void> scrollUntilVisible(
    SerializableFinder scrollable,
    SerializableFinder item, {
684 685 686
    double alignment = 0.0,
    double dxScroll = 0.0,
    double dyScroll = 0.0,
687
    Duration timeout,
688 689 690 691 692 693 694 695
  }) async {
    assert(scrollable != null);
    assert(item != null);
    assert(alignment != null);
    assert(dxScroll != null);
    assert(dyScroll != null);
    assert(dxScroll != 0.0 || dyScroll != 0.0);

696 697 698 699
    // 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.
700
    bool isVisible = false;
701 702
    waitFor(item, timeout: timeout).then<void>((_) { isVisible = true; });
    await Future<void>.delayed(const Duration(milliseconds: 500));
703 704
    while (!isVisible) {
      await scroll(scrollable, dxScroll, dyScroll, const Duration(milliseconds: 100));
705
      await Future<void>.delayed(const Duration(milliseconds: 500));
706 707 708 709 710
    }

    return scrollIntoView(item, alignment: alignment);
  }

711
  /// Returns the text in the `Text` widget located by [finder].
712
  Future<String> getText(SerializableFinder finder, { Duration timeout }) async {
713
    return GetTextResult.fromJson(await _sendCommand(GetText(finder, timeout: timeout))).text;
714 715
  }

716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732
  /// 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.
  ///
733 734 735
  /// For this method to work, text emulation must be enabled (see
  /// [setTextEntryEmulation]). Text emulation is enabled by default.
  ///
736 737 738 739
  /// Example:
  ///
  /// ```dart
  /// test('enters text in a text field', () async {
740 741 742 743 744 745
  ///   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
746 747
  /// });
  /// ```
748
  Future<void> enterText(String text, { Duration timeout }) async {
749
    await _sendCommand(EnterText(text, timeout: timeout));
750 751
  }

752 753
  /// Configures text entry emulation.
  ///
754
  /// If `enabled` is true, enables text entry emulation via [enterText]. If
755 756 757 758 759 760
  /// `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.
761
  ///
762 763
  /// When enabled, the operating system's configured keyboard will not be
  /// invoked when the widget is focused, as the [SystemChannels.textInput]
764
  /// channel will be mocked out.
765
  Future<void> setTextEntryEmulation({ @required bool enabled, Duration timeout }) async {
766
    assert(enabled != null);
767
    await _sendCommand(SetTextEntryEmulation(enabled, timeout: timeout));
768 769
  }

770 771
  /// Sends a string and returns a string.
  ///
772 773 774 775
  /// 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.
776
  Future<String> requestData(String message, { Duration timeout }) async {
777
    return RequestDataResult.fromJson(await _sendCommand(RequestData(message, timeout: timeout))).message;
778 779
  }

780 781
  /// Turns semantics on or off in the Flutter app under test.
  ///
782
  /// Returns true when the call actually changed the state from on to off or
783
  /// vice versa.
784
  Future<bool> setSemantics(bool enabled, { Duration timeout }) async {
785
    final SetSemanticsResult result = SetSemanticsResult.fromJson(await _sendCommand(SetSemantics(enabled, timeout: timeout)));
786 787 788
    return result.changedState;
  }

789 790
  /// Retrieves the semantics node id for the object returned by `finder`, or
  /// the nearest ancestor with a semantics node.
791
  ///
792 793
  /// Throws an error if `finder` returns multiple elements or a semantics
  /// node is not found.
794
  ///
795 796
  /// Semantics must be enabled to use this method, either using a platform
  /// specific shell command or [setSemantics].
797
  Future<int> getSemanticsId(SerializableFinder finder, { Duration timeout }) async {
798
    final Map<String, dynamic> jsonResponse = await _sendCommand(GetSemanticsId(finder, timeout: timeout));
799 800 801 802
    final GetSemanticsIdResult result = GetSemanticsIdResult.fromJson(jsonResponse);
    return result.id;
  }

803 804 805 806
  /// Take a screenshot.
  ///
  /// The image will be returned as a PNG.
  Future<List<int>> screenshot() async {
807 808 809 810 811 812 813
    // 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:
    //
    //       -------------------------------------------------------------------
814
    //       Without this delay:
815 816 817 818 819 820 821 822 823 824
    //       -------------------------------------------------------------------
    //       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
825 826 827 828
    //       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.
829 830
    //
    //       -------------------------------------------------------------------
831
    //       With this delay, if we're lucky:
832 833 834 835 836 837 838 839 840
    //       -------------------------------------------------------------------
    //       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.
841 842 843 844 845 846 847 848 849 850 851 852
    //
    //       -------------------------------------------------------------------
    //       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.
853
    await Future<void>.delayed(const Duration(seconds: 2));
854

855
    final Map<String, dynamic> result = await _peer.sendRequest('_flutter.screenshot');
856
    return base64.decode(result['screenshot']);
857 858
  }

859 860
  /// Returns the Flags set in the Dart VM as JSON.
  ///
861 862
  /// See the complete documentation for [the `getFlagList` Dart VM service
  /// method][getFlagList].
863 864 865 866 867 868 869 870 871 872 873 874 875 876 877
  ///
  /// 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
878
  Future<List<Map<String, dynamic>>> getVmFlags() async {
879
    await _restorePeerConnectionIfNeeded();
880
    final Map<String, dynamic> result = await _peer.sendRequest('getFlagList');
881 882 883
    return result != null
        ? result['flags'].cast<Map<String,dynamic>>()
        : const <Map<String, dynamic>>[];
884 885
  }

886
  /// Starts recording performance traces.
887 888 889 890
  ///
  /// 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.
891
  Future<void> startTracing({
892
    List<TimelineStream> streams = _defaultStreams,
893
    Duration timeout = kUnusuallyLongTimeout,
894
  }) async {
895
    assert(streams != null && streams.isNotEmpty);
896
    assert(timeout != null);
897
    try {
898 899
      await _warnIfSlow<void>(
        future: _peer.sendRequest(_setVMTimelineFlagsMethodName, <String, String>{
900
          'recordedStreams': _timelineStreamsToString(streams),
901 902 903 904
        }),
        timeout: timeout,
        message: 'VM is taking an unusually long time to respond to being told to start tracing...',
      );
905
    } catch (error, stackTrace) {
906
      throw DriverError(
907 908
        'Failed to start tracing due to remote error',
        error,
909
        stackTrace,
910 911 912 913
      );
    }
  }

914
  /// Stops recording performance traces and downloads the timeline.
915 916 917 918 919
  ///
  /// 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({
920
    Duration timeout = kUnusuallyLongTimeout,
921 922
  }) async {
    assert(timeout != null);
923
    try {
924 925 926 927 928
      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...',
      );
929
      return Timeline.fromJson(await _peer.sendRequest(_getVMTimelineMethodName));
930
    } catch (error, stackTrace) {
931
      throw DriverError(
932
        'Failed to stop tracing due to remote error',
933
        error,
934
        stackTrace,
935
      );
936 937 938
    }
  }

939 940 941 942 943 944 945 946 947 948
  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;
  }

949 950 951 952 953 954
  /// 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
955
  /// [stopTracingAndDownloadTimeline].
956 957 958
  ///
  /// [streams] limits the recorded timeline event streams to only the ones
  /// listed. By default, all streams are recorded.
959 960 961 962
  ///
  /// 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.
963 964 965
  ///
  /// If this is run in debug mode, a warning message will be printed to suggest
  /// running the benchmark in profile mode instead.
966 967
  Future<Timeline> traceAction(
    Future<dynamic> action(), {
968 969
    List<TimelineStream> streams = _defaultStreams,
    bool retainPriorEvents = false,
970 971 972 973
  }) async {
    if (!retainPriorEvents) {
      await clearTimeline();
    }
974
    await startTracing(streams: streams);
975
    await action();
976 977 978 979

    if (!(await _isPrecompiledMode())) {
      _log.warning(_kDebugWarning);
    }
980
    return stopTracingAndDownloadTimeline();
981 982
  }

983
  /// Clears all timeline events recorded up until now.
984 985 986 987 988
  ///
  /// 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({
989
    Duration timeout = kUnusuallyLongTimeout,
990 991
  }) async {
    assert(timeout != null);
992
    try {
993 994 995 996 997
      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...',
      );
998
    } catch (error, stackTrace) {
999
      throw DriverError(
1000 1001 1002 1003 1004 1005 1006
        'Failed to clear event timeline due to remote error',
        error,
        stackTrace,
      );
    }
  }

1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022
  /// [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.
1023
  Future<T> runUnsynchronized<T>(Future<T> action(), { Duration timeout }) async {
1024
    await _sendCommand(SetFrameSync(false, timeout: timeout));
1025
    T result;
1026 1027 1028
    try {
      result = await action();
    } finally {
1029
      await _sendCommand(SetFrameSync(true, timeout: timeout));
1030 1031 1032 1033
    }
    return result;
  }

1034 1035 1036 1037 1038
  /// Force a garbage collection run in the VM.
  Future<void> forceGC() async {
    try {
      await _peer
          .sendRequest(_collectAllGarbageMethodName, <String, String>{
1039
            'isolateId': 'isolates/${appIsolate.numberAsString}',
1040 1041 1042 1043 1044 1045 1046 1047 1048 1049
          });
    } catch (error, stackTrace) {
      throw DriverError(
        'Failed to force a GC due to remote error',
        error,
        stackTrace,
      );
    }
  }

1050 1051 1052
  /// Closes the underlying connection to the VM service.
  ///
  /// Returns a [Future] that fires once the connection has been closed.
1053
  Future<void> close() async {
1054
    // Don't leak vm_service_client-specific objects, if any
1055
    await serviceClient.close();
1056 1057
    await _peer.close();
  }
1058
}
yjbanov's avatar
yjbanov committed
1059

1060
/// Encapsulates connection information to an instance of a Flutter application.
1061
@visibleForTesting
1062
class VMServiceClientConnection {
1063 1064 1065
  /// Creates an instance of this class given a [client] and a [peer].
  VMServiceClientConnection(this.client, this.peer);

1066 1067 1068 1069 1070 1071 1072 1073 1074 1075
  /// 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
1076
/// A function that connects to a Dart VM service given the [url].
1077
typedef VMServiceConnectFunction = Future<VMServiceClientConnection> Function(String url);
yjbanov's avatar
yjbanov committed
1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089

/// 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;
}

1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114
/// The JSON RPC 2 spec says that a notification from a client must not respond
/// to the client. It's possible the client sent a notification as a "ping", but
/// the service isn't set up yet to respond.
///
/// For example, if the client sends a notification message to the server for
/// 'streamNotify', but the server has not finished loading, it will throw an
/// exception. Since the message is a notification, the server follows the
/// specification and does not send a response back, but is left with an
/// unhandled exception. That exception is safe for us to ignore - the client
/// is signaling that it will try again later if it doesn't get what it wants
/// here by sending a notification.
// This may be ignoring too many exceptions. It would be best to rewrite
// the client code to not use notifications so that it gets error replies back
// and can decide what to do from there.
// TODO(dnfield): https://github.com/flutter/flutter/issues/31813
bool _ignoreRpcError(dynamic error) {
  if (error is rpc.RpcException) {
    final rpc.RpcException exception = error;
    return exception.data == null || exception.data['id'] == null;
  } else if (error is String && error.startsWith('JSON-RPC error -32601')) {
    return true;
  }
  return false;
}

1115
void _unhandledJsonRpcError(dynamic error, dynamic stack) {
1116 1117 1118
  if (_ignoreRpcError(error)) {
    return;
  }
1119
  _log.trace('Unhandled RPC error:\n$error\n$stack');
1120 1121
  // TODO(dnfield): https://github.com/flutter/flutter/issues/31813
  // assert(false);
1122 1123
}

1124
String _getWebSocketUrl(String url) {
1125
  Uri uri = Uri.parse(url);
1126 1127 1128 1129 1130
  final List<String> pathSegments = <String>[
    // If there's an authentication code (default), we need to add it to our path.
    if (uri.pathSegments.isNotEmpty) uri.pathSegments.first,
    'ws',
  ];
1131
  if (uri.scheme == 'http')
1132
    uri = uri.replace(scheme: 'ws', pathSegments: pathSegments);
1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145
  return uri.toString();
}

void _checkCloseCode(WebSocket ws) {
  if (ws.closeCode != 1000 && ws.closeCode != null) {
    _log.warning('$ws is closed with an unexpected code ${ws.closeCode}');
  }
}

/// Waits for a real Dart VM service to become available, then connects using
/// the [VMServiceClient].
Future<VMServiceClientConnection> _waitAndConnect(String url) async {
  final String webSocketUrl = _getWebSocketUrl(url);
1146 1147
  int attempts = 0;
  while (true) {
1148 1149 1150
    WebSocket ws1;
    WebSocket ws2;
    try {
1151 1152 1153
      ws1 = await WebSocket.connect(webSocketUrl);
      ws2 = await WebSocket.connect(webSocketUrl);

1154 1155
      ws1.done.whenComplete(() => _checkCloseCode(ws1));
      ws2.done.whenComplete(() => _checkCloseCode(ws2));
1156

1157 1158
      return VMServiceClientConnection(
        VMServiceClient(IOWebSocketChannel(ws1).cast()),
1159 1160 1161 1162
        rpc.Peer(
            IOWebSocketChannel(ws2).cast(),
            onUnhandledError: _unhandledJsonRpcError,
        )..listen(),
1163
      );
1164
    } catch (e) {
1165 1166
      await ws1?.close();
      await ws2?.close();
1167 1168 1169 1170
      if (attempts > 5)
        _log.warning('It is taking an unusually long time to connect to the VM...');
      attempts += 1;
      await Future<void>.delayed(_kPauseBetweenReconnectAttempts);
1171
    }
yjbanov's avatar
yjbanov committed
1172 1173
  }
}
1174 1175 1176 1177 1178

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

1179
  /// Finds [Text] and [EditableText] widgets containing string equal to [text].
1180
  SerializableFinder text(String text) => ByText(text);
1181

1182
  /// Finds widgets by [key]. Only [String] and [int] values can be used.
1183
  SerializableFinder byValueKey(dynamic key) => ByValueKey(key);
1184 1185

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

1188 1189 1190
  /// Finds widgets with the given semantics [label].
  SerializableFinder bySemanticsLabel(Pattern label) => BySemanticsLabel(label);

1191
  /// Finds widgets whose class name matches the given string.
1192
  SerializableFinder byType(String type) => ByType(type);
1193 1194

  /// Finds the back button on a Material or Cupertino page's scaffold.
1195
  SerializableFinder pageBack() => const PageBack();
1196 1197 1198 1199 1200 1201

  /// Finds the widget that is an ancestor of the `of` parameter and that
  /// matches the `matching` parameter.
  ///
  /// If the `matchRoot` argument is true then the widget specified by `of` will
  /// be considered for a match. The argument defaults to false.
1202 1203 1204
  ///
  /// If `firstMatchOnly` is true then only the first ancestor matching
  /// `matching` will be returned. Defaults to false.
1205 1206 1207 1208
  SerializableFinder ancestor({
    @required SerializableFinder of,
    @required SerializableFinder matching,
    bool matchRoot = false,
1209 1210
    bool firstMatchOnly = false,
  }) => Ancestor(of: of, matching: matching, matchRoot: matchRoot, firstMatchOnly: firstMatchOnly);
1211 1212 1213 1214 1215 1216

  /// Finds the widget that is an descendant of the `of` parameter and that
  /// matches the `matching` parameter.
  ///
  /// If the `matchRoot` argument is true then the widget specified by `of` will
  /// be considered for a match. The argument defaults to false.
1217 1218 1219
  ///
  /// If `firstMatchOnly` is true then only the first descendant matching
  /// `matching` will be returned. Defaults to false.
1220 1221 1222 1223
  SerializableFinder descendant({
    @required SerializableFinder of,
    @required SerializableFinder matching,
    bool matchRoot = false,
1224 1225
    bool firstMatchOnly = false,
  }) => Descendant(of: of, matching: matching, matchRoot: matchRoot, firstMatchOnly: firstMatchOnly);
1226
}
1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252

/// An immutable 2D floating-point offset used by Flutter Driver.
class DriverOffset {
  /// Creates an offset.
  const DriverOffset(this.dx, this.dy);

  /// The x component of the offset.
  final double dx;

  /// The y component of the offset.
  final double dy;

  @override
  String toString() => '$runtimeType($dx, $dy)';

  @override
  bool operator ==(dynamic other) {
    if (other is! DriverOffset)
      return false;
    final DriverOffset typedOther = other;
    return dx == typedOther.dx && dy == typedOther.dy;
  }

  @override
  int get hashCode => dx.hashCode + dy.hashCode;
}