drive.dart 24.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 '../base/process.dart';
20
import '../build_info.dart';
21
import '../convert.dart';
22
import '../dart/package_map.dart';
23
import '../device.dart';
24
import '../globals.dart' as globals;
25
import '../project.dart';
26
import '../resident_runner.dart';
27
import '../runner/flutter_command.dart' show FlutterCommandResult, FlutterOptions;
28
import '../vmservice.dart';
29
import '../web/web_runner.dart';
yjbanov's avatar
yjbanov committed
30 31 32 33 34 35 36 37 38 39 40
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
41 42
/// 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
43
/// `_test.dart` file would generally be a Dart program that uses
44
/// `package:flutter_driver` and exercises your application. Most commonly it
yjbanov's avatar
yjbanov committed
45 46 47 48 49 50 51
/// 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.
52
class DriveCommand extends RunCommandBase {
53 54 55
  DriveCommand({
    bool verboseHelp = false,
  }) {
56
    requiresPubspecYaml();
57
    addEnableExperimentation(hide: !verboseHelp);
58 59 60 61
    argParser
      ..addFlag('keep-app-running',
        defaultsTo: null,
        help: 'Will keep the Flutter application running when done testing.\n'
62 63 64
              '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 '
65 66 67
              'running, and --no-keep-app-running overrides it.',
      )
      ..addOption('use-existing-app',
68 69
        help: 'Connect to an already running instance via the given observatory URL. '
              'If this option is given, the application will not be automatically started, '
70 71 72 73
              'and it will only be stopped if --no-keep-app-running is explicitly set.',
        valueHelp: 'url',
      )
      ..addOption('driver',
74 75 76 77 78 79
        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".',
80
        valueHelp: 'path',
81 82 83 84
      )
      ..addFlag('build',
        defaultsTo: true,
        help: 'Build the app before running.',
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
      )
      ..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>[
101
          'android-chrome',
102 103 104 105 106 107 108 109 110 111 112 113
          '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).',
114 115 116 117
      )
      ..addFlag('android-emulator',
        defaultsTo: true,
        help: 'Whether to perform Flutter Driver testing on Android Emulator.'
118 119 120
          'Works only if \'browser-name\' is set to \'android-chrome\'')
      ..addOption('chrome-binary',
        help: 'Location of Chrome binary. '
121 122 123 124 125 126
          '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
127 128
  }

129
  @override
130
  final String name = 'drive';
131 132

  @override
133
  final String description = 'Run integration tests for the project on an attached device or emulator.';
134 135

  @override
136 137 138 139
  final List<String> aliases = <String>['driver'];

  Device _device;
  Device get device => _device;
140
  bool get shouldBuild => boolArg('build');
yjbanov's avatar
yjbanov committed
141

142
  bool get verboseSystemLogs => boolArg('verbose-system-logs');
143
  String get userIdentifier => stringArg(FlutterOptions.kDeviceUser);
144

145
  /// Subscription to log messages printed on the device or simulator.
146
  // ignore: cancel_subscriptions
147 148
  StreamSubscription<String> _deviceLogSubscription;

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

160
  @override
161
  Future<FlutterCommandResult> runCommand() async {
162
    final String testFile = _getTestFile();
163
    if (testFile == null) {
164
      throwToolExit(null);
165
    }
yjbanov's avatar
yjbanov committed
166

167
    _device = await findTargetDevice(timeout: deviceDiscoveryTimeout);
168
    if (device == null) {
169
      throwToolExit(null);
170
    }
171

172
    if (await globals.fs.type(testFile) != FileSystemEntityType.file) {
173
      throwToolExit('Test file not found: $testFile');
174
    }
yjbanov's avatar
yjbanov committed
175

176
    String observatoryUri;
177
    ResidentRunner residentRunner;
178
    final BuildInfo buildInfo = getBuildInfo();
179
    final bool isWebPlatform = await device.targetPlatform == TargetPlatform.web_javascript;
180
    if (argResults['use-existing-app'] == null) {
181
      globals.printStatus('Starting application: $targetFile');
182

183
      if (buildInfo.isRelease && !isWebPlatform) {
184
        // This is because we need VM service to be able to drive the app.
185
        // For Flutter Web, testing in release mode is allowed.
186
        throwToolExit(
187
          'Flutter Driver (non-web) does not support running in release mode.\n'
188 189 190 191 192 193
          '\n'
          'Use --profile mode for testing application performance.\n'
          'Use --debug (default) mode for testing correctness (with assertions).'
        );
      }

194 195 196 197 198 199 200 201 202
      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,
203
          buildInfo: buildInfo,
204
          platform: globals.platform,
205 206 207 208 209 210
        );
        residentRunner = webRunnerFactory.createWebRunner(
          flutterDevice,
          target: targetFile,
          flutterProject: flutterProject,
          ipv6: ipv6,
211 212 213 214 215 216 217 218 219
          debuggingOptions: getBuildInfo().isRelease ?
            DebuggingOptions.disabled(
              getBuildInfo(),
              port: stringArg('web-port')
            )
            : DebuggingOptions.enabled(
              getBuildInfo(),
              port: stringArg('web-port')
            ),
220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236
          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;
      }

      final LaunchResult result = await appStarter(this, webUri);
237
      if (result == null) {
238
        throwToolExit('Application failed to start. Will not run test. Quitting.', exitCode: 1);
239
      }
240
      observatoryUri = result.observatoryUri.toString();
241
      // TODO(bkonyi): add web support (https://github.com/flutter/flutter/issues/61259)
242
      if (!isWebPlatform && !disableDds) {
243 244 245 246
        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.
247 248 249 250 251 252 253
          await device.dds.startDartDevelopmentService(
            Uri.parse(observatoryUri),
            ddsPort,
            ipv6,
            disableServiceAuthCodes,
          );
          observatoryUri = device.dds.uri.toString();
254 255 256 257
        } on dds.DartDevelopmentServiceException catch(_) {
          globals.printTrace('Note: DDS is already connected to $observatoryUri.');
        }
      }
258
    } else {
259
      globals.printStatus('Will connect to already running application instance.');
260
      observatoryUri = stringArg('use-existing-app');
yjbanov's avatar
yjbanov committed
261 262
    }

263 264 265 266
    final Map<String, String> environment = <String, String>{
      'VM_SERVICE_URL': observatoryUri,
    };

267
    async_io.WebDriver driver;
268 269 270
    // For web device, WebDriver session will be launched beforehand
    // so that FlutterDriver can reuse it.
    if (isWebPlatform) {
271 272 273
      final Browser browser = _browserNameToEnum(
          argResults['browser-name'].toString());
      final String driverPort = argResults['driver-port'].toString();
274
      // start WebDriver
275 276 277 278 279
      try {
        driver = await _createDriver(
          driverPort,
          browser,
          argResults['headless'].toString() == 'true',
280
          stringArg('chrome-binary'),
281 282 283 284 285 286 287 288 289
        );
      } 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'
        );
      }
290

291 292
      final bool isAndroidChrome = browser == Browser.androidChrome;
      final bool useEmulator = argResults['android-emulator'] as bool;
293
      // set window size
294 295 296 297 298 299 300 301 302 303 304
      // 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('''
305 306 307
Dimension provided to --browser-dimension is invalid:
$ex
        ''');
308 309
        }
        final async_io.Window window = await driver.window;
310 311
        await window.setLocation(const math.Point<int>(0, 0));
        await window.setSize(math.Rectangle<int>(0, 0, x, y));
312 313 314 315 316 317 318
      }

      // 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(),
319
        'DRIVER_SESSION_CAPABILITIES': json.encode(driver.capabilities),
320 321
        'SUPPORT_TIMELINE_ACTION': (browser == Browser.chrome).toString(),
        'FLUTTER_WEB_TEST': 'true',
322
        'ANDROID_CHROME_ON_EMULATOR': (isAndroidChrome && useEmulator).toString(),
323 324 325
      });
    }

yjbanov's avatar
yjbanov committed
326
    try {
327 328 329 330 331 332 333 334 335 336 337 338
      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,
      );
339
    } on Exception catch (error, stackTrace) {
340
      if (error is ToolExit) {
341
        rethrow;
342
      }
343
      throw Exception('Unable to run test: $error\n$stackTrace');
yjbanov's avatar
yjbanov committed
344
    } finally {
345
      await residentRunner?.exit();
346
      await driver?.quit();
347 348 349 350 351 352 353 354 355 356 357
      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);
      }
358
      if (boolArg('keep-app-running') ?? (argResults['use-existing-app'] != null)) {
359
        globals.printStatus('Leaving the application running.');
360
      } else {
361
        globals.printStatus('Stopping application instance.');
362
        await appStopper(this);
363
      }
yjbanov's avatar
yjbanov committed
364
    }
365

366
    return FlutterCommandResult.success();
yjbanov's avatar
yjbanov committed
367 368 369
  }

  String _getTestFile() {
370
    if (argResults['driver'] != null) {
371
      return stringArg('driver');
372
    }
373 374 375

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

378
    // This command extends `flutter run` and therefore CWD == package dir
379
    final String packageDir = globals.fs.currentDirectory.path;
380 381 382

    // Make appFile path relative to package directory because we are looking
    // for the corresponding test file relative to it.
383 384 385
    if (!globals.fs.path.isRelative(appFile)) {
      if (!globals.fs.path.isWithin(packageDir, appFile)) {
        globals.printError(
386 387 388 389 390
          'Application file $appFile is outside the package directory $packageDir'
        );
        return null;
      }

391
      appFile = globals.fs.path.relative(appFile, from: packageDir);
392 393
    }

394
    final List<String> parts = globals.fs.path.split(appFile);
395 396

    if (parts.length < 2) {
397
      globals.printError(
398 399 400 401 402 403 404 405 406
        '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`.
407
    final String pathWithNoExtension = globals.fs.path.withoutExtension(globals.fs.path.joinAll(
408
      <String>[packageDir, 'test_driver', ...parts.skip(1)]));
409
    return '${pathWithNoExtension}_test${globals.fs.path.extension(appFile)}';
yjbanov's avatar
yjbanov committed
410 411
  }
}
412

413
Future<Device> findTargetDevice({ @required Duration timeout }) async {
414
  final DeviceManager deviceManager = globals.deviceManager;
415
  final List<Device> devices = await deviceManager.findTargetDevices(FlutterProject.current(), timeout: timeout);
416

417
  if (deviceManager.hasSpecifiedDeviceId) {
418
    if (devices.isEmpty) {
419
      globals.printStatus("No devices found with name or id matching '${deviceManager.specifiedDeviceId}'");
420 421 422
      return null;
    }
    if (devices.length > 1) {
423
      globals.printStatus("Found ${devices.length} devices with name or id matching '${deviceManager.specifiedDeviceId}':");
424
      await Device.printDevices(devices);
425 426 427
      return null;
    }
    return devices.first;
428 429
  }

430
  if (devices.isEmpty) {
431
    globals.printError('No devices found.');
432
    return null;
433
  } else if (devices.length > 1) {
434
    globals.printStatus('Found multiple connected devices:');
435
    await Device.printDevices(devices);
436
  }
437
  globals.printStatus('Using device ${devices.first.name}.');
438
  return devices.first;
439 440 441
}

/// Starts the application on the device given command configuration.
442
typedef AppStarter = Future<LaunchResult> Function(DriveCommand command, Uri webUri);
443

444
AppStarter appStarter = _startApp; // (mutable for testing)
445
void restoreAppStarter() {
446
  appStarter = _startApp;
447 448
}

449 450 451 452 453
Future<LaunchResult> _startApp(
  DriveCommand command,
  Uri webUri, {
  String userIdentifier,
}) async {
454
  final String mainPath = findMainDartFile(command.targetFile);
455 456
  if (await globals.fs.type(mainPath) != FileSystemEntityType.file) {
    globals.printError('Tried to run $mainPath, but that file does not exist.');
457
    return null;
458 459
  }

460
  globals.printTrace('Stopping previously running application, if any.');
461 462
  await appStopper(command);

463
  final ApplicationPackage package = await command.applicationPackages
464
      .getPackageForPlatform(await command.device.targetPlatform, command.getBuildInfo());
465 466

  if (command.shouldBuild) {
467
    globals.printTrace('Installing application package.');
468 469
    if (await command.device.isAppInstalled(package, userIdentifier: userIdentifier)) {
      await command.device.uninstallApp(package, userIdentifier: userIdentifier);
470
    }
471
    await command.device.installApp(package, userIdentifier: userIdentifier);
472
  }
473

474
  final Map<String, dynamic> platformArgs = <String, dynamic>{};
475
  if (command.traceStartup) {
476
    platformArgs['trace-startup'] = command.traceStartup;
477
  }
478

479 480 481 482 483 484 485 486 487
  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;
    }
  }

488
  globals.printTrace('Starting application.');
489 490

  // Forward device log messages to the terminal window running the "drive" command.
491 492 493 494
  final DeviceLogReader logReader = await command.device.getLogReader(app: package);
  command._deviceLogSubscription = logReader
    .logLines
    .listen(globals.printStatus);
495

496
  final LaunchResult result = await command.device.startApp(
497 498 499
    package,
    mainPath: mainPath,
    route: command.route,
500
    debuggingOptions: DebuggingOptions.enabled(
501
      command.getBuildInfo(),
Devon Carew's avatar
Devon Carew committed
502
      startPaused: true,
503 504
      hostVmServicePort: webUri != null ? command.hostVmservicePort : 0,
      ddsPort: command.ddsPort,
505
      verboseSystemLogs: command.verboseSystemLogs,
506
      cacheSkSL: command.cacheSkSL,
507
      dumpSkpOnShaderCompilation: command.dumpSkpOnShaderCompilation,
508
      purgePersistentCache: command.purgePersistentCache,
Devon Carew's avatar
Devon Carew committed
509
    ),
510
    platformArgs: platformArgs,
511
    prebuiltApplication: !command.shouldBuild,
512
    userIdentifier: userIdentifier,
513 514
  );

515 516
  if (!result.started) {
    await command._deviceLogSubscription.cancel();
517
    return null;
518 519
  }

520
  return result;
521 522 523
}

/// Runs driver tests.
524
typedef TestRunner = Future<void> Function(List<String> testArgs, Map<String, String> environment);
525
TestRunner testRunner = _runTests;
526
void restoreTestRunner() {
527
  testRunner = _runTests;
528 529
}

530
Future<void> _runTests(List<String> testArgs, Map<String, String> environment) async {
531
  globals.printTrace('Running driver tests.');
532

533
  globalPackagesPath = globals.fs.path.normalize(globals.fs.path.absolute(globalPackagesPath));
534
  final int result = await processUtils.stream(
535
    <String>[
536
      globals.artifacts.getArtifactPath(Artifact.engineDartBinary),
537
      ...testArgs,
538
      '--packages=$globalPackagesPath',
539 540
      '-rexpanded',
    ],
541
    environment: environment,
542
  );
543
  if (result != 0) {
544
    throwToolExit('Driver tests failed: $result', exitCode: result);
545
  }
546 547 548 549
}


/// Stops the application.
550
typedef AppStopper = Future<bool> Function(DriveCommand command);
551
AppStopper appStopper = _stopApp;
552
void restoreAppStopper() {
553
  appStopper = _stopApp;
554 555
}

556
Future<bool> _stopApp(DriveCommand command) async {
557
  globals.printTrace('Stopping application.');
558 559 560 561
  final ApplicationPackage package = await command.applicationPackages.getPackageForPlatform(
    await command.device.targetPlatform,
    command.getBuildInfo(),
  );
562
  final bool stopped = await command.device.stopApp(package, userIdentifier: command.userIdentifier);
563
  await command._deviceLogSubscription?.cancel();
564
  return stopped;
565
}
566

567
/// A list of supported browsers.
568 569
@visibleForTesting
enum Browser {
570 571
  /// Chrome on Android: https://developer.chrome.com/multidevice/android/overview
  androidChrome,
572 573 574 575 576 577 578 579 580 581 582 583 584 585 586
  /// 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) {
587
    case 'android-chrome': return Browser.androidChrome;
588 589 590 591 592 593 594 595 596
    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');
}

597
Future<async_io.WebDriver> _createDriver(String driverPort, Browser browser, bool headless, String chromeBinary) async {
598 599
  return async_io.createDriver(
      uri: Uri.parse('http://localhost:$driverPort/'),
600
      desired: getDesiredCapabilities(browser, headless, chromeBinary),
601
      spec: async_io.WebDriverSpec.Auto
602 603 604
  );
}

605 606
/// Returns desired capabilities for given [browser], [headless] and
/// [chromeBinary].
607
@visibleForTesting
608
Map<String, dynamic> getDesiredCapabilities(Browser browser, bool headless, [String chromeBinary]) {
609 610 611 612 613
  switch (browser) {
    case Browser.chrome:
      return <String, dynamic>{
        'acceptInsecureCerts': true,
        'browserName': 'chrome',
614
        'goog:loggingPrefs': <String, String>{ async_io.LogType.performance: 'ALL'},
615
        'chromeOptions': <String, dynamic>{
616 617
          if (chromeBinary != null)
            'binary': chromeBinary,
618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636
          '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'
          }
637
        },
638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 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
      };
      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
      };
679 680 681 682 683 684 685 686 687
    case Browser.androidChrome:
      return <String, dynamic>{
        'browserName': 'chrome',
        'platformName': 'android',
        'goog:chromeOptions': <String, dynamic>{
          'androidPackage': 'com.android.chrome',
          'args': <String>['--disable-fullscreen']
        },
      };
688 689 690 691
    default:
      throw UnsupportedError('Browser $browser not supported.');
  }
}