blob: 6ea5281c56f74809c2c7e9375ea301d0dd9fff68 [file] [log] [blame]
// Copyright 2014 The Flutter 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';
import 'dart:math' as math;
import 'package:meta/meta.dart';
import 'package:webdriver/async_io.dart' as async_io;
import '../application_package.dart';
import '../base/common.dart';
import '../base/file_system.dart';
import '../base/process.dart';
import '../build_info.dart';
import '../cache.dart';
import '../convert.dart';
import '../dart/package_map.dart';
import '../dart/sdk.dart';
import '../device.dart';
import '../globals.dart' as globals;
import '../project.dart';
import '../resident_runner.dart';
import '../runner/flutter_command.dart' show FlutterCommandResult;
import '../web/web_runner.dart';
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
/// 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
/// `_test.dart` file would generally be a Dart program that uses
/// `package:flutter_driver` and exercises your application. Most commonly it
/// 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.
class DriveCommand extends RunCommandBase {
DriveCommand() {
defaultsTo: null,
help: 'Will keep the Flutter application running when done testing.\n'
'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 '
'running, and --no-keep-app-running overrides it.',
help: 'Connect to an already running instance via the given observatory URL. '
'If this option is given, the application will not be automatically started, '
'and it will only be stopped if --no-keep-app-running is explicitly set.',
valueHelp: 'url',
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 '
valueHelp: 'path',
defaultsTo: true,
help: 'Build the app before running.',
defaultsTo: '4444',
help: 'The port where Webdriver server is launched at. Defaults to 4444.',
valueHelp: '4444'
defaultsTo: true,
help: 'Whether the driver browser is going to be launched in headless mode. Defaults to true.',
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>[
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).',
final String name = 'drive';
final String description = 'Runs Flutter Driver tests for the current project.';
final List<String> aliases = <String>['driver'];
Device _device;
Device get device => _device;
bool get shouldBuild => boolArg('build');
bool get verboseSystemLogs => boolArg('verbose-system-logs');
/// Subscription to log messages printed on the device or simulator.
// ignore: cancel_subscriptions
StreamSubscription<String> _deviceLogSubscription;
Future<FlutterCommandResult> runCommand() async {
final String testFile = _getTestFile();
if (testFile == null) {
_device = await findTargetDevice();
if (device == null) {
if (await globals.fs.type(testFile) != FileSystemEntityType.file) {
throwToolExit('Test file not found: $testFile');
String observatoryUri;
ResidentRunner residentRunner;
final bool isWebPlatform = await device.targetPlatform == TargetPlatform.web_javascript;
if (argResults['use-existing-app'] == null) {
globals.printStatus('Starting application: $targetFile');
if (getBuildInfo().isRelease && !isWebPlatform) {
// This is because we need VM service to be able to drive the app.
// For Flutter Web, testing in release mode is allowed.
'Flutter Driver (non-web) does not support running in release mode.\n'
'Use --profile mode for testing application performance.\n'
'Use --debug (default) mode for testing correctness (with assertions).'
if (isWebPlatform && getBuildInfo().isDebug) {
// TODO(angjieli): remove this once running against
// target under test_driver in debug mode is supported
'Flutter Driver web does not support running in debug mode.\n'
'Use --profile mode for testing application performance.\n'
'Use --release mode for testing correctness (with assertions).'
Uri webUri;
if (isWebPlatform) {
// Start Flutter web application for current test
final FlutterProject flutterProject = FlutterProject.current();
final FlutterDevice flutterDevice = await FlutterDevice.create(
flutterProject: flutterProject,
trackWidgetCreation: boolArg('track-widget-creation'),
target: targetFile,
buildMode: getBuildMode()
residentRunner = webRunnerFactory.createWebRunner(
target: targetFile,
flutterProject: flutterProject,
ipv6: ipv6,
debuggingOptions: DebuggingOptions.enabled(getBuildInfo()),
stayResident: false,
dartDefines: dartDefines,
urlTunneller: null,
final Completer<void> appStartedCompleter = Completer<void>.sync();
final int result = await
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);
if (result == null) {
throwToolExit('Application failed to start. Will not run test. Quitting.', exitCode: 1);
observatoryUri = result.observatoryUri.toString();
} else {
globals.printStatus('Will connect to already running application instance.');
observatoryUri = stringArg('use-existing-app');
final Map<String, String> environment = <String, String>{
'VM_SERVICE_URL': observatoryUri,
async_io.WebDriver driver;
// For web device, WebDriver session will be launched beforehand
// so that FlutterDriver can reuse it.
if (isWebPlatform) {
final Browser browser = _browserNameToEnum(
final String driverPort = argResults['driver-port'].toString();
// start WebDriver
try {
driver = await _createDriver(
argResults['headless'].toString() == 'true',
} on Exception catch (ex) {
'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'
// set window size
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) {
Dimension provided to --browser-dimension is invalid:
final async_io.Window window = await driver.window;
try {
await window.setLocation(const math.Point<int>(0, 0));
await window.setSize(math.Rectangle<int>(0, 0, x, y));
} catch (_) {
// Error might be thrown in some browsers.
// add driver info to environment variables
environment.addAll(<String, String> {
'DRIVER_SESSION_URI': driver.uri.toString(),
'DRIVER_SESSION_SPEC': driver.spec.toString(),
'DRIVER_SESSION_CAPABILITIES': jsonEncode(driver.capabilities),
try {
await testRunner(<String>[testFile], environment);
} catch (error, stackTrace) {
if (error is ToolExit) {
throw Exception('Unable to run test: $error\n$stackTrace');
} finally {
await residentRunner?.exit();
await driver?.quit();
if (boolArg('keep-app-running') ?? (argResults['use-existing-app'] != null)) {
globals.printStatus('Leaving the application running.');
} else {
globals.printStatus('Stopping application instance.');
await appStopper(this);
return FlutterCommandResult.success();
String _getTestFile() {
if (argResults['driver'] != null) {
return stringArg('driver');
// If the --driver argument wasn't provided, then derive the value from
// the target file.
String appFile = globals.fs.path.normalize(targetFile);
// This command extends `flutter run` and therefore CWD == package dir
final String packageDir = globals.fs.currentDirectory.path;
// Make appFile path relative to package directory because we are looking
// for the corresponding test file relative to it.
if (!globals.fs.path.isRelative(appFile)) {
if (!globals.fs.path.isWithin(packageDir, appFile)) {
'Application file $appFile is outside the package directory $packageDir'
return null;
appFile = globals.fs.path.relative(appFile, from: packageDir);
final List<String> parts = globals.fs.path.split(appFile);
if (parts.length < 2) {
'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`.
final String pathWithNoExtension = globals.fs.path.withoutExtension(globals.fs.path.joinAll(
<String>[packageDir, 'test_driver',]));
return '${pathWithNoExtension}_test${globals.fs.path.extension(appFile)}';
Future<Device> findTargetDevice() async {
final List<Device> devices = await deviceManager.findTargetDevices(FlutterProject.current());
if (deviceManager.hasSpecifiedDeviceId) {
if (devices.isEmpty) {
globals.printStatus("No devices found with name or id matching '${deviceManager.specifiedDeviceId}'");
return null;
if (devices.length > 1) {
globals.printStatus("Found ${devices.length} devices with name or id matching '${deviceManager.specifiedDeviceId}':");
await Device.printDevices(devices);
return null;
return devices.first;
if (devices.isEmpty) {
globals.printError('No devices found.');
return null;
} else if (devices.length > 1) {
globals.printStatus('Found multiple connected devices:');
await Device.printDevices(devices);
globals.printStatus('Using device ${}.');
return devices.first;
/// Starts the application on the device given command configuration.
typedef AppStarter = Future<LaunchResult> Function(DriveCommand command, Uri webUri);
AppStarter appStarter = _startApp; // (mutable for testing)
void restoreAppStarter() {
appStarter = _startApp;
Future<LaunchResult> _startApp(DriveCommand command, Uri webUri) async {
final String mainPath = findMainDartFile(command.targetFile);
if (await globals.fs.type(mainPath) != FileSystemEntityType.file) {
globals.printError('Tried to run $mainPath, but that file does not exist.');
return null;
globals.printTrace('Stopping previously running application, if any.');
await appStopper(command);
final ApplicationPackage package = await command.applicationPackages
.getPackageForPlatform(await command.device.targetPlatform);
if (command.shouldBuild) {
globals.printTrace('Installing application package.');
if (await command.device.isAppInstalled(package)) {
await command.device.uninstallApp(package);
await command.device.installApp(package);
final Map<String, dynamic> platformArgs = <String, dynamic>{};
if (command.traceStartup) {
platformArgs['trace-startup'] = command.traceStartup;
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;
globals.printTrace('Starting application.');
// Forward device log messages to the terminal window running the "drive" command.
command._deviceLogSubscription = command
.getLogReader(app: package)
final LaunchResult result = await command.device.startApp(
mainPath: mainPath,
route: command.route,
debuggingOptions: DebuggingOptions.enabled(
startPaused: true,
hostVmServicePort: command.hostVmservicePort,
verboseSystemLogs: command.verboseSystemLogs,
cacheSkSL: command.cacheSkSL,
dumpSkpOnShaderCompilation: command.dumpSkpOnShaderCompilation,
platformArgs: platformArgs,
prebuiltApplication: !command.shouldBuild,
if (!result.started) {
await command._deviceLogSubscription.cancel();
return null;
return result;
/// Runs driver tests.
typedef TestRunner = Future<void> Function(List<String> testArgs, Map<String, String> environment);
TestRunner testRunner = _runTests;
void restoreTestRunner() {
testRunner = _runTests;
Future<void> _runTests(List<String> testArgs, Map<String, String> environment) async {
globals.printTrace('Running driver tests.');
PackageMap.globalPackagesPath = globals.fs.path.normalize(globals.fs.path.absolute(PackageMap.globalPackagesPath));
final String dartVmPath = globals.fs.path.join(dartSdkPath, 'bin', 'dart');
final int result = await
environment: environment,
if (result != 0) {
throwToolExit('Driver tests failed: $result', exitCode: result);
/// Stops the application.
typedef AppStopper = Future<bool> Function(DriveCommand command);
AppStopper appStopper = _stopApp;
void restoreAppStopper() {
appStopper = _stopApp;
Future<bool> _stopApp(DriveCommand command) async {
globals.printTrace('Stopping application.');
final ApplicationPackage package = await command.applicationPackages.getPackageForPlatform(await command.device.targetPlatform);
final bool stopped = await command.device.stopApp(package);
await command._deviceLogSubscription?.cancel();
return stopped;
/// A list of supported browsers
enum Browser {
/// Chrome:
/// Edge:
/// Firefox:
/// Safari in iOS:
/// Safari in macOS:
/// Converts [browserName] string to [Browser]
Browser _browserNameToEnum(String browserName){
switch (browserName) {
case 'chrome': return;
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');
Future<async_io.WebDriver> _createDriver(String driverPort, Browser browser, bool headless) async {
return async_io.createDriver(
uri: Uri.parse('http://localhost:$driverPort/'),
desired: getDesiredCapabilities(browser, headless),
spec: async_io.WebDriverSpec.Auto
/// Returns desired capabilities for given [browser] and [headless].
Map<String, dynamic> getDesiredCapabilities(Browser browser, bool headless) {
switch (browser) {
return <String, dynamic>{
'acceptInsecureCerts': true,
'browserName': 'chrome',
'goog:loggingPrefs': <String, String>{ async_io.LogType.performance: 'ALL'},
'chromeOptions': <String, dynamic>{
'w3c': false,
'args': <String>[
if (headless) '--headless'
'perfLoggingPrefs': <String, String>{
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'}
case Browser.edge:
return <String, dynamic>{
'acceptInsecureCerts': true,
'browserName': 'edge',
case Browser.safari:
return <String, dynamic>{
'browserName': 'safari',
case Browser.iosSafari:
return <String, dynamic>{
'platformName': 'ios',
'browserName': 'safari',
'safari:useSimulator': true
throw UnsupportedError('Browser $browser not supported.');