blob: c532f515ca86fa9e75bc8420f1a860b35a675e41 [file] [log] [blame]
// 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:flutter_driver/src/common/error.dart';
import 'package:flutter_driver/src/common/health.dart';
import 'package:flutter_driver/src/driver/driver.dart';
import 'package:flutter_driver/src/driver/timeline.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';
import 'package:quiver/testing/async.dart';
import 'common.dart';
/// Magical timeout value that's different from the default.
const Duration _kTestTimeout = Duration(milliseconds: 1234);
const String _kSerializedTestTimeout = '1234';
void main() {
group('FlutterDriver.connect', () {
List<LogRecord> log;
StreamSubscription<LogRecord> logSub;
MockVMServiceClient mockClient;
MockVM mockVM;
MockIsolate mockIsolate;
MockPeer mockPeer;
void expectLogContains(String message) {
expect(log.map<String>((LogRecord r) => '$r'), anyElement(contains(message)));
}
setUp(() {
log = <LogRecord>[];
logSub = flutterDriverLog.listen(log.add);
mockClient = MockVMServiceClient();
mockVM = MockVM();
mockIsolate = MockIsolate();
mockPeer = MockPeer();
when(mockClient.getVM()).thenAnswer((_) => Future<MockVM>.value(mockVM));
when(mockVM.isolates).thenReturn(<VMRunnableIsolate>[mockIsolate]);
when(mockIsolate.loadRunnable()).thenAnswer((_) => Future<MockIsolate>.value(mockIsolate));
when(mockIsolate.invokeExtension(any, any)).thenAnswer(
(Invocation invocation) => makeMockResponse(<String, dynamic>{'status': 'ok'}));
vmServiceConnectFunction = (String url) {
return Future<VMServiceClientConnection>.value(
VMServiceClientConnection(mockClient, mockPeer)
);
};
});
tearDown(() async {
await logSub.cancel();
restoreVmServiceConnectFunction();
});
test('connects to isolate paused at start', () async {
final List<String> connectionLog = <String>[];
when(mockPeer.sendRequest('streamListen', any)).thenAnswer((Invocation invocation) {
connectionLog.add('streamListen');
return null;
});
when(mockIsolate.pauseEvent).thenReturn(MockVMPauseStartEvent());
when(mockIsolate.resume()).thenAnswer((Invocation invocation) {
connectionLog.add('resume');
return Future<dynamic>.value(null);
});
when(mockIsolate.onExtensionAdded).thenAnswer((Invocation invocation) {
connectionLog.add('onExtensionAdded');
return Stream<String>.fromIterable(<String>['ext.flutter.driver']);
});
final FlutterDriver driver = await FlutterDriver.connect(dartVmServiceUrl: '');
expect(driver, isNotNull);
expectLogContains('Isolate is paused at start');
expect(connectionLog, <String>['streamListen', 'onExtensionAdded', 'resume']);
});
test('connects to isolate paused mid-flight', () async {
when(mockIsolate.pauseEvent).thenReturn(MockVMPauseBreakpointEvent());
when(mockIsolate.resume()).thenAnswer((Invocation invocation) => Future<dynamic>.value(null));
final FlutterDriver driver = await FlutterDriver.connect(dartVmServiceUrl: '');
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(MockVMPauseBreakpointEvent());
when(mockIsolate.resume()).thenAnswer((Invocation invocation) {
// This needs to be wrapped in a closure to not be considered uncaught
// by package:test
return Future<dynamic>.error(rpc.RpcException(101, ''));
});
final FlutterDriver driver = await FlutterDriver.connect(dartVmServiceUrl: '');
expect(driver, isNotNull);
expectLogContains('Attempted to resume an already resumed isolate');
});
test('connects to unpaused isolate', () async {
when(mockIsolate.pauseEvent).thenReturn(MockVMResumeEvent());
final FlutterDriver driver = await FlutterDriver.connect(dartVmServiceUrl: '');
expect(driver, isNotNull);
expectLogContains('Isolate is not paused. Assuming application is ready.');
});
});
group('FlutterDriver', () {
MockVMServiceClient mockClient;
MockPeer mockPeer;
MockIsolate mockIsolate;
FlutterDriver driver;
setUp(() {
mockClient = MockVMServiceClient();
mockPeer = MockPeer();
mockIsolate = MockIsolate();
driver = FlutterDriver.connectedTo(mockClient, mockPeer, mockIsolate);
});
test('checks the health of the driver extension', () async {
when(mockIsolate.invokeExtension(any, any)).thenAnswer(
(Invocation invocation) => makeMockResponse(<String, dynamic>{'status': 'ok'}));
final Health result = await driver.checkHealth();
expect(result.status, HealthStatus.ok);
});
test('closes connection', () async {
when(mockClient.close()).thenAnswer((Invocation invocation) => Future<dynamic>.value(null));
await driver.close();
});
group('ByValueKey', () {
test('restricts value types', () async {
expect(() => find.byValueKey(null),
throwsA(isInstanceOf<DriverError>()));
});
test('finds by ValueKey', () async {
when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) {
expect(i.positionalArguments[1], <String, String>{
'command': 'tap',
'timeout': _kSerializedTestTimeout,
'finderType': 'ByValueKey',
'keyValueString': 'foo',
'keyValueType': 'String',
});
return makeMockResponse(<String, dynamic>{});
});
await driver.tap(find.byValueKey('foo'), timeout: _kTestTimeout);
});
});
group('BySemanticsLabel', () {
test('finds by Semantic label using String', () async {
when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) {
expect(i.positionalArguments[1], <String, String>{
'command': 'tap',
'timeout': _kSerializedTestTimeout,
'finderType': 'BySemanticsLabel',
'label': 'foo',
});
return makeMockResponse(<String, dynamic>{});
});
await driver.tap(find.bySemanticsLabel('foo'), timeout: _kTestTimeout);
});
test('finds by Semantic label using RegExp', () async {
when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) {
expect(i.positionalArguments[1], <String, String>{
'command': 'tap',
'timeout': _kSerializedTestTimeout,
'finderType': 'BySemanticsLabel',
'label': '^foo',
'isRegExp': 'true',
});
return makeMockResponse(<String, dynamic>{});
});
await driver.tap(find.bySemanticsLabel(RegExp('^foo')), timeout: _kTestTimeout);
});
});
group('tap', () {
test('requires a target reference', () async {
expect(driver.tap(null), throwsA(isInstanceOf<DriverError>()));
});
test('sends the tap command', () async {
when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) {
expect(i.positionalArguments[1], <String, dynamic>{
'command': 'tap',
'timeout': _kSerializedTestTimeout,
'finderType': 'ByText',
'text': 'foo',
});
return makeMockResponse(<String, dynamic>{});
});
await driver.tap(find.text('foo'), timeout: _kTestTimeout);
});
});
group('getText', () {
test('requires a target reference', () async {
expect(driver.getText(null), throwsA(isInstanceOf<DriverError>()));
});
test('sends the getText command', () async {
when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) {
expect(i.positionalArguments[1], <String, dynamic>{
'command': 'get_text',
'timeout': _kSerializedTestTimeout,
'finderType': 'ByValueKey',
'keyValueString': '123',
'keyValueType': 'int',
});
return makeMockResponse(<String, String>{
'text': 'hello',
});
});
final String result = await driver.getText(find.byValueKey(123), timeout: _kTestTimeout);
expect(result, 'hello');
});
});
group('waitFor', () {
test('requires a target reference', () async {
expect(driver.waitFor(null), throwsA(isInstanceOf<DriverError>()));
});
test('sends the waitFor command', () async {
when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) {
expect(i.positionalArguments[1], <String, dynamic>{
'command': 'waitFor',
'finderType': 'ByTooltipMessage',
'text': 'foo',
'timeout': _kSerializedTestTimeout,
});
return makeMockResponse(<String, dynamic>{});
});
await driver.waitFor(find.byTooltip('foo'), timeout: _kTestTimeout);
});
});
group('waitUntilNoTransientCallbacks', () {
test('sends the waitUntilNoTransientCallbacks command', () async {
when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) {
expect(i.positionalArguments[1], <String, dynamic>{
'command': 'waitUntilNoTransientCallbacks',
'timeout': _kSerializedTestTimeout,
});
return makeMockResponse(<String, dynamic>{});
});
await driver.waitUntilNoTransientCallbacks(timeout: _kTestTimeout);
});
});
group('clearTimeline', () {
test('clears timeline', () async {
bool clearWasCalled = false;
when(mockPeer.sendRequest('_clearVMTimeline', argThat(equals(<String, dynamic>{}))))
.thenAnswer((Invocation invocation) async {
clearWasCalled = true;
return null;
});
await driver.clearTimeline();
expect(clearWasCalled, isTrue);
});
});
group('traceAction', () {
List<String> log;
setUp(() async {
log = <String>[];
when(mockPeer.sendRequest('_clearVMTimeline', argThat(equals(<String, dynamic>{}))))
.thenAnswer((Invocation invocation) async {
log.add('clear');
return null;
});
when(mockPeer.sendRequest('_setVMTimelineFlags', argThat(equals(<String, dynamic>{'recordedStreams': '[all]'}))))
.thenAnswer((Invocation invocation) async {
log.add('startTracing');
return null;
});
when(mockPeer.sendRequest('_setVMTimelineFlags', argThat(equals(<String, dynamic>{'recordedStreams': '[]'}))))
.thenAnswer((Invocation invocation) async {
log.add('stopTracing');
return null;
});
when(mockPeer.sendRequest('_getVMTimeline')).thenAnswer((Invocation invocation) async {
log.add('download');
return <String, dynamic>{
'traceEvents': <dynamic>[
<String, String>{
'name': 'test event',
},
],
};
});
});
test('without clearing timeline', () async {
final Timeline timeline = await driver.traceAction(() async {
log.add('action');
}, retainPriorEvents: true);
expect(log, const <String>[
'startTracing',
'action',
'stopTracing',
'download',
]);
expect(timeline.events.single.name, 'test event');
});
test('with clearing timeline', () async {
final Timeline timeline = await driver.traceAction(() async {
log.add('action');
});
expect(log, const <String>[
'clear',
'startTracing',
'action',
'stopTracing',
'download',
]);
expect(timeline.events.single.name, 'test event');
});
});
group('traceAction with timeline streams', () {
test('specify non-default timeline streams', () async {
bool actionCalled = false;
bool startTracingCalled = false;
bool stopTracingCalled = false;
when(mockPeer.sendRequest('_setVMTimelineFlags', argThat(equals(<String, dynamic>{'recordedStreams': '[Dart, GC, Compiler]'}))))
.thenAnswer((Invocation invocation) async {
startTracingCalled = true;
return null;
});
when(mockPeer.sendRequest('_setVMTimelineFlags', argThat(equals(<String, dynamic>{'recordedStreams': '[]'}))))
.thenAnswer((Invocation invocation) async {
stopTracingCalled = true;
return null;
});
when(mockPeer.sendRequest('_getVMTimeline')).thenAnswer((Invocation invocation) async {
return <String, dynamic>{
'traceEvents': <dynamic>[
<String, String>{
'name': 'test event',
},
],
};
});
final Timeline timeline = await driver.traceAction(() async {
actionCalled = true;
},
streams: const <TimelineStream>[
TimelineStream.dart,
TimelineStream.gc,
TimelineStream.compiler,
],
retainPriorEvents: true);
expect(actionCalled, isTrue);
expect(startTracingCalled, isTrue);
expect(stopTracingCalled, isTrue);
expect(timeline.events.single.name, 'test event');
});
});
group('sendCommand error conditions', () {
test('local timeout', () async {
final List<String> log = <String>[];
final StreamSubscription<LogRecord> logSub = flutterDriverLog.listen((LogRecord s) => log.add(s.toString()));
when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) {
// completer never completed to trigger timeout
return Completer<Map<String, dynamic>>().future;
});
FakeAsync().run((FakeAsync time) {
driver.waitFor(find.byTooltip('foo'));
expect(log, <String>[]);
time.elapse(const Duration(hours: 1));
});
expect(log, <String>['[warning] FlutterDriver: waitFor message is taking a long time to complete...']);
await logSub.cancel();
});
test('remote error', () async {
when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) {
return makeMockResponse(<String, dynamic>{
'message': 'This is a failure',
}, isError: true);
});
try {
await driver.waitFor(find.byTooltip('foo'));
fail('expected an exception');
} catch (error) {
expect(error is DriverError, isTrue);
expect(error.message, 'Error in Flutter application: {message: This is a failure}');
}
});
});
});
group('FlutterDriver with custom timeout', () {
MockVMServiceClient mockClient;
MockPeer mockPeer;
MockIsolate mockIsolate;
FlutterDriver driver;
setUp(() {
mockClient = MockVMServiceClient();
mockPeer = MockPeer();
mockIsolate = MockIsolate();
driver = FlutterDriver.connectedTo(mockClient, mockPeer, mockIsolate);
});
test('GetHealth has no default timeout', () async {
when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) {
expect(i.positionalArguments[1], <String, String>{
'command': 'get_health',
});
return makeMockResponse(<String, dynamic>{'status': 'ok'});
});
await driver.checkHealth();
});
test('does not interfere with explicit timeouts', () async {
when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) {
expect(i.positionalArguments[1], <String, String>{
'command': 'get_health',
'timeout': _kSerializedTestTimeout,
});
return makeMockResponse(<String, dynamic>{'status': 'ok'});
});
await driver.checkHealth(timeout: _kTestTimeout);
});
});
}
Future<Map<String, dynamic>> makeMockResponse(
Map<String, dynamic> response, {
bool isError = false,
}) {
return Future<Map<String, dynamic>>.value(<String, dynamic>{
'isError': isError,
'response': response,
});
}
class MockVMServiceClient extends Mock implements VMServiceClient { }
class MockVM extends Mock implements VM { }
class MockIsolate extends Mock implements VMRunnableIsolate { }
class MockVMPauseStartEvent extends Mock implements VMPauseStartEvent { }
class MockVMPauseBreakpointEvent extends Mock implements VMPauseBreakpointEvent { }
class MockVMResumeEvent extends Mock implements VMResumeEvent { }
class MockPeer extends Mock implements rpc.Peer { }