add flutter_driver package
This commit contains: - FlutterDriver API for e2e tests usable in conjunction with package:test - FlutterDriverExtension to be enabled by the application in order to allow an external agent to connect to it and drive user interactions and probe into the element tree - initial implementations of tap, findByValueKey and getText commands (to be expanded in future PRs)
This commit is contained in:
parent
d93a87ee12
commit
b0e4559459
27
packages/flutter_driver/lib/driver_extension.dart
Normal file
27
packages/flutter_driver/lib/driver_extension.dart
Normal file
@ -0,0 +1,27 @@
|
||||
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
/// This library provides a Dart VM service extension that is required for
|
||||
/// tests that use `package:flutter_driver` to drive applications from a
|
||||
/// separate process, similar to Selenium (web), Espresso (Android) and UI
|
||||
/// Automation (iOS).
|
||||
///
|
||||
/// The extension must be installed in the same process (isolate) with your
|
||||
/// application.
|
||||
///
|
||||
/// To enable the extension call [enableFlutterDriverExtension] early in your
|
||||
/// program, prior to running your application, e.g. before you call `runApp`.
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// import 'package:flutter/material.dart';
|
||||
/// import 'package:flutter_driver/driver_extension.dart';
|
||||
///
|
||||
/// main() {
|
||||
/// enableFlutterDriverExtension();
|
||||
/// runApp(new ExampleApp());
|
||||
/// }
|
||||
library flutter_driver_extension;
|
||||
|
||||
export 'src/extension.dart' show enableFlutterDriverExtension;
|
38
packages/flutter_driver/lib/flutter_driver.dart
Normal file
38
packages/flutter_driver/lib/flutter_driver.dart
Normal file
@ -0,0 +1,38 @@
|
||||
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
/// This library provides API to test Flutter applications that run on real
|
||||
/// devices and emulators.
|
||||
///
|
||||
/// The application run in a separate process from the test itself. If you are
|
||||
/// familiar with Selenium (web), Espresso (Android) or UI Automation (iOS),
|
||||
/// this is Flutter's version of that.
|
||||
///
|
||||
/// This is Flutter's version of Selenium WebDriver (generic web),
|
||||
/// Protractor (Angular), Espresso (Android) or Earl Gray (iOS).
|
||||
library flutter_driver;
|
||||
|
||||
export 'src/driver.dart' show
|
||||
FlutterDriver;
|
||||
|
||||
export 'src/error.dart' show
|
||||
DriverError,
|
||||
LogLevel,
|
||||
LogRecord,
|
||||
flutterDriverLog;
|
||||
|
||||
export 'src/find.dart' show
|
||||
ObjectRef,
|
||||
GetTextResult;
|
||||
|
||||
export 'src/health.dart' show
|
||||
Health,
|
||||
HealthStatus;
|
||||
|
||||
export 'src/message.dart' show
|
||||
Message,
|
||||
Command,
|
||||
ObjectRef,
|
||||
CommandWithTarget,
|
||||
Result;
|
176
packages/flutter_driver/lib/src/driver.dart
Normal file
176
packages/flutter_driver/lib/src/driver.dart
Normal file
@ -0,0 +1,176 @@
|
||||
// Copyright 2016 The Chromium 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:vm_service_client/vm_service_client.dart';
|
||||
import 'package:json_rpc_2/json_rpc_2.dart' as rpc;
|
||||
|
||||
import 'error.dart';
|
||||
import 'find.dart';
|
||||
import 'gesture.dart';
|
||||
import 'health.dart';
|
||||
import 'message.dart';
|
||||
|
||||
/// A function that connects to a Dart VM service given the [url].
|
||||
typedef Future<VMServiceClient> VMServiceConnectFunction(String url);
|
||||
|
||||
/// Connects to a real Dart VM service using the [VMServiceClient].
|
||||
final VMServiceConnectFunction vmServiceClientConnectFunction =
|
||||
VMServiceClient.connect;
|
||||
|
||||
/// The connection function used by [FlutterDriver.connect].
|
||||
///
|
||||
/// Overwrite this function if you require a different method for connecting to
|
||||
/// the VM service.
|
||||
VMServiceConnectFunction vmServiceConnectFunction =
|
||||
vmServiceClientConnectFunction;
|
||||
|
||||
/// Drives a Flutter Application running in another process.
|
||||
class FlutterDriver {
|
||||
|
||||
static const String _flutterExtensionMethod = 'ext.flutter_driver';
|
||||
static final Logger _log = new Logger('FlutterDriver');
|
||||
|
||||
/// Connects to a Flutter application.
|
||||
///
|
||||
/// Resumes the application if it is currently paused (e.g. at a breakpoint).
|
||||
///
|
||||
/// [dartVmServiceUrl] is the URL to Dart observatory (a.k.a. VM service). By
|
||||
/// default it connects to `http://localhost:8181`.
|
||||
static Future<FlutterDriver> connect({String dartVmServiceUrl: 'http://localhost:8181'}) async {
|
||||
// Connect to Dart VM servcies
|
||||
_log.info('Connecting to Flutter application at $dartVmServiceUrl');
|
||||
VMServiceClient client = await vmServiceConnectFunction(dartVmServiceUrl);
|
||||
VM vm = await client.getVM();
|
||||
_log.trace('Looking for the isolate');
|
||||
VMIsolate isolate = await vm.isolates.first.load();
|
||||
FlutterDriver driver = new FlutterDriver.connectedTo(client, isolate);
|
||||
|
||||
// Attempts to resume the isolate, but does not crash if it fails because
|
||||
// the isolate is already resumed. There could be a race with other tools,
|
||||
// such as a debugger, any of which could have resumed the isolate.
|
||||
Future resumeLeniently() {
|
||||
_log.trace('Attempting to resume isolate');
|
||||
return isolate.resume().catchError((e) {
|
||||
const vmMustBePausedCode = 101;
|
||||
if (e is rpc.RpcException && e.code == vmMustBePausedCode) {
|
||||
// No biggie; something else must have resumed the isolate
|
||||
_log.warning(
|
||||
'Attempted to resume an already resumed isolate. This may happen '
|
||||
'when we lose a race with another tool (usually a debugger) that '
|
||||
'is connected to the same isolate.'
|
||||
);
|
||||
} else {
|
||||
// Failed to resume due to another reason. Fail hard.
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Attempt to resume isolate if it was paused
|
||||
if (isolate.pauseEvent is VMPauseStartEvent) {
|
||||
_log.trace('Isolate is paused at start.');
|
||||
|
||||
// Waits for a signal from the VM service that the extension is registered
|
||||
Future waitForServiceExtension() {
|
||||
return isolate.onServiceExtensionAdded.firstWhere((VMServiceExtension ext) {
|
||||
return ext.method == _flutterExtensionMethod;
|
||||
});
|
||||
}
|
||||
|
||||
// If the isolate is paused at the start, e.g. via the --start-paused
|
||||
// option, then the VM service extension is not registered yet. Wait for
|
||||
// it to be registered.
|
||||
Future whenResumed = resumeLeniently();
|
||||
Future whenServiceExtensionReady = Future.any(<Future>[
|
||||
waitForServiceExtension(),
|
||||
// We will never receive the extension event if the user does not
|
||||
// register it. If that happens time out.
|
||||
new Future<String>.delayed(const Duration(seconds: 10), () => 'timeout')
|
||||
]);
|
||||
await whenResumed;
|
||||
_log.trace('Waiting for service extension');
|
||||
dynamic signal = await whenServiceExtensionReady;
|
||||
if (signal == 'timeout') {
|
||||
throw new DriverError(
|
||||
'Timed out waiting for Flutter Driver extension to become available. '
|
||||
'To enable the driver extension call registerFlutterDriverExtension '
|
||||
'first thing in the main method of your application.'
|
||||
);
|
||||
}
|
||||
} else if (isolate.pauseEvent is VMPauseExitEvent ||
|
||||
isolate.pauseEvent is VMPauseBreakpointEvent ||
|
||||
isolate.pauseEvent is VMPauseExceptionEvent ||
|
||||
isolate.pauseEvent is VMPauseInterruptedEvent) {
|
||||
// If the isolate is paused for any other reason, assume the extension is
|
||||
// already there.
|
||||
_log.trace('Isolate is paused mid-flight.');
|
||||
await resumeLeniently();
|
||||
} else if (isolate.pauseEvent is VMResumeEvent) {
|
||||
_log.trace('Isolate is not paused. Assuming application is ready.');
|
||||
} else {
|
||||
_log.warning(
|
||||
'Unknown pause event type ${isolate.pauseEvent.runtimeType}. '
|
||||
'Assuming application is ready.'
|
||||
);
|
||||
}
|
||||
|
||||
// At this point the service extension must be installed. Verify it.
|
||||
Health health = await driver.checkHealth();
|
||||
if (health.status != HealthStatus.ok) {
|
||||
client.close();
|
||||
throw new DriverError('Flutter application health check failed.');
|
||||
}
|
||||
|
||||
_log.info('Connected to Flutter application.');
|
||||
return driver;
|
||||
}
|
||||
|
||||
FlutterDriver.connectedTo(this._serviceClient, this._appIsolate);
|
||||
|
||||
/// Client connected to the Dart VM running the Flutter application
|
||||
final VMServiceClient _serviceClient;
|
||||
/// The main isolate hosting the Flutter application
|
||||
final VMIsolateRef _appIsolate;
|
||||
|
||||
Future<Map<String, dynamic>> _sendCommand(Command command) async {
|
||||
Map<String, dynamic> json = <String, dynamic>{'kind': command.kind}
|
||||
..addAll(command.toJson());
|
||||
return _appIsolate.invokeExtension(_flutterExtensionMethod, json)
|
||||
.then((Map<String, dynamic> result) => result, onError: (error, stackTrace) {
|
||||
throw new DriverError(
|
||||
'Failed to fulfill ${command.runtimeType} due to remote error',
|
||||
error,
|
||||
stackTrace
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// Checks the status of the Flutter Driver extension.
|
||||
Future<Health> checkHealth() async {
|
||||
return Health.fromJson(await _sendCommand(new GetHealth()));
|
||||
}
|
||||
|
||||
Future<ObjectRef> findByValueKey(dynamic key) async {
|
||||
return ObjectRef.fromJson(await _sendCommand(new FindByValueKey(key)));
|
||||
}
|
||||
|
||||
Future<Null> tap(ObjectRef ref) async {
|
||||
return await _sendCommand(new Tap(ref)).then((_) => null);
|
||||
}
|
||||
|
||||
Future<String> getText(ObjectRef ref) async {
|
||||
GetTextResult result = GetTextResult.fromJson(await _sendCommand(new GetText(ref)));
|
||||
return result.text;
|
||||
}
|
||||
|
||||
/// Closes the underlying connection to the VM service.
|
||||
///
|
||||
/// Returns a [Future] that fires once the connection has been closed.
|
||||
// TODO(yjbanov): cleanup object references
|
||||
Future close() => _serviceClient.close().then((_) {
|
||||
// Don't leak vm_service_client-specific objects, if any
|
||||
return null;
|
||||
});
|
||||
}
|
81
packages/flutter_driver/lib/src/error.dart
Normal file
81
packages/flutter_driver/lib/src/error.dart
Normal file
@ -0,0 +1,81 @@
|
||||
// Copyright 2016 The Chromium 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:io' show stderr;
|
||||
|
||||
/// Standard error thrown by Flutter Driver API.
|
||||
class DriverError extends Error {
|
||||
DriverError(this.message, [this.originalError, this.originalStackTrace]);
|
||||
|
||||
/// Human-readable error message.
|
||||
final String message;
|
||||
|
||||
final dynamic originalError;
|
||||
final dynamic originalStackTrace;
|
||||
|
||||
String toString() => 'DriverError: $message';
|
||||
}
|
||||
|
||||
// Whether someone redirected the log messages somewhere.
|
||||
bool _noLogSubscribers = true;
|
||||
|
||||
final StreamController<LogRecord> _logger =
|
||||
new StreamController<LogRecord>.broadcast(sync: true, onListen: () {
|
||||
_noLogSubscribers = false;
|
||||
});
|
||||
|
||||
void _log(LogLevel level, String loggerName, Object message) {
|
||||
LogRecord record = new LogRecord._(level, loggerName, '$message');
|
||||
// If nobody expressed interest in rerouting log messages somewhere specific,
|
||||
// print them to stderr.
|
||||
if (_noLogSubscribers)
|
||||
stderr.writeln(record);
|
||||
else
|
||||
_logger.add(record);
|
||||
}
|
||||
|
||||
/// Emits log records from Flutter Driver.
|
||||
final Stream<LogRecord> flutterDriverLog = _logger.stream;
|
||||
|
||||
/// Severity of a log entry.
|
||||
enum LogLevel { trace, info, warning, error, critical }
|
||||
|
||||
/// A log entry.
|
||||
class LogRecord {
|
||||
const LogRecord._(this.level, this.loggerName, this.message);
|
||||
|
||||
final LogLevel level;
|
||||
final String loggerName;
|
||||
final String message;
|
||||
|
||||
String toString() => '[${"$level".split(".").last}] $loggerName: $message';
|
||||
}
|
||||
|
||||
/// Package-private; users should use other public logging libraries.
|
||||
class Logger {
|
||||
Logger(this.name);
|
||||
|
||||
final String name;
|
||||
|
||||
void trace(Object message) {
|
||||
_log(LogLevel.trace, name, message);
|
||||
}
|
||||
|
||||
void info(Object message) {
|
||||
_log(LogLevel.info, name, message);
|
||||
}
|
||||
|
||||
void warning(Object message) {
|
||||
_log(LogLevel.warning, name, message);
|
||||
}
|
||||
|
||||
void error(Object message) {
|
||||
_log(LogLevel.error, name, message);
|
||||
}
|
||||
|
||||
void critical(Object message) {
|
||||
_log(LogLevel.critical, name, message);
|
||||
}
|
||||
}
|
137
packages/flutter_driver/lib/src/extension.dart
Normal file
137
packages/flutter_driver/lib/src/extension.dart
Normal file
@ -0,0 +1,137 @@
|
||||
// Copyright 2016 The Chromium 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:convert';
|
||||
import 'dart:developer';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/src/instrumentation.dart';
|
||||
|
||||
import 'error.dart';
|
||||
import 'find.dart';
|
||||
import 'gesture.dart';
|
||||
import 'health.dart';
|
||||
import 'message.dart';
|
||||
|
||||
const String _extensionMethod = 'ext.flutter_driver';
|
||||
|
||||
bool _flutterDriverExtensionEnabled = false;
|
||||
|
||||
/// Enables Flutter Driver VM service extension.
|
||||
///
|
||||
/// This extension is required for tests that use `package:flutter_driver` to
|
||||
/// drive applications from a separate process.
|
||||
///
|
||||
/// Call this function prior to running your application, e.g. before you call
|
||||
/// `runApp`.
|
||||
void enableFlutterDriverExtension() {
|
||||
if (_flutterDriverExtensionEnabled)
|
||||
return;
|
||||
FlutterDriverExtension extension = new FlutterDriverExtension();
|
||||
registerExtension(_extensionMethod, (String methodName, Map<String, String> params) {
|
||||
return extension.call(params);
|
||||
});
|
||||
_flutterDriverExtensionEnabled = true;
|
||||
}
|
||||
|
||||
/// Handles a command and returns a result.
|
||||
typedef Future<R> CommandHandlerCallback<R extends Result>(Command c);
|
||||
|
||||
/// Deserializes JSON map to a command object.
|
||||
typedef Command CommandDeserializerCallback(Map<String, String> params);
|
||||
|
||||
class FlutterDriverExtension {
|
||||
static final Logger _log = new Logger('FlutterDriverExtension');
|
||||
|
||||
FlutterDriverExtension() {
|
||||
_commandHandlers = {
|
||||
'get_health': getHealth,
|
||||
'find_by_value_key': findByValueKey,
|
||||
'tap': tap,
|
||||
'get_text': getText,
|
||||
};
|
||||
|
||||
_commandDeserializers = {
|
||||
'get_health': GetHealth.fromJson,
|
||||
'find_by_value_key': FindByValueKey.fromJson,
|
||||
'tap': Tap.fromJson,
|
||||
'get_text': GetText.fromJson,
|
||||
};
|
||||
}
|
||||
|
||||
final Instrumentation prober = new Instrumentation();
|
||||
|
||||
Map<String, CommandHandlerCallback> _commandHandlers =
|
||||
<String, CommandHandlerCallback>{};
|
||||
|
||||
Map<String, CommandDeserializerCallback> _commandDeserializers =
|
||||
<String, CommandDeserializerCallback>{};
|
||||
|
||||
Future<ServiceExtensionResponse> call(Map<String, String> params) async {
|
||||
String commandKind = params['kind'];
|
||||
CommandHandlerCallback commandHandler = _commandHandlers[commandKind];
|
||||
CommandDeserializerCallback commandDeserializer =
|
||||
_commandDeserializers[commandKind];
|
||||
|
||||
if (commandHandler == null || commandDeserializer == null) {
|
||||
return new ServiceExtensionResponse.error(
|
||||
ServiceExtensionResponse.kInvalidParams,
|
||||
'Extension $_extensionMethod does not support command $commandKind'
|
||||
);
|
||||
}
|
||||
|
||||
Command command = commandDeserializer(params);
|
||||
return commandHandler(command).then((Result result) {
|
||||
return new ServiceExtensionResponse.result(JSON.encode(result.toJson()));
|
||||
}, onError: (e, s) {
|
||||
_log.warning('$e:\n$s');
|
||||
return new ServiceExtensionResponse.error(
|
||||
ServiceExtensionResponse.kExtensionError, '$e');
|
||||
});
|
||||
}
|
||||
|
||||
Future<Health> getHealth(GetHealth command) async => new Health(HealthStatus.ok);
|
||||
|
||||
Future<ObjectRef> findByValueKey(FindByValueKey command) {
|
||||
Element elem = prober.findElementByKey(new ValueKey<dynamic>(command.keyValue));
|
||||
ObjectRef elemRef = elem != null
|
||||
? new ObjectRef(_registerObject(elem))
|
||||
: new ObjectRef.notFound();
|
||||
return new Future.value(elemRef);
|
||||
}
|
||||
|
||||
Future<TapResult> tap(Tap command) async {
|
||||
Element target = await _dereferenceOrDie(command.targetRef);
|
||||
prober.tap(target);
|
||||
return new TapResult();
|
||||
}
|
||||
|
||||
Future<GetTextResult> getText(GetText command) async {
|
||||
Element target = await _dereferenceOrDie(command.targetRef);
|
||||
// TODO(yjbanov): support more ways to read text
|
||||
Text text = target.widget;
|
||||
return new GetTextResult(text.data);
|
||||
}
|
||||
|
||||
int _refCounter = 1;
|
||||
final Map<String, Object> _objectRefs = <String, Object>{};
|
||||
String _registerObject(Object obj) {
|
||||
if (obj == null)
|
||||
throw new ArgumentError('Cannot register null object');
|
||||
String refKey = '${_refCounter++}';
|
||||
_objectRefs[refKey] = obj;
|
||||
return refKey;
|
||||
}
|
||||
|
||||
dynamic _dereference(String reference) => _objectRefs[reference];
|
||||
|
||||
Future<dynamic> _dereferenceOrDie(String reference) {
|
||||
Element object = _dereference(reference);
|
||||
|
||||
if (object == null)
|
||||
return new Future.error('Object reference not found ($reference).');
|
||||
|
||||
return new Future.value(object);
|
||||
}
|
||||
}
|
82
packages/flutter_driver/lib/src/find.dart
Normal file
82
packages/flutter_driver/lib/src/find.dart
Normal file
@ -0,0 +1,82 @@
|
||||
// Copyright 2016 The Chromium 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 'error.dart';
|
||||
import 'message.dart';
|
||||
|
||||
const List<Type> _supportedKeyValueTypes = const <Type>[String, int];
|
||||
|
||||
/// Command to find an element by a value key.
|
||||
class FindByValueKey extends Command {
|
||||
final String kind = 'find_by_value_key';
|
||||
|
||||
FindByValueKey(dynamic keyValue)
|
||||
: this.keyValue = keyValue,
|
||||
this.keyValueString = '$keyValue',
|
||||
this.keyValueType = '${keyValue.runtimeType}' {
|
||||
if (!_supportedKeyValueTypes.contains(keyValue.runtimeType))
|
||||
_throwInvalidKeyValueType('$keyValue.runtimeType');
|
||||
}
|
||||
|
||||
/// The true value of the key.
|
||||
final dynamic keyValue;
|
||||
|
||||
/// Stringified value of the key (we can only send strings to the VM service)
|
||||
final String keyValueString;
|
||||
|
||||
/// The type name of the key.
|
||||
///
|
||||
/// May be one of "String", "int". The list of supported types may change.
|
||||
final String keyValueType;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'keyValueString': keyValueString,
|
||||
'keyValueType': keyValueType,
|
||||
};
|
||||
|
||||
static FindByValueKey fromJson(Map<String, dynamic> json) {
|
||||
String keyValueString = json['keyValueString'];
|
||||
String keyValueType = json['keyValueType'];
|
||||
switch(keyValueType) {
|
||||
case 'int':
|
||||
return new FindByValueKey(int.parse(keyValueString));
|
||||
case 'String':
|
||||
return new FindByValueKey(keyValueString);
|
||||
default:
|
||||
return _throwInvalidKeyValueType(keyValueType);
|
||||
}
|
||||
}
|
||||
|
||||
static _throwInvalidKeyValueType(String invalidType) {
|
||||
throw new DriverError('Unsupported key value type $invalidType. Flutter Driver only supports ${_supportedKeyValueTypes.join(", ")}');
|
||||
}
|
||||
}
|
||||
|
||||
/// Command to read the text from a given element.
|
||||
class GetText extends CommandWithTarget {
|
||||
final String kind = 'get_text';
|
||||
|
||||
static GetText fromJson(Map<String, dynamic> json) {
|
||||
return new GetText(new ObjectRef(json['targetRef']));
|
||||
}
|
||||
|
||||
/// [targetRef] identifies an element that contains a piece of text.
|
||||
GetText(ObjectRef targetRef) : super(targetRef);
|
||||
|
||||
Map<String, dynamic> toJson() => super.toJson();
|
||||
}
|
||||
|
||||
class GetTextResult extends Result {
|
||||
static GetTextResult fromJson(Map<String, dynamic> json) {
|
||||
return new GetTextResult(json['text']);
|
||||
}
|
||||
|
||||
GetTextResult(this.text);
|
||||
|
||||
final String text;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'text': text,
|
||||
};
|
||||
}
|
25
packages/flutter_driver/lib/src/gesture.dart
Normal file
25
packages/flutter_driver/lib/src/gesture.dart
Normal file
@ -0,0 +1,25 @@
|
||||
// Copyright 2016 The Chromium 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 'message.dart';
|
||||
|
||||
class Tap extends CommandWithTarget {
|
||||
final String kind = 'tap';
|
||||
|
||||
Tap(ObjectRef targetRef) : super(targetRef);
|
||||
|
||||
static Tap fromJson(Map<String, dynamic> json) {
|
||||
return new Tap(new ObjectRef(json['targetRef']));
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => super.toJson();
|
||||
}
|
||||
|
||||
class TapResult extends Result {
|
||||
static TapResult fromJson(Map<String, dynamic> json) {
|
||||
return new TapResult();
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {};
|
||||
}
|
54
packages/flutter_driver/lib/src/health.dart
Normal file
54
packages/flutter_driver/lib/src/health.dart
Normal file
@ -0,0 +1,54 @@
|
||||
// Copyright 2016 The Chromium 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 'message.dart';
|
||||
|
||||
/// Requests an application health check.
|
||||
class GetHealth implements Command {
|
||||
final String kind = 'get_health';
|
||||
|
||||
static fromJson(Map<String, dynamic> json) => new GetHealth();
|
||||
|
||||
Map<String, dynamic> toJson() => const {};
|
||||
}
|
||||
|
||||
/// Application health status.
|
||||
enum HealthStatus {
|
||||
/// Application is known to be in a good shape and should be able to respond.
|
||||
ok,
|
||||
|
||||
/// Application is not known to be in a good shape and may be unresponsive.
|
||||
bad,
|
||||
}
|
||||
|
||||
/// Application health status.
|
||||
class Health extends Result {
|
||||
Health(this.status) {
|
||||
assert(status != null);
|
||||
}
|
||||
|
||||
static Health fromJson(Map<String, dynamic> json) {
|
||||
return new Health(_statusFromId(json['status']));
|
||||
}
|
||||
|
||||
/// Health status
|
||||
final HealthStatus status;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'status': _getStatusId(status)
|
||||
};
|
||||
}
|
||||
|
||||
String _getStatusId(HealthStatus status) => status.toString().split('.').last;
|
||||
|
||||
final Map<String, HealthStatus> _idToStatus = new Map<String, HealthStatus>.fromIterable(
|
||||
HealthStatus.values,
|
||||
key: _getStatusId
|
||||
);
|
||||
|
||||
HealthStatus _statusFromId(String id) {
|
||||
return _idToStatus.containsKey(id)
|
||||
? _idToStatus[id]
|
||||
: throw new ArgumentError.value(id, 'id', 'unknown');
|
||||
}
|
75
packages/flutter_driver/lib/src/message.dart
Normal file
75
packages/flutter_driver/lib/src/message.dart
Normal file
@ -0,0 +1,75 @@
|
||||
// Copyright 2016 The Chromium 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 'error.dart';
|
||||
|
||||
/// A piece of data travelling between Flutter Driver and a Flutter application.
|
||||
abstract class Message {
|
||||
/// Serializes this message to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
}
|
||||
|
||||
/// A message that travels from the Flutter Driver to a Flutter application to
|
||||
/// instruct the application to perform a task.
|
||||
abstract class Command extends Message {
|
||||
/// Identifies the type of the command object and of the handler.
|
||||
String get kind;
|
||||
}
|
||||
|
||||
/// A message sent from a Flutter application back to the Flutter Driver in
|
||||
/// response to a command.
|
||||
abstract class Result extends Message { }
|
||||
|
||||
/// A serializable reference to an object that lives in the application isolate.
|
||||
class ObjectRef extends Result {
|
||||
ObjectRef(this.objectReferenceKey);
|
||||
|
||||
ObjectRef.notFound() : this(null);
|
||||
|
||||
static ObjectRef fromJson(Map<String, dynamic> json) {
|
||||
return json['objectReferenceKey'] != null
|
||||
? new ObjectRef(json['objectReferenceKey'])
|
||||
: null;
|
||||
}
|
||||
|
||||
/// Identifier used to dereference an object.
|
||||
///
|
||||
/// This value is generated by the application-side isolate. Flutter driver
|
||||
/// tests should not generate these keys.
|
||||
final String objectReferenceKey;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'objectReferenceKey': objectReferenceKey,
|
||||
};
|
||||
}
|
||||
|
||||
/// A command aimed at an object represented by [targetRef].
|
||||
///
|
||||
/// Implementations must provide a concrete [kind]. If additional data is
|
||||
/// required beyond the [targetRef] the implementation may override [toJson]
|
||||
/// and add more keys to the returned map.
|
||||
abstract class CommandWithTarget extends Command {
|
||||
CommandWithTarget(ObjectRef ref) : this.targetRef = ref?.objectReferenceKey {
|
||||
if (ref == null)
|
||||
throw new DriverError('${this.runtimeType} target cannot be null');
|
||||
|
||||
if (ref.objectReferenceKey == null)
|
||||
throw new DriverError('${this.runtimeType} target reference cannot be null');
|
||||
}
|
||||
|
||||
/// Refers to the object targeted by this command.
|
||||
final String targetRef;
|
||||
|
||||
/// This method is meant to be overridden if data in addition to [targetRef]
|
||||
/// is serialized to JSON.
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// Map<String, dynamic> toJson() => super.toJson()..addAll({
|
||||
/// 'foo': this.foo,
|
||||
/// });
|
||||
Map<String, dynamic> toJson() => {
|
||||
'targetRef': targetRef,
|
||||
};
|
||||
}
|
24
packages/flutter_driver/pubspec.yaml
Normal file
24
packages/flutter_driver/pubspec.yaml
Normal file
@ -0,0 +1,24 @@
|
||||
name: flutter_driver
|
||||
version: 0.0.1
|
||||
description: Integration and performance test API for Flutter applications
|
||||
homepage: http://flutter.io
|
||||
author: Flutter Authors <flutter-dev@googlegroups.com>
|
||||
|
||||
environment:
|
||||
sdk: '>=1.12.0 <2.0.0'
|
||||
|
||||
dependencies:
|
||||
vm_service_client:
|
||||
git:
|
||||
url: git://github.com/yjbanov/vm_service_client.git
|
||||
ref: 54085d1
|
||||
json_rpc_2: any
|
||||
logging: '>=0.11.0 <1.0.0'
|
||||
flutter:
|
||||
path: '../flutter'
|
||||
flutter_test:
|
||||
path: '../flutter_test'
|
||||
|
||||
dev_dependencies:
|
||||
test: '>=0.12.6 <1.0.0'
|
||||
mockito: ^0.10.1
|
208
packages/flutter_driver/test/flutter_driver_test.dart
Normal file
208
packages/flutter_driver/test/flutter_driver_test.dart
Normal file
@ -0,0 +1,208 @@
|
||||
// Copyright 2016 The Chromium 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:test/test.dart';
|
||||
import 'package:flutter_driver/src/driver.dart';
|
||||
import 'package:flutter_driver/src/error.dart';
|
||||
import 'package:flutter_driver/src/health.dart';
|
||||
import 'package:flutter_driver/src/message.dart';
|
||||
import 'package:json_rpc_2/json_rpc_2.dart' as rpc;
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:vm_service_client/vm_service_client.dart';
|
||||
|
||||
main() {
|
||||
group('FlutterDriver.connect', () {
|
||||
List<LogRecord> log;
|
||||
StreamSubscription logSub;
|
||||
MockVMServiceClient mockClient;
|
||||
MockVM mockVM;
|
||||
MockIsolate mockIsolate;
|
||||
|
||||
expectLogContains(String message) {
|
||||
expect(log.map((r) => '$r'), anyElement(contains(message)));
|
||||
}
|
||||
|
||||
setUp(() {
|
||||
log = <LogRecord>[];
|
||||
logSub = flutterDriverLog.listen(log.add);
|
||||
mockClient = new MockVMServiceClient();
|
||||
mockVM = new MockVM();
|
||||
mockIsolate = new MockIsolate();
|
||||
when(mockClient.getVM()).thenReturn(mockVM);
|
||||
when(mockVM.isolates).thenReturn([mockIsolate]);
|
||||
when(mockIsolate.load()).thenReturn(mockIsolate);
|
||||
when(mockIsolate.invokeExtension(any, any))
|
||||
.thenReturn(new Future.value({'status': 'ok'}));
|
||||
vmServiceConnectFunction = (_) => new Future.value(mockClient);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await logSub.cancel();
|
||||
vmServiceConnectFunction = vmServiceClientConnectFunction;
|
||||
});
|
||||
|
||||
test('connects to isolate paused at start', () async {
|
||||
when(mockIsolate.pauseEvent).thenReturn(new MockVMPauseStartEvent());
|
||||
when(mockIsolate.resume()).thenReturn(new Future.value());
|
||||
MockVMServiceExtension mockExtension = new MockVMServiceExtension();
|
||||
when(mockExtension.method).thenReturn('ext.flutter_driver');
|
||||
when(mockIsolate.onServiceExtensionAdded)
|
||||
.thenReturn(new Stream.fromIterable([mockExtension]));
|
||||
|
||||
FlutterDriver driver = await FlutterDriver.connect();
|
||||
expect(driver, isNotNull);
|
||||
expectLogContains('Isolate is paused at start');
|
||||
});
|
||||
|
||||
test('connects to isolate paused mid-flight', () async {
|
||||
when(mockIsolate.pauseEvent).thenReturn(new MockVMPauseBreakpointEvent());
|
||||
when(mockIsolate.resume()).thenReturn(new Future.value());
|
||||
|
||||
FlutterDriver driver = await FlutterDriver.connect();
|
||||
expect(driver, isNotNull);
|
||||
expectLogContains('Isolate is paused mid-flight');
|
||||
});
|
||||
|
||||
// This test simulates a situation when we believe that the isolate is
|
||||
// currently paused, but something else (e.g. a debugger) resumes it before
|
||||
// we do. There's no need to fail as we should be able to drive the app
|
||||
// just fine.
|
||||
test('connects despite losing the race to resume isolate', () async {
|
||||
when(mockIsolate.pauseEvent).thenReturn(new MockVMPauseBreakpointEvent());
|
||||
when(mockIsolate.resume()).thenAnswer((_) {
|
||||
// This needs to be wrapped in a closure to not be considered uncaught
|
||||
// by package:test
|
||||
return new Future.error(new rpc.RpcException(101, ''));
|
||||
});
|
||||
|
||||
FlutterDriver driver = await FlutterDriver.connect();
|
||||
expect(driver, isNotNull);
|
||||
expectLogContains('Attempted to resume an already resumed isolate');
|
||||
});
|
||||
|
||||
test('connects to unpaused isolate', () async {
|
||||
when(mockIsolate.pauseEvent).thenReturn(new MockVMResumeEvent());
|
||||
FlutterDriver driver = await FlutterDriver.connect();
|
||||
expect(driver, isNotNull);
|
||||
expectLogContains('Isolate is not paused. Assuming application is ready.');
|
||||
});
|
||||
});
|
||||
|
||||
group('FlutterDriver', () {
|
||||
MockVMServiceClient mockClient;
|
||||
MockIsolate mockIsolate;
|
||||
FlutterDriver driver;
|
||||
|
||||
setUp(() {
|
||||
mockClient = new MockVMServiceClient();
|
||||
mockIsolate = new MockIsolate();
|
||||
driver = new FlutterDriver.connectedTo(mockClient, mockIsolate);
|
||||
});
|
||||
|
||||
test('checks the health of the driver extension', () async {
|
||||
when(mockIsolate.invokeExtension(any, any)).thenReturn(new Future.value({
|
||||
'status': 'ok',
|
||||
}));
|
||||
Health result = await driver.checkHealth();
|
||||
expect(result.status, HealthStatus.ok);
|
||||
});
|
||||
|
||||
test('closes connection', () async {
|
||||
when(mockClient.close()).thenReturn(new Future.value());
|
||||
await driver.close();
|
||||
});
|
||||
|
||||
group('findByValueKey', () {
|
||||
test('restricts value types', () async {
|
||||
expect(driver.findByValueKey(null),
|
||||
throwsA(new isInstanceOf<DriverError>()));
|
||||
});
|
||||
|
||||
test('finds by ValueKey', () async {
|
||||
when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) {
|
||||
expect(i.positionalArguments[1], {
|
||||
'kind': 'find_by_value_key',
|
||||
'keyValueString': 'foo',
|
||||
'keyValueType': 'String',
|
||||
});
|
||||
return new Future.value({
|
||||
'objectReferenceKey': '123',
|
||||
});
|
||||
});
|
||||
ObjectRef result = await driver.findByValueKey('foo');
|
||||
expect(result, isNotNull);
|
||||
expect(result.objectReferenceKey, '123');
|
||||
});
|
||||
});
|
||||
|
||||
group('tap', () {
|
||||
test('requires a target reference', () async {
|
||||
expect(driver.tap(null), throwsA(new isInstanceOf<DriverError>()));
|
||||
});
|
||||
|
||||
test('requires a valid target reference', () async {
|
||||
expect(driver.tap(new ObjectRef.notFound()),
|
||||
throwsA(new isInstanceOf<DriverError>()));
|
||||
});
|
||||
|
||||
test('sends the tap command', () async {
|
||||
when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) {
|
||||
expect(i.positionalArguments[1], {
|
||||
'kind': 'tap',
|
||||
'targetRef': '123'
|
||||
});
|
||||
return new Future.value();
|
||||
});
|
||||
await driver.tap(new ObjectRef('123'));
|
||||
});
|
||||
});
|
||||
|
||||
group('getText', () {
|
||||
test('requires a target reference', () async {
|
||||
expect(driver.getText(null), throwsA(new isInstanceOf<DriverError>()));
|
||||
});
|
||||
|
||||
test('requires a valid target reference', () async {
|
||||
expect(driver.getText(new ObjectRef.notFound()),
|
||||
throwsA(new isInstanceOf<DriverError>()));
|
||||
});
|
||||
|
||||
test('sends the getText command', () async {
|
||||
when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) {
|
||||
expect(i.positionalArguments[1], {
|
||||
'kind': 'get_text',
|
||||
'targetRef': '123'
|
||||
});
|
||||
return new Future.value({
|
||||
'text': 'hello'
|
||||
});
|
||||
});
|
||||
String result = await driver.getText(new ObjectRef('123'));
|
||||
expect(result, 'hello');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@proxy
|
||||
class MockVMServiceClient extends Mock implements VMServiceClient { }
|
||||
|
||||
@proxy
|
||||
class MockVM extends Mock implements VM { }
|
||||
|
||||
@proxy
|
||||
class MockIsolate extends Mock implements VMRunnableIsolate { }
|
||||
|
||||
@proxy
|
||||
class MockVMPauseStartEvent extends Mock implements VMPauseStartEvent { }
|
||||
|
||||
@proxy
|
||||
class MockVMPauseBreakpointEvent extends Mock implements VMPauseBreakpointEvent { }
|
||||
|
||||
@proxy
|
||||
class MockVMResumeEvent extends Mock implements VMResumeEvent { }
|
||||
|
||||
@proxy
|
||||
class MockVMServiceExtension extends Mock implements VMServiceExtension { }
|
@ -15,5 +15,6 @@ flutter analyze --flutter-repo --no-current-directory --no-current-package --con
|
||||
(cd packages/newton; pub run test -j1)
|
||||
# (cd packages/playfair; ) # No tests to run.
|
||||
# (cd packages/updater; ) # No tests to run.
|
||||
(cd packages/flutter_driver; pub run test -j1)
|
||||
|
||||
(cd examples/stocks; flutter test)
|
||||
|
Loading…
x
Reference in New Issue
Block a user