Commit 3301ae7d authored by Yegor's avatar Yegor

Merge pull request #2097 from yjbanov/flutter-driver-create

"flutter create" can generate a basic driver test; "flutter drive" gains new options
parents 347ee25a 278630e6
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:io' as dart_io;
import 'package:file/io.dart'; import 'package:file/io.dart';
import 'package:file/sync_io.dart'; import 'package:file/sync_io.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
...@@ -16,16 +17,42 @@ export 'package:file/sync_io.dart'; ...@@ -16,16 +17,42 @@ export 'package:file/sync_io.dart';
FileSystem fs = new LocalFileSystem(); FileSystem fs = new LocalFileSystem();
SyncFileSystem syncFs = new SyncLocalFileSystem(); SyncFileSystem syncFs = new SyncLocalFileSystem();
typedef String CurrentDirectoryGetter();
final CurrentDirectoryGetter _defaultCurrentDirectoryGetter = () {
return dart_io.Directory.current.path;
};
/// Points to the current working directory (like `pwd`).
CurrentDirectoryGetter getCurrentDirectory = _defaultCurrentDirectoryGetter;
/// Exits the process with the given [exitCode].
typedef void ExitFunction([int exitCode]);
final ExitFunction _defaultExitFunction = ([int exitCode]) {
dart_io.exit(exitCode);
};
/// Exits the process.
ExitFunction exit = _defaultExitFunction;
/// Restores [fs] and [syncFs] to the default local disk-based implementation. /// Restores [fs] and [syncFs] to the default local disk-based implementation.
void restoreFileSystem() { void restoreFileSystem() {
fs = new LocalFileSystem(); fs = new LocalFileSystem();
syncFs = new SyncLocalFileSystem(); syncFs = new SyncLocalFileSystem();
getCurrentDirectory = _defaultCurrentDirectoryGetter;
exit = _defaultExitFunction;
} }
void useInMemoryFileSystem() { /// Uses in-memory replacments for `dart:io` functionality. Useful in tests.
void useInMemoryFileSystem({ cwd: '/', ExitFunction exitFunction }) {
MemoryFileSystem memFs = new MemoryFileSystem(); MemoryFileSystem memFs = new MemoryFileSystem();
fs = memFs; fs = memFs;
syncFs = new SyncMemoryFileSystem(backedBy: memFs.storage); syncFs = new SyncMemoryFileSystem(backedBy: memFs.storage);
getCurrentDirectory = () => cwd;
exit = exitFunction ?? ([int exitCode]) {
throw new Exception('Exited with code $exitCode');
};
} }
/// Create the ancestor directories of a file path if they do not already exist. /// Create the ancestor directories of a file path if they do not already exist.
......
...@@ -30,6 +30,12 @@ class CreateCommand extends Command { ...@@ -30,6 +30,12 @@ class CreateCommand extends Command {
defaultsTo: true, defaultsTo: true,
help: 'Whether to run "pub get" after the project has been created.' help: 'Whether to run "pub get" after the project has been created.'
); );
argParser.addFlag(
'with-driver-test',
negatable: true,
defaultsTo: false,
help: 'Also add Flutter Driver dependencies and generate a sample driver test.'
);
} }
String get invocation => "${runner.executableName} $name <output directory>"; String get invocation => "${runner.executableName} $name <output directory>";
...@@ -50,12 +56,19 @@ class CreateCommand extends Command { ...@@ -50,12 +56,19 @@ class CreateCommand extends Command {
String flutterRoot = path.absolute(ArtifactStore.flutterRoot); String flutterRoot = path.absolute(ArtifactStore.flutterRoot);
String flutterPackagePath = path.join(flutterRoot, 'packages', 'flutter'); String flutterPackagesDirectory = path.join(flutterRoot, 'packages');
String flutterPackagePath = path.join(flutterPackagesDirectory, 'flutter');
if (!FileSystemEntity.isFileSync(path.join(flutterPackagePath, 'pubspec.yaml'))) { if (!FileSystemEntity.isFileSync(path.join(flutterPackagePath, 'pubspec.yaml'))) {
printError('Unable to find package:flutter in $flutterPackagePath'); printError('Unable to find package:flutter in $flutterPackagePath');
return 2; return 2;
} }
String flutterDriverPackagePath = path.join(flutterRoot, 'packages', 'flutter_driver');
if (!FileSystemEntity.isFileSync(path.join(flutterDriverPackagePath, 'pubspec.yaml'))) {
printError('Unable to find package:flutter_driver in $flutterDriverPackagePath');
return 2;
}
Directory out; Directory out;
if (argResults.wasParsed('out')) { if (argResults.wasParsed('out')) {
...@@ -64,7 +77,15 @@ class CreateCommand extends Command { ...@@ -64,7 +77,15 @@ class CreateCommand extends Command {
out = new Directory(argResults.rest.first); out = new Directory(argResults.rest.first);
} }
new FlutterSimpleTemplate().generateInto(out, flutterPackagePath); FlutterSimpleTemplate template = new FlutterSimpleTemplate();
if (argResults['with-driver-test'])
template.withDriverTest();
template.generateInto(
dir: out,
flutterPackagesDirectory: flutterPackagesDirectory
);
printStatus(''); printStatus('');
...@@ -90,19 +111,23 @@ All done! To run your application: ...@@ -90,19 +111,23 @@ All done! To run your application:
abstract class Template { abstract class Template {
final String name; final String name;
final String description; final String description;
final Map<String, String> files = <String, String>{};
Map<String, String> files = <String, String>{}; final Map<String, dynamic> additionalTemplateVariables = <String, dynamic>{};
Template(this.name, this.description); Template(this.name, this.description);
void generateInto(Directory dir, String flutterPackagePath) { void generateInto({
Directory dir,
String flutterPackagesDirectory
}) {
String dirPath = path.normalize(dir.absolute.path); String dirPath = path.normalize(dir.absolute.path);
String projectName = _normalizeProjectName(path.basename(dirPath)); String projectName = _normalizeProjectName(path.basename(dirPath));
String projectIdentifier = _createProjectIdentifier(path.basename(dirPath)); String projectIdentifier = _createProjectIdentifier(path.basename(dirPath));
printStatus('Creating ${path.basename(projectName)}...'); printStatus('Creating ${path.basename(projectName)}...');
dir.createSync(recursive: true); dir.createSync(recursive: true);
String relativeFlutterPackagePath = path.relative(flutterPackagePath, from: dirPath); String relativeFlutterPackagesDirectory =
path.relative(flutterPackagesDirectory, from: dirPath);
Iterable<String> paths = files.keys.toList()..sort(); Iterable<String> paths = files.keys.toList()..sort();
for (String filePath in paths) { for (String filePath in paths) {
...@@ -111,8 +136,9 @@ abstract class Template { ...@@ -111,8 +136,9 @@ abstract class Template {
'projectName': projectName, 'projectName': projectName,
'projectIdentifier': projectIdentifier, 'projectIdentifier': projectIdentifier,
'description': description, 'description': description,
'flutterPackagePath': relativeFlutterPackagePath 'flutterPackagesDirectory': relativeFlutterPackagesDirectory,
}; };
m.addAll(additionalTemplateVariables);
contents = mustache.render(contents, m); contents = mustache.render(contents, m);
filePath = filePath.replaceAll('/', Platform.pathSeparator); filePath = filePath.replaceAll('/', Platform.pathSeparator);
File file = new File(path.join(dir.path, filePath)); File file = new File(path.join(dir.path, filePath));
...@@ -142,6 +168,12 @@ class FlutterSimpleTemplate extends Template { ...@@ -142,6 +168,12 @@ class FlutterSimpleTemplate extends Template {
// iOS files. // iOS files.
files.addAll(iosTemplateFiles); files.addAll(iosTemplateFiles);
} }
void withDriverTest() {
additionalTemplateVariables['withDriverTest?'] = {};
files['test_driver/e2e.dart'] = _e2eApp;
files['test_driver/e2e_test.dart'] = _e2eTest;
}
} }
String _normalizeProjectName(String name) { String _normalizeProjectName(String name) {
...@@ -194,7 +226,12 @@ name: {{projectName}} ...@@ -194,7 +226,12 @@ name: {{projectName}}
description: {{description}} description: {{description}}
dependencies: dependencies:
flutter: flutter:
path: {{flutterPackagePath}} path: {{flutterPackagesDirectory}}/flutter
{{#withDriverTest?}}
dev_dependencies:
flutter_driver:
path: {{flutterPackagesDirectory}}/flutter_driver
{{/withDriverTest?}}
'''; ''';
const String _flutterYaml = r''' const String _flutterYaml = r'''
...@@ -240,10 +277,14 @@ class _FlutterDemoState extends State<FlutterDemo> { ...@@ -240,10 +277,14 @@ class _FlutterDemoState extends State<FlutterDemo> {
), ),
body: new Material( body: new Material(
child: new Center( child: new Center(
child: new Text('Button tapped $_counter times.') child: new Text(
'Button tapped $_counter times.',
key: const ValueKey('counter')
)
) )
), ),
floatingActionButton: new FloatingActionButton( floatingActionButton: new FloatingActionButton(
key: const ValueKey('fab'),
child: new Icon( child: new Icon(
icon: 'content/add' icon: 'content/add'
), ),
...@@ -254,6 +295,65 @@ class _FlutterDemoState extends State<FlutterDemo> { ...@@ -254,6 +295,65 @@ class _FlutterDemoState extends State<FlutterDemo> {
} }
'''; ''';
const String _e2eApp = '''
// Starts the app with Flutter Driver extension enabled to allow Flutter Driver
// to test the app.
import 'package:{{projectName}}/main.dart' as app;
import 'package:flutter_driver/driver_extension.dart';
main() {
enableFlutterDriverExtension();
app.main();
}
''';
const String _e2eTest = '''
// This is a basic Flutter Driver test for the application. A Flutter Driver
// test is an end-to-end test that "drives" your application from another
// process or even from another computer. If you are familiar with
// Selenium/WebDriver for web, Espresso for Android or UI Automation for iOS,
// this is simply Flutter's version of that.
//
// To start the test run the following command from the root of your application
// package:
//
// flutter drive --target=test_driver/e2e.dart
//
import 'package:flutter_driver/flutter_driver.dart';
import 'package:test/test.dart';
main() {
group('end-to-end test', () {
FlutterDriver driver;
setUpAll(() async {
// Connect to a running Flutter application instance.
driver = await FlutterDriver.connect();
});
tearDownAll(() async {
if (driver != null) driver.close();
});
test('find the floating action button by value key', () async {
ObjectRef elem = await driver.findByValueKey('fab');
expect(elem, isNotNull);
expect(elem.objectReferenceKey, isNotNull);
});
test('tap on the floating action button; verify counter', () async {
ObjectRef fab = await driver.findByValueKey('fab');
expect(fab, isNotNull);
await driver.tap(fab);
ObjectRef counter = await driver.findByValueKey('counter');
expect(counter, isNotNull);
String text = await driver.getText(counter);
expect(text, contains("Button tapped 1 times."));
});
});
}
''';
final String _apkManifest = ''' final String _apkManifest = '''
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="{{projectIdentifier}}" package="{{projectIdentifier}}"
......
...@@ -25,9 +25,10 @@ typedef Future<int> StopAppFunction(); ...@@ -25,9 +25,10 @@ typedef Future<int> StopAppFunction();
/// ///
/// This command takes a target Flutter application that you would like to test /// 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 /// as the `--target` option (defaults to `lib/main.dart`). It then looks for a
/// file with the same name but containing the `_test.dart` suffix. The /// corresponding test file within the `test_driver` directory. The test file is
/// `_test.dart` file is expected to be a program that uses /// expected to have the same name but contain the `_test.dart` suffix. The
/// `package:flutter_driver` that exercises your application. Most commonly it /// `_test.dart` file would generall 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 /// is a test written using `package:test`, but you are free to use something
/// else. /// else.
/// ///
...@@ -59,6 +60,25 @@ class DriveCommand extends RunCommand { ...@@ -59,6 +60,25 @@ class DriveCommand extends RunCommand {
_runApp = runAppFn ?? super.runInProject; _runApp = runAppFn ?? super.runInProject;
_runTests = runTestsFn ?? executable.main; _runTests = runTestsFn ?? executable.main;
_stopApp = stopAppFn ?? this.stop; _stopApp = stopAppFn ?? this.stop;
argParser.addFlag(
'keep-app-running',
negatable: true,
defaultsTo: false,
help:
'Will keep the Flutter application running when done testing. By '
'default Flutter Driver stops the application after tests are finished.'
);
argParser.addFlag(
'use-existing-app',
negatable: true,
defaultsTo: false,
help:
'Will not start a new Flutter application but connect to an '
'already running instance. This will also cause the driver to keep '
'the application running after tests are done.'
);
} }
DriveCommand() : this.custom(); DriveCommand() : this.custom();
...@@ -66,27 +86,40 @@ class DriveCommand extends RunCommand { ...@@ -66,27 +86,40 @@ class DriveCommand extends RunCommand {
@override @override
Future<int> runInProject() async { Future<int> runInProject() async {
String testFile = _getTestFile(); String testFile = _getTestFile();
if (testFile == null) {
return 1;
}
if (await fs.type(testFile) != FileSystemEntityType.FILE) { if (await fs.type(testFile) != FileSystemEntityType.FILE) {
printError('Test file not found: $testFile'); printError('Test file not found: $testFile');
return 1; return 1;
} }
int result = await _runApp(); if (!argResults['use-existing-app']) {
if (result != 0) { printStatus('Starting application: ${argResults["target"]}');
printError('Application failed to start. Will not run test. Quitting.'); int result = await _runApp();
return result; if (result != 0) {
printError('Application failed to start. Will not run test. Quitting.');
return result;
}
} else {
printStatus('Will connect to already running application instance');
} }
try { try {
return await _runTests([testFile]) return await _runTests([testFile])
.then((_) => 0) .then((_) => 0)
.catchError((error, stackTrace) { .catchError((error, stackTrace) {
printError('ERROR: $error\n$stackTrace'); printError('CAUGHT EXCEPTION: $error\n$stackTrace');
return 1; return 1;
}); });
} finally { } finally {
await _stopApp(); if (!argResults['keep-app-running'] && !argResults['use-existing-app']) {
printStatus('Stopping application instance');
await _stopApp();
} else {
printStatus('Leaving the application running');
}
} }
} }
...@@ -95,9 +128,39 @@ class DriveCommand extends RunCommand { ...@@ -95,9 +128,39 @@ class DriveCommand extends RunCommand {
} }
String _getTestFile() { String _getTestFile() {
String appFile = argResults['target']; String appFile = path.normalize(argResults['target']);
String extension = path.extension(appFile);
String name = path.withoutExtension(appFile); // This command extends `flutter start` and therefore CWD == package dir
return '${name}_test$extension'; String packageDir = getCurrentDirectory();
// Make appFile path relative to package directory because we are looking
// for the corresponding test file relative to it.
if (!path.isRelative(appFile)) {
if (!path.isWithin(packageDir, appFile)) {
printError(
'Application file $appFile is outside the package directory $packageDir'
);
return null;
}
appFile = path.relative(appFile, from: packageDir);
}
List<String> parts = path.split(appFile);
if (parts.length < 2) {
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`.
String pathWithNoExtension = path.withoutExtension(path.joinAll(
[packageDir, 'test_driver']..addAll(parts.skip(1))));
return '${pathWithNoExtension}_test${path.extension(appFile)}';
} }
} }
...@@ -20,7 +20,7 @@ main() => defineTests(); ...@@ -20,7 +20,7 @@ main() => defineTests();
defineTests() { defineTests() {
group('drive', () { group('drive', () {
setUp(() { setUp(() {
useInMemoryFileSystem(); useInMemoryFileSystem(cwd: '/some/app');
}); });
tearDown(() { tearDown(() {
...@@ -39,7 +39,7 @@ defineTests() { ...@@ -39,7 +39,7 @@ defineTests() {
expect(code, equals(1)); expect(code, equals(1));
BufferLogger buffer = logger; BufferLogger buffer = logger;
expect(buffer.errorText, expect(buffer.errorText,
contains('Test file not found: /some/app/test/e2e_test.dart')); contains('Test file not found: /some/app/test_driver/e2e_test.dart'));
}); });
}); });
...@@ -49,8 +49,8 @@ defineTests() { ...@@ -49,8 +49,8 @@ defineTests() {
})); }));
applyMocksToCommand(command); applyMocksToCommand(command);
String testApp = '/some/app/test/e2e.dart'; String testApp = '/some/app/test_driver/e2e.dart';
String testFile = '/some/app/test/e2e_test.dart'; String testFile = '/some/app/test_driver/e2e_test.dart';
MemoryFileSystem memFs = fs; MemoryFileSystem memFs = fs;
await memFs.file(testApp).writeAsString('main() {}'); await memFs.file(testApp).writeAsString('main() {}');
...@@ -69,9 +69,50 @@ defineTests() { ...@@ -69,9 +69,50 @@ defineTests() {
}); });
}); });
testUsingContext('returns 1 when app file is outside package', () async {
String packageDir = '/my/app';
useInMemoryFileSystem(cwd: packageDir);
DriveCommand command = new DriveCommand();
applyMocksToCommand(command);
String appFile = '/not/in/my/app.dart';
List<String> args = [
'drive',
'--target=$appFile',
];
return createTestCommandRunner(command).run(args).then((int code) {
expect(code, equals(1));
BufferLogger buffer = logger;
expect(buffer.errorText, contains(
'Application file $appFile is outside the package directory $packageDir'
));
});
});
testUsingContext('returns 1 when app file is in the root dir', () async {
String packageDir = '/my/app';
useInMemoryFileSystem(cwd: packageDir);
DriveCommand command = new DriveCommand();
applyMocksToCommand(command);
String appFile = '/my/app/main.dart';
List<String> args = [
'drive',
'--target=$appFile',
];
return createTestCommandRunner(command).run(args).then((int code) {
expect(code, equals(1));
BufferLogger buffer = logger;
expect(buffer.errorText, contains(
'Application file main.dart must reside in one of the '
'sub-directories of the package structure, not in the root directory.'
));
});
});
testUsingContext('returns 0 when test ends successfully', () async { testUsingContext('returns 0 when test ends successfully', () async {
String testApp = '/some/app/test/e2e.dart'; String testApp = '/some/app/test/e2e.dart';
String testFile = '/some/app/test/e2e_test.dart'; String testFile = '/some/app/test_driver/e2e_test.dart';
DriveCommand command = new DriveCommand.custom( DriveCommand command = new DriveCommand.custom(
runAppFn: expectAsync(() { runAppFn: expectAsync(() {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment