1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
// 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 'package:meta/meta.dart';
import 'package:package_config/package_config_types.dart';
import '../android/android_device.dart';
import '../application_package.dart';
import '../artifacts.dart';
import '../base/common.dart';
import '../base/file_system.dart';
import '../base/logger.dart';
import '../base/platform.dart';
import '../build_info.dart';
import '../dart/package_map.dart';
import '../device.dart';
import '../drive/drive_service.dart';
import '../globals.dart' as globals;
import '../resident_runner.dart';
import '../runner/flutter_command.dart' show FlutterCommandCategory, FlutterCommandResult, FlutterOptions;
import '../web/web_device.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({
bool verboseHelp = false,
@visibleForTesting FlutterDriverFactory? flutterDriverFactory,
required FileSystem fileSystem,
required Logger? logger,
required Platform platform,
}) : _flutterDriverFactory = flutterDriverFactory,
_fileSystem = fileSystem,
_logger = logger,
_fsUtils = FileSystemUtils(fileSystem: fileSystem, platform: platform),
super(verboseHelp: verboseHelp) {
requiresPubspecYaml();
addEnableExperimentation(hide: !verboseHelp);
// 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);
addMultidexOption();
argParser
..addFlag('keep-app-running',
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.',
)
..addOption('use-existing-app',
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',
)
..addOption('driver',
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".',
valueHelp: 'path',
)
..addFlag('build',
defaultsTo: true,
help: '(deprecated) Build the app before running. To use an existing app, pass the "--${FlutterOptions.kUseApplicationBinary}" '
'flag with an existing APK.',
)
..addOption('screenshot',
valueHelp: 'path/to/directory',
help: 'Directory location to write screenshots on test failure.',
)
..addOption('driver-port',
defaultsTo: '4444',
help: 'The port where Webdriver server is launched at.',
valueHelp: '4444'
)
..addFlag('headless',
defaultsTo: true,
help: 'Whether the driver browser is going to be launched in headless mode.',
)
..addOption('browser-name',
defaultsTo: 'chrome',
help: 'Name of the browser where tests will be executed.',
allowed: <String>[
'android-chrome',
'chrome',
'edge',
'firefox',
'ios-safari',
'safari',
],
allowedHelp: <String, String>{
'android-chrome': 'Chrome on Android (see also "--android-emulator").',
'chrome': 'Google Chrome on this computer (see also "--chrome-binary").',
'edge': 'Microsoft Edge on this computer (Windows only).',
'firefox': 'Mozilla Firefox on this computer.',
'ios-safari': 'Apple Safari on an iOS device.',
'safari': 'Apple Safari on this computer (macOS only).',
},
)
..addOption('browser-dimension',
defaultsTo: '1600,1024',
help: 'The dimension of the browser when running a Flutter Web test. '
'This will affect screenshot and all offset-related actions.',
valueHelp: 'width,height',
)
..addFlag('android-emulator',
defaultsTo: true,
help: 'Whether to perform Flutter Driver testing using an Android Emulator. '
'Works only if "browser-name" is set to "android-chrome".')
..addOption('chrome-binary',
help: 'Location of the Chrome binary. '
'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.')
..addMultiOption('test-arguments', help: 'Additional arguments to pass to the '
'Dart VM running The test script.')
..addOption('profile-memory', help: 'Launch devtools and profile application memory, writing '
'The output data to the file path provided to this argument as JSON.',
valueHelp: 'profile_memory.json');
}
// `pub` must always be run due to the test script running from source,
// even if an application binary is used. Default to true unless the user explicitly
// specified not to.
@override
bool get shouldRunPub {
if (argResults!.wasParsed('pub') && !boolArgDeprecated('pub')) {
return false;
}
return true;
}
FlutterDriverFactory? _flutterDriverFactory;
final FileSystem _fileSystem;
final Logger? _logger;
final FileSystemUtils _fsUtils;
@override
final String name = 'drive';
@override
final String description = 'Run integration tests for the project on an attached device or emulator.';
@override
String get category => FlutterCommandCategory.project;
@override
final List<String> aliases = <String>['driver'];
String? get userIdentifier => stringArgDeprecated(FlutterOptions.kDeviceUser);
String? get screenshot => stringArgDeprecated('screenshot');
@override
bool get startPausedDefault => true;
@override
bool get cachePubGet => false;
@override
Future<void> validateCommand() async {
if (userIdentifier != null) {
final Device? device = await findTargetDevice();
if (device is! AndroidDevice) {
throwToolExit('--${FlutterOptions.kDeviceUser} is only supported for Android');
}
}
return super.validateCommand();
}
@override
Future<FlutterCommandResult> runCommand() async {
final String testFile = _getTestFile()!;
if (testFile == null) {
throwToolExit(null);
}
if (await _fileSystem.type(testFile) != FileSystemEntityType.file) {
throwToolExit('Test file not found: $testFile');
}
final String? applicationBinaryPath = stringArgDeprecated(FlutterOptions.kUseApplicationBinary);
final Device? device = await findTargetDevice(includeUnsupportedDevices: applicationBinaryPath == null);
if (device == null) {
throwToolExit(null);
}
if (screenshot != null && !device.supportsScreenshot) {
_logger!.printError('Screenshot not supported for ${device.name}.');
}
final bool web = device is WebServerDevice || device is ChromiumDevice;
_flutterDriverFactory ??= FlutterDriverFactory(
applicationPackageFactory: ApplicationPackageFactory.instance!,
logger: _logger!,
processUtils: globals.processUtils,
dartSdkPath: globals.artifacts!.getHostArtifact(HostArtifact.engineDartBinary).path,
devtoolsLauncher: DevtoolsLauncher.instance!,
);
final PackageConfig packageConfig = await loadPackageConfigWithLogging(
_fileSystem.file('.packages'),
logger: _logger!,
throwOnError: false,
);
final DriverService driverService = _flutterDriverFactory!.createDriverService(web);
final BuildInfo buildInfo = await getBuildInfo();
final DebuggingOptions debuggingOptions = await createDebuggingOptions(web);
final File? applicationBinary = applicationBinaryPath == null
? null
: _fileSystem.file(applicationBinaryPath);
bool screenshotTaken = false;
try {
if (stringArgDeprecated('use-existing-app') == null) {
await driverService.start(
buildInfo,
device,
debuggingOptions,
ipv6 ?? false,
applicationBinary: applicationBinary,
route: route,
userIdentifier: userIdentifier,
mainPath: targetFile,
platformArgs: <String, Object>{
if (traceStartup)
'trace-startup': traceStartup,
if (web)
'--no-launch-chrome': true,
if (boolArgDeprecated('multidex'))
'multidex': true,
}
);
} else {
final Uri? uri = Uri.tryParse(stringArgDeprecated('use-existing-app')!);
if (uri == null) {
throwToolExit('Invalid VM Service URI: ${stringArgDeprecated('use-existing-app')}');
}
await driverService.reuseApplication(
uri,
device,
debuggingOptions,
ipv6 ?? false,
);
}
final int testResult = await driverService.startTest(
testFile,
stringsArg('test-arguments'),
<String, String>{},
packageConfig,
chromeBinary: stringArgDeprecated('chrome-binary'),
headless: boolArgDeprecated('headless'),
webBrowserFlags: stringsArg(FlutterOptions.kWebBrowserFlag),
browserDimension: stringArgDeprecated('browser-dimension')!.split(','),
browserName: stringArgDeprecated('browser-name'),
driverPort: stringArgDeprecated('driver-port') != null
? int.tryParse(stringArgDeprecated('driver-port')!)
: null,
androidEmulator: boolArgDeprecated('android-emulator'),
profileMemory: stringArgDeprecated('profile-memory'),
);
if (testResult != 0 && screenshot != null) {
// Take a screenshot while the app is still running.
await _takeScreenshot(device);
screenshotTaken = true;
}
if (boolArgDeprecated('keep-app-running')) {
_logger!.printStatus('Leaving the application running.');
} else {
final File? skslFile = stringArgDeprecated('write-sksl-on-exit') != null
? _fileSystem.file(stringArgDeprecated('write-sksl-on-exit'))
: null;
await driverService.stop(userIdentifier: userIdentifier, writeSkslOnExit: skslFile);
}
if (testResult != 0) {
throwToolExit(null);
}
} on Exception catch(_) {
// On exceptions, including ToolExit, take a screenshot on the device
// unless a screenshot was already taken on test failure.
if (!screenshotTaken && screenshot != null) {
await _takeScreenshot(device);
}
rethrow;
}
return FlutterCommandResult.success();
}
String? _getTestFile() {
if (argResults!['driver'] != null) {
return stringArgDeprecated('driver');
}
// If the --driver argument wasn't provided, then derive the value from
// the target file.
String appFile = _fileSystem.path.normalize(targetFile);
// This command extends `flutter run` and therefore CWD == package dir
final String packageDir = _fileSystem.currentDirectory.path;
// Make appFile path relative to package directory because we are looking
// for the corresponding test file relative to it.
if (!_fileSystem.path.isRelative(appFile)) {
if (!_fileSystem.path.isWithin(packageDir, appFile)) {
_logger!.printError(
'Application file $appFile is outside the package directory $packageDir'
);
return null;
}
appFile = _fileSystem.path.relative(appFile, from: packageDir);
}
final List<String> parts = _fileSystem.path.split(appFile);
if (parts.length < 2) {
_logger!.printError(
'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 = _fileSystem.path.withoutExtension(_fileSystem.path.joinAll(
<String>[packageDir, 'test_driver', ...parts.skip(1)]));
return '${pathWithNoExtension}_test${_fileSystem.path.extension(appFile)}';
}
Future<void> _takeScreenshot(Device device) async {
if (!device.supportsScreenshot) {
return;
}
try {
final Directory outputDirectory = _fileSystem.directory(screenshot)
..createSync(recursive: true);
final File outputFile = _fsUtils.getUniqueFile(
outputDirectory,
'drive',
'png',
);
await device.takeScreenshot(outputFile);
_logger!.printStatus('Screenshot written to ${outputFile.path}');
} on Exception catch (error) {
_logger!.printError('Error taking screenshot: $error');
}
}
}