Commit b0e45594 authored by Yegor Jbanov's avatar Yegor Jbanov Committed by yjbanov

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)
parent d93a87ee
// 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;
// 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;
// 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;
});
}
// 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);
}
}
// 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);
}
}
// 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,
};
}
// 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() => {};
}
// 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');
}
// 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,
};
}
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
// 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)
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