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

import 'dart:async';
6 7
import 'dart:math' as math;

8
import 'package:dds/dds.dart' as dds;
9 10
import 'package:vm_service/vm_service_io.dart' as vm_service;
import 'package:vm_service/vm_service.dart' as vm_service;
11
import 'package:meta/meta.dart';
12
import 'package:webdriver/async_io.dart' as async_io;
yjbanov's avatar
yjbanov committed
13

14
import '../android/android_device.dart';
15
import '../application_package.dart';
16
import '../artifacts.dart';
Devon Carew's avatar
Devon Carew committed
17
import '../base/common.dart';
18
import '../base/file_system.dart';
19
import '../build_info.dart';
20
import '../convert.dart';
21
import '../dart/package_map.dart';
22
import '../device.dart';
23
import '../globals.dart' as globals;
24
import '../project.dart';
25
import '../resident_runner.dart';
26
import '../runner/flutter_command.dart' show FlutterCommandResult, FlutterOptions;
27
import '../vmservice.dart';
28
import '../web/web_runner.dart';
yjbanov's avatar
yjbanov committed
29 30 31 32 33 34 35 36 37 38 39
import 'run.dart';

/// Runs integration (a.k.a. end-to-end) tests.
///
/// An integration test is a program that runs in a separate process from your
/// Flutter application. It connects to the application and acts like a user,
/// performing taps, scrolls, reading out widget properties and verifying their
/// correctness.
///
/// This command takes a target Flutter application that you would like to test
/// as the `--target` option (defaults to `lib/main.dart`). It then looks for a
40 41
/// corresponding test file within the `test_driver` directory. The test file is
/// expected to have the same name but contain the `_test.dart` suffix. The
42
/// `_test.dart` file would generally be a Dart program that uses
43
/// `package:flutter_driver` and exercises your application. Most commonly it
yjbanov's avatar
yjbanov committed
44 45 46 47 48 49 50
/// is a test written using `package:test`, but you are free to use something
/// else.
///
/// The app and the test are launched simultaneously. Once the test completes
/// the application is stopped and the command exits. If all these steps are
/// successful the exit code will be `0`. Otherwise, you will see a non-zero
/// exit code.
51
class DriveCommand extends RunCommandBase {
52 53 54
  DriveCommand({
    bool verboseHelp = false,
  }) {
55
    requiresPubspecYaml();
56
    addEnableExperimentation(hide: !verboseHelp);
57 58 59 60 61

    // By default, the drive app should not publish the VM service port over mDNS
    // to prevent a local network permission dialog on iOS 14+,
    // which cannot be accepted or dismissed in a CI environment.
    addPublishPort(enabledByDefault: false, verboseHelp: verboseHelp);
62 63 64 65
    argParser
      ..addFlag('keep-app-running',
        defaultsTo: null,
        help: 'Will keep the Flutter application running when done testing.\n'
66 67 68
              'By default, "flutter drive" stops the application after tests are finished, '
              'and --keep-app-running overrides this. On the other hand, if --use-existing-app '
              'is specified, then "flutter drive" instead defaults to leaving the application '
69 70 71
              'running, and --no-keep-app-running overrides it.',
      )
      ..addOption('use-existing-app',
72 73
        help: 'Connect to an already running instance via the given observatory URL. '
              'If this option is given, the application will not be automatically started, '
74 75 76 77
              'and it will only be stopped if --no-keep-app-running is explicitly set.',
        valueHelp: 'url',
      )
      ..addOption('driver',
78 79 80 81 82 83
        help: 'The test file to run on the host (as opposed to the target file to run on '
              'the device).\n'
              'By default, this file has the same base name as the target file, but in the '
              '"test_driver/" directory instead, and with "_test" inserted just before the '
              'extension, so e.g. if the target is "lib/main.dart", the driver will be '
              '"test_driver/main_test.dart".',
84
        valueHelp: 'path',
85 86 87
      )
      ..addFlag('build',
        defaultsTo: true,
88 89
        help: '(Deprecated) Build the app before running. To use an existing app, pass the --use-application-binary '
          'flag with an existing APK',
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
      )
      ..addOption('driver-port',
        defaultsTo: '4444',
        help: 'The port where Webdriver server is launched at. Defaults to 4444.',
        valueHelp: '4444'
      )
      ..addFlag('headless',
        defaultsTo: true,
        help: 'Whether the driver browser is going to be launched in headless mode. Defaults to true.',
      )
      ..addOption('browser-name',
        defaultsTo: 'chrome',
        help: 'Name of browser where tests will be executed. \n'
              'Following browsers are supported: \n'
              'Chrome, Firefox, Safari (macOS and iOS) and Edge. Defaults to Chrome.',
        allowed: <String>[
106
          'android-chrome',
107 108 109 110 111 112 113 114 115 116 117 118
          'chrome',
          'edge',
          'firefox',
          'ios-safari',
          'safari',
        ]
      )
      ..addOption('browser-dimension',
        defaultsTo: '1600,1024',
        help: 'The dimension of browser when running Flutter Web test. \n'
              'This will affect screenshot and all offset-related actions. \n'
              'By default. it is set to 1600,1024 (1600 by 1024).',
119 120 121 122
      )
      ..addFlag('android-emulator',
        defaultsTo: true,
        help: 'Whether to perform Flutter Driver testing on Android Emulator.'
123 124 125
          'Works only if \'browser-name\' is set to \'android-chrome\'')
      ..addOption('chrome-binary',
        help: 'Location of Chrome binary. '
126 127 128 129 130 131
          'Works only if \'browser-name\' is set to \'chrome\'')
      ..addOption('write-sksl-on-exit',
        help:
          'Attempts to write an SkSL file when the drive process is finished '
          'to the provided file, overwriting it if necessary.',
      );
yjbanov's avatar
yjbanov committed
132 133
  }

134
  @override
135
  final String name = 'drive';
136 137

  @override
138
  final String description = 'Run integration tests for the project on an attached device or emulator.';
139 140

  @override
141 142 143 144
  final List<String> aliases = <String>['driver'];

  Device _device;
  Device get device => _device;
yjbanov's avatar
yjbanov committed
145

146
  bool get verboseSystemLogs => boolArg('verbose-system-logs');
147
  String get userIdentifier => stringArg(FlutterOptions.kDeviceUser);
148

149
  /// Subscription to log messages printed on the device or simulator.
150
  // ignore: cancel_subscriptions
151 152
  StreamSubscription<String> _deviceLogSubscription;

153 154 155
  @override
  Future<void> validateCommand() async {
    if (userIdentifier != null) {
156
      final Device device = await findTargetDevice(timeout: deviceDiscoveryTimeout);
157 158 159 160 161 162 163
      if (device is! AndroidDevice) {
        throwToolExit('--${FlutterOptions.kDeviceUser} is only supported for Android');
      }
    }
    return super.validateCommand();
  }

164
  @override
165
  Future<FlutterCommandResult> runCommand() async {
166
    final String testFile = _getTestFile();
167
    if (testFile == null) {
168
      throwToolExit(null);
169
    }
yjbanov's avatar
yjbanov committed
170

171
    _device = await findTargetDevice(timeout: deviceDiscoveryTimeout);
172
    if (device == null) {
173
      throwToolExit(null);
174
    }
175

176
    if (await globals.fs.type(testFile) != FileSystemEntityType.file) {
177
      throwToolExit('Test file not found: $testFile');
178
    }
yjbanov's avatar
yjbanov committed
179

180
    String observatoryUri;
181
    ResidentRunner residentRunner;
182
    final BuildInfo buildInfo = getBuildInfo();
183
    final bool isWebPlatform = await device.targetPlatform == TargetPlatform.web_javascript;
184 185 186 187 188 189 190 191
    final File applicationBinary = stringArg('use-application-binary') == null
      ? null
      : globals.fs.file(stringArg('use-application-binary'));
    final ApplicationPackage package = await applicationPackages.getPackageForPlatform(
      await device.targetPlatform,
      buildInfo: buildInfo,
      applicationBinary: applicationBinary,
    );
192
    if (argResults['use-existing-app'] == null) {
193
      globals.printStatus('Starting application: $targetFile');
194

195
      if (buildInfo.isRelease && !isWebPlatform) {
196
        // This is because we need VM service to be able to drive the app.
197
        // For Flutter Web, testing in release mode is allowed.
198
        throwToolExit(
199
          'Flutter Driver (non-web) does not support running in release mode.\n'
200 201 202 203 204 205
          '\n'
          'Use --profile mode for testing application performance.\n'
          'Use --debug (default) mode for testing correctness (with assertions).'
        );
      }

206 207 208 209 210 211 212 213 214
      Uri webUri;

      if (isWebPlatform) {
        // Start Flutter web application for current test
        final FlutterProject flutterProject = FlutterProject.current();
        final FlutterDevice flutterDevice = await FlutterDevice.create(
          device,
          flutterProject: flutterProject,
          target: targetFile,
215
          buildInfo: buildInfo,
216
          platform: globals.platform,
217 218 219 220 221 222
        );
        residentRunner = webRunnerFactory.createWebRunner(
          flutterDevice,
          target: targetFile,
          flutterProject: flutterProject,
          ipv6: ipv6,
223 224 225 226 227 228 229
          debuggingOptions: getBuildInfo().isRelease ?
            DebuggingOptions.disabled(
              getBuildInfo(),
              port: stringArg('web-port')
            )
            : DebuggingOptions.enabled(
              getBuildInfo(),
230 231
              port: stringArg('web-port'),
              disablePortPublication: disablePortPublication,
232
            ),
233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248
          stayResident: false,
          urlTunneller: null,
        );
        final Completer<void> appStartedCompleter = Completer<void>.sync();
        final int result = await residentRunner.run(
          appStartedCompleter: appStartedCompleter,
          route: route,
        );
        if (result != 0) {
          throwToolExit(null, exitCode: result);
        }
        // Wait until the app is started.
        await appStartedCompleter.future;
        webUri = residentRunner.uri;
      }

249 250 251 252 253 254 255 256 257 258 259 260 261
      // Attempt to launch the application up to 3 times, to validate whether it
      // is possible to reduce flakiness by hardnening the launch code.
      int attempt = 0;
      LaunchResult result;
      while (attempt < 3) {
        // On attempts past 1, assume the application is built correctly and re-use it.
        result = await appStarter(this, webUri, package, applicationBinary != null || attempt > 0);
        if (result != null) {
          break;
        }
        attempt += 1;
        globals.printError('Application failed to start on attempt: $attempt');
      }
262
      if (result == null) {
263
        throwToolExit('Application failed to start. Will not run test. Quitting.', exitCode: 1);
264
      }
265
      observatoryUri = result.observatoryUri.toString();
266
      // TODO(bkonyi): add web support (https://github.com/flutter/flutter/issues/61259)
267
      if (!isWebPlatform && !disableDds) {
268 269 270 271
        try {
          // If there's another flutter_tools instance still connected to the target
          // application, DDS will already be running remotely and this call will fail.
          // We can ignore this and continue to use the remote DDS instance.
272 273 274 275 276 277 278
          await device.dds.startDartDevelopmentService(
            Uri.parse(observatoryUri),
            ddsPort,
            ipv6,
            disableServiceAuthCodes,
          );
          observatoryUri = device.dds.uri.toString();
279 280 281 282
        } on dds.DartDevelopmentServiceException catch(_) {
          globals.printTrace('Note: DDS is already connected to $observatoryUri.');
        }
      }
283
    } else {
284
      globals.printStatus('Will connect to already running application instance.');
285
      observatoryUri = stringArg('use-existing-app');
yjbanov's avatar
yjbanov committed
286 287
    }

288 289 290 291
    final Map<String, String> environment = <String, String>{
      'VM_SERVICE_URL': observatoryUri,
    };

292
    async_io.WebDriver driver;
293 294 295
    // For web device, WebDriver session will be launched beforehand
    // so that FlutterDriver can reuse it.
    if (isWebPlatform) {
296 297 298
      final Browser browser = _browserNameToEnum(
          argResults['browser-name'].toString());
      final String driverPort = argResults['driver-port'].toString();
299
      // start WebDriver
300 301 302 303 304
      try {
        driver = await _createDriver(
          driverPort,
          browser,
          argResults['headless'].toString() == 'true',
305
          stringArg('chrome-binary'),
306 307 308 309 310 311 312 313 314
        );
      } on Exception catch (ex) {
        throwToolExit(
          'Unable to start WebDriver Session for Flutter for Web testing. \n'
          'Make sure you have the correct WebDriver Server running at $driverPort. \n'
          'Make sure the WebDriver Server matches option --browser-name. \n'
          '$ex'
        );
      }
315

316 317
      final bool isAndroidChrome = browser == Browser.androidChrome;
      final bool useEmulator = argResults['android-emulator'] as bool;
318
      // set window size
319 320 321 322 323 324 325 326 327 328 329
      // for android chrome, skip such action
      if (!isAndroidChrome) {
        final List<String> dimensions = argResults['browser-dimension'].split(
            ',') as List<String>;
        assert(dimensions.length == 2);
        int x, y;
        try {
          x = int.parse(dimensions[0]);
          y = int.parse(dimensions[1]);
        } on FormatException catch (ex) {
          throwToolExit('''
330 331 332
Dimension provided to --browser-dimension is invalid:
$ex
        ''');
333 334
        }
        final async_io.Window window = await driver.window;
335 336
        await window.setLocation(const math.Point<int>(0, 0));
        await window.setSize(math.Rectangle<int>(0, 0, x, y));
337 338 339 340 341 342 343
      }

      // add driver info to environment variables
      environment.addAll(<String, String> {
        'DRIVER_SESSION_ID': driver.id,
        'DRIVER_SESSION_URI': driver.uri.toString(),
        'DRIVER_SESSION_SPEC': driver.spec.toString(),
344
        'DRIVER_SESSION_CAPABILITIES': json.encode(driver.capabilities),
345 346
        'SUPPORT_TIMELINE_ACTION': (browser == Browser.chrome).toString(),
        'FLUTTER_WEB_TEST': 'true',
347
        'ANDROID_CHROME_ON_EMULATOR': (isAndroidChrome && useEmulator).toString(),
348 349 350
      });
    }

yjbanov's avatar
yjbanov committed
351
    try {
352 353 354 355 356 357 358 359 360 361 362 363
      await testRunner(
        <String>[
          if (buildInfo.dartExperiments.isNotEmpty)
            '--enable-experiment=${buildInfo.dartExperiments.join(',')}',
          if (buildInfo.nullSafetyMode == NullSafetyMode.sound)
            '--sound-null-safety',
          if (buildInfo.nullSafetyMode == NullSafetyMode.unsound)
            '--no-sound-null-safety',
          testFile,
        ],
        environment,
      );
364
    } on Exception catch (error, stackTrace) {
365
      if (error is ToolExit) {
366
        rethrow;
367
      }
368
      throw Exception('Unable to run test: $error\n$stackTrace');
yjbanov's avatar
yjbanov committed
369
    } finally {
370
      await residentRunner?.exit();
371
      await driver?.quit();
372 373 374 375 376 377 378 379 380 381 382
      if (stringArg('write-sksl-on-exit') != null) {
        final File outputFile = globals.fs.file(stringArg('write-sksl-on-exit'));
        final vm_service.VmService vmService = await connectToVmService(
          Uri.parse(observatoryUri),
        );
        final FlutterView flutterView = (await vmService.getFlutterViews()).first;
        final Map<String, Object> result = await vmService.getSkSLs(
          viewId: flutterView.id
        );
        await sharedSkSlWriter(_device, result, outputFile: outputFile);
      }
383
      if (boolArg('keep-app-running') ?? (argResults['use-existing-app'] != null)) {
384
        globals.printStatus('Leaving the application running.');
385
      } else {
386
        globals.printStatus('Stopping application instance.');
387
        await appStopper(this, package);
388
      }
389 390

      await device?.dispose();
yjbanov's avatar
yjbanov committed
391
    }
392

393
    return FlutterCommandResult.success();
yjbanov's avatar
yjbanov committed
394 395 396
  }

  String _getTestFile() {
397
    if (argResults['driver'] != null) {
398
      return stringArg('driver');
399
    }
400 401 402

    // If the --driver argument wasn't provided, then derive the value from
    // the target file.
403
    String appFile = globals.fs.path.normalize(targetFile);
404

405
    // This command extends `flutter run` and therefore CWD == package dir
406
    final String packageDir = globals.fs.currentDirectory.path;
407 408 409

    // Make appFile path relative to package directory because we are looking
    // for the corresponding test file relative to it.
410 411 412
    if (!globals.fs.path.isRelative(appFile)) {
      if (!globals.fs.path.isWithin(packageDir, appFile)) {
        globals.printError(
413 414 415 416 417
          'Application file $appFile is outside the package directory $packageDir'
        );
        return null;
      }

418
      appFile = globals.fs.path.relative(appFile, from: packageDir);
419 420
    }

421
    final List<String> parts = globals.fs.path.split(appFile);
422 423

    if (parts.length < 2) {
424
      globals.printError(
425 426 427 428 429 430 431 432 433
        'Application file $appFile must reside in one of the sub-directories '
        'of the package structure, not in the root directory.'
      );
      return null;
    }

    // Look for the test file inside `test_driver/` matching the sub-path, e.g.
    // if the application is `lib/foo/bar.dart`, the test file is expected to
    // be `test_driver/foo/bar_test.dart`.
434
    final String pathWithNoExtension = globals.fs.path.withoutExtension(globals.fs.path.joinAll(
435
      <String>[packageDir, 'test_driver', ...parts.skip(1)]));
436
    return '${pathWithNoExtension}_test${globals.fs.path.extension(appFile)}';
yjbanov's avatar
yjbanov committed
437 438
  }
}
439

440
Future<Device> findTargetDevice({ @required Duration timeout }) async {
441
  final DeviceManager deviceManager = globals.deviceManager;
442
  final List<Device> devices = await deviceManager.findTargetDevices(null, timeout: timeout);
443

444
  if (deviceManager.hasSpecifiedDeviceId) {
445
    if (devices.isEmpty) {
446
      globals.printStatus("No devices found with name or id matching '${deviceManager.specifiedDeviceId}'");
447 448 449
      return null;
    }
    if (devices.length > 1) {
450
      globals.printStatus("Found ${devices.length} devices with name or id matching '${deviceManager.specifiedDeviceId}':");
451
      await Device.printDevices(devices, globals.logger);
452 453 454
      return null;
    }
    return devices.first;
455 456
  }

457
  if (devices.isEmpty) {
458
    globals.printError('No devices found.');
459
    return null;
460
  } else if (devices.length > 1) {
461
    globals.printStatus('Found multiple connected devices:');
462
    await Device.printDevices(devices, globals.logger);
463
  }
464
  globals.printStatus('Using device ${devices.first.name}.');
465
  return devices.first;
466 467 468
}

/// Starts the application on the device given command configuration.
469
typedef AppStarter = Future<LaunchResult> Function(DriveCommand command, Uri webUri, ApplicationPackage applicationPackage, bool prebuiltApplication);
470

471
AppStarter appStarter = _startApp; // (mutable for testing)
472
void restoreAppStarter() {
473
  appStarter = _startApp;
474 475
}

476 477
Future<LaunchResult> _startApp(
  DriveCommand command,
478 479 480 481
  Uri webUri,
  ApplicationPackage applicationPackage,
  bool prebuiltApplication,
) async {
482
  final String mainPath = findMainDartFile(command.targetFile);
483 484
  if (await globals.fs.type(mainPath) != FileSystemEntityType.file) {
    globals.printError('Tried to run $mainPath, but that file does not exist.');
485
    return null;
486 487
  }

488
  globals.printTrace('Stopping previously running application, if any.');
489 490
  await appStopper(command, applicationPackage);

491

492
  final Map<String, dynamic> platformArgs = <String, dynamic>{};
493
  if (command.traceStartup) {
494
    platformArgs['trace-startup'] = command.traceStartup;
495
  }
496

497 498 499 500 501 502 503 504 505
  if (webUri != null) {
    platformArgs['uri'] = webUri.toString();
    if (!command.getBuildInfo().isDebug) {
      // For web device, startApp will be triggered twice
      // and it will error out for chrome the second time.
      platformArgs['no-launch-chrome'] = true;
    }
  }

506
  globals.printTrace('Starting application.');
507

508
  // Forward device log messages to the terminal window running the "drive" command.
509
  final DeviceLogReader logReader = await command.device.getLogReader(app: applicationPackage);
510 511 512 513
  command._deviceLogSubscription = logReader
    .logLines
    .listen(globals.printStatus);

514
  final LaunchResult result = await command.device.startApp(
515
    applicationPackage,
516 517
    mainPath: mainPath,
    route: command.route,
518
    debuggingOptions: DebuggingOptions.enabled(
519
      command.getBuildInfo(),
Devon Carew's avatar
Devon Carew committed
520
      startPaused: true,
521
      hostVmServicePort: webUri != null ? command.hostVmservicePort : 0,
522
      disablePortPublication: command.disablePortPublication,
523
      ddsPort: command.ddsPort,
524
      verboseSystemLogs: command.verboseSystemLogs,
525
      cacheSkSL: command.cacheSkSL,
526
      dumpSkpOnShaderCompilation: command.dumpSkpOnShaderCompilation,
527
      purgePersistentCache: command.purgePersistentCache,
Devon Carew's avatar
Devon Carew committed
528
    ),
529
    platformArgs: platformArgs,
530 531
    userIdentifier: command.userIdentifier,
    prebuiltApplication: prebuiltApplication,
532 533
  );

534
  if (!result.started) {
535
    await command._deviceLogSubscription.cancel();
536
    return null;
537 538
  }

539
  return result;
540 541 542
}

/// Runs driver tests.
543
typedef TestRunner = Future<void> Function(List<String> testArgs, Map<String, String> environment);
544
TestRunner testRunner = _runTests;
545
void restoreTestRunner() {
546
  testRunner = _runTests;
547 548
}

549
Future<void> _runTests(List<String> testArgs, Map<String, String> environment) async {
550
  globals.printTrace('Running driver tests.');
551

552
  globalPackagesPath = globals.fs.path.normalize(globals.fs.path.absolute(globalPackagesPath));
553
  final int result = await globals.processUtils.stream(
554
    <String>[
555
      globals.artifacts.getArtifactPath(Artifact.engineDartBinary),
556
      ...testArgs,
557
      '--packages=$globalPackagesPath',
558 559
      '-rexpanded',
    ],
560
    environment: environment,
561
  );
562
  if (result != 0) {
563
    throwToolExit('Driver tests failed: $result', exitCode: result);
564
  }
565 566 567 568
}


/// Stops the application.
569
typedef AppStopper = Future<bool> Function(DriveCommand command, ApplicationPackage applicationPackage);
570
AppStopper appStopper = _stopApp;
571
void restoreAppStopper() {
572
  appStopper = _stopApp;
573 574
}

575
Future<bool> _stopApp(DriveCommand command, ApplicationPackage package) async {
576
  globals.printTrace('Stopping application.');
577
  final bool stopped = await command.device.stopApp(package, userIdentifier: command.userIdentifier);
578
  await command.device.uninstallApp(package);
579
  await command._deviceLogSubscription?.cancel();
580
  return stopped;
581
}
582

583
/// A list of supported browsers.
584 585
@visibleForTesting
enum Browser {
586 587
  /// Chrome on Android: https://developer.chrome.com/multidevice/android/overview
  androidChrome,
588 589 590 591 592 593 594 595 596 597 598 599 600 601 602
  /// Chrome: https://www.google.com/chrome/
  chrome,
  /// Edge: https://www.microsoft.com/en-us/windows/microsoft-edge
  edge,
  /// Firefox: https://www.mozilla.org/en-US/firefox/
  firefox,
  /// Safari in iOS: https://www.apple.com/safari/
  iosSafari,
  /// Safari in macOS: https://www.apple.com/safari/
  safari,
}

/// Converts [browserName] string to [Browser]
Browser _browserNameToEnum(String browserName){
  switch (browserName) {
603
    case 'android-chrome': return Browser.androidChrome;
604 605 606 607 608 609 610 611 612
    case 'chrome': return Browser.chrome;
    case 'edge': return Browser.edge;
    case 'firefox': return Browser.firefox;
    case 'ios-safari': return Browser.iosSafari;
    case 'safari': return Browser.safari;
  }
  throw UnsupportedError('Browser $browserName not supported');
}

613
Future<async_io.WebDriver> _createDriver(String driverPort, Browser browser, bool headless, String chromeBinary) async {
614 615
  return async_io.createDriver(
      uri: Uri.parse('http://localhost:$driverPort/'),
616
      desired: getDesiredCapabilities(browser, headless, chromeBinary),
617
      spec: async_io.WebDriverSpec.Auto
618 619 620
  );
}

621 622
/// Returns desired capabilities for given [browser], [headless] and
/// [chromeBinary].
623
@visibleForTesting
624
Map<String, dynamic> getDesiredCapabilities(Browser browser, bool headless, [String chromeBinary]) {
625 626 627 628 629
  switch (browser) {
    case Browser.chrome:
      return <String, dynamic>{
        'acceptInsecureCerts': true,
        'browserName': 'chrome',
630
        'goog:loggingPrefs': <String, String>{ async_io.LogType.performance: 'ALL'},
631
        'chromeOptions': <String, dynamic>{
632 633
          if (chromeBinary != null)
            'binary': chromeBinary,
634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652
          'w3c': false,
          'args': <String>[
            '--bwsi',
            '--disable-background-timer-throttling',
            '--disable-default-apps',
            '--disable-extensions',
            '--disable-popup-blocking',
            '--disable-translate',
            '--no-default-browser-check',
            '--no-sandbox',
            '--no-first-run',
            if (headless) '--headless'
          ],
          'perfLoggingPrefs': <String, String>{
            'traceCategories':
            'devtools.timeline,'
                'v8,blink.console,benchmark,blink,'
                'blink.user_timing'
          }
653
        },
654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694
      };
      break;
    case Browser.firefox:
      return <String, dynamic>{
        'acceptInsecureCerts': true,
        'browserName': 'firefox',
        'moz:firefoxOptions' : <String, dynamic>{
          'args': <String>[
            if (headless) '-headless'
          ],
          'prefs': <String, dynamic>{
            'dom.file.createInChild': true,
            'dom.timeout.background_throttling_max_budget': -1,
            'media.autoplay.default': 0,
            'media.gmp-manager.url': '',
            'media.gmp-provider.enabled': false,
            'network.captive-portal-service.enabled': false,
            'security.insecure_field_warning.contextual.enabled': false,
            'test.currentTimeOffsetSeconds': 11491200
          },
          'log': <String, String>{'level': 'trace'}
        }
      };
      break;
    case Browser.edge:
      return <String, dynamic>{
        'acceptInsecureCerts': true,
        'browserName': 'edge',
      };
      break;
    case Browser.safari:
      return <String, dynamic>{
        'browserName': 'safari',
      };
      break;
    case Browser.iosSafari:
      return <String, dynamic>{
        'platformName': 'ios',
        'browserName': 'safari',
        'safari:useSimulator': true
      };
695 696 697 698 699 700 701 702 703
    case Browser.androidChrome:
      return <String, dynamic>{
        'browserName': 'chrome',
        'platformName': 'android',
        'goog:chromeOptions': <String, dynamic>{
          'androidPackage': 'com.android.chrome',
          'args': <String>['--disable-fullscreen']
        },
      };
704 705 706 707
    default:
      throw UnsupportedError('Browser $browser not supported.');
  }
}