// 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 '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 log; StreamSubscription logSub; MockVMServiceClient mockClient; MockVM mockVM; MockIsolate mockIsolate; MockPeer mockPeer; void expectLogContains(String message) { expect(log.map((LogRecord r) => '$r'), anyElement(contains(message))); } setUp(() { log = []; logSub = flutterDriverLog.listen(log.add); mockClient = new MockVMServiceClient(); mockVM = new MockVM(); mockIsolate = new MockIsolate(); mockPeer = new MockPeer(); when(mockClient.getVM()).thenAnswer((_) => new Future.value(mockVM)); when(mockVM.isolates).thenReturn([mockIsolate]); when(mockIsolate.loadRunnable()).thenAnswer((_) => new Future.value(mockIsolate)); when(mockIsolate.invokeExtension(any, any)).thenAnswer( (Invocation invocation) => makeMockResponse({'status': 'ok'})); vmServiceConnectFunction = (String url) { return new Future.value( new VMServiceClientConnection(mockClient, mockPeer) ); }; }); tearDown(() async { await logSub.cancel(); restoreVmServiceConnectFunction(); }); test('connects to isolate paused at start', () async { final List connectionLog = []; when(mockPeer.sendRequest('streamListen', any)).thenAnswer((Invocation invocation) { connectionLog.add('streamListen'); return null; }); when(mockIsolate.pauseEvent).thenReturn(new MockVMPauseStartEvent()); when(mockIsolate.resume()).thenAnswer((Invocation invocation) { connectionLog.add('resume'); return new Future.value(); }); when(mockIsolate.onExtensionAdded).thenAnswer((Invocation invocation) { connectionLog.add('onExtensionAdded'); return new Stream.fromIterable(['ext.flutter.driver']); }); final FlutterDriver driver = await FlutterDriver.connect(dartVmServiceUrl: ''); expect(driver, isNotNull); expectLogContains('Isolate is paused at start'); expect(connectionLog, ['streamListen', 'onExtensionAdded', 'resume']); }); test('connects to isolate paused mid-flight', () async { when(mockIsolate.pauseEvent).thenReturn(new MockVMPauseBreakpointEvent()); when(mockIsolate.resume()).thenAnswer((Invocation invocation) => new Future.value()); 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(new MockVMPauseBreakpointEvent()); when(mockIsolate.resume()).thenAnswer((Invocation invocation) { // This needs to be wrapped in a closure to not be considered uncaught // by package:test return new Future.error(new 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(new 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 = new MockVMServiceClient(); mockPeer = new MockPeer(); mockIsolate = new MockIsolate(); driver = new FlutterDriver.connectedTo(mockClient, mockPeer, mockIsolate); }); test('checks the health of the driver extension', () async { when(mockIsolate.invokeExtension(any, any)).thenAnswer( (Invocation invocation) => makeMockResponse({'status': 'ok'})); final Health result = await driver.checkHealth(); expect(result.status, HealthStatus.ok); }); test('closes connection', () async { when(mockClient.close()).thenAnswer((Invocation invocation) => new Future.value()); await driver.close(); }); group('ByValueKey', () { test('restricts value types', () async { expect(() => find.byValueKey(null), throwsA(isInstanceOf())); }); test('finds by ValueKey', () async { when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) { expect(i.positionalArguments[1], { 'command': 'tap', 'timeout': _kSerializedTestTimeout, 'finderType': 'ByValueKey', 'keyValueString': 'foo', 'keyValueType': 'String' }); return makeMockResponse({}); }); await driver.tap(find.byValueKey('foo'), timeout: _kTestTimeout); }); }); group('tap', () { test('requires a target reference', () async { expect(driver.tap(null), throwsA(isInstanceOf())); }); test('sends the tap command', () async { when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) { expect(i.positionalArguments[1], { 'command': 'tap', 'timeout': _kSerializedTestTimeout, 'finderType': 'ByText', 'text': 'foo', }); return makeMockResponse({}); }); await driver.tap(find.text('foo'), timeout: _kTestTimeout); }); }); group('getText', () { test('requires a target reference', () async { expect(driver.getText(null), throwsA(isInstanceOf())); }); test('sends the getText command', () async { when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) { expect(i.positionalArguments[1], { 'command': 'get_text', 'timeout': _kSerializedTestTimeout, 'finderType': 'ByValueKey', 'keyValueString': '123', 'keyValueType': 'int' }); return makeMockResponse({ '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())); }); test('sends the waitFor command', () async { when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) { expect(i.positionalArguments[1], { 'command': 'waitFor', 'finderType': 'ByTooltipMessage', 'text': 'foo', 'timeout': _kSerializedTestTimeout, }); return makeMockResponse({}); }); 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], { 'command': 'waitUntilNoTransientCallbacks', 'timeout': _kSerializedTestTimeout, }); return makeMockResponse({}); }); await driver.waitUntilNoTransientCallbacks(timeout: _kTestTimeout); }); }); group('clearTimeline', () { test('clears timeline', () async { bool clearWasCalled = false; when(mockPeer.sendRequest('_clearVMTimeline', argThat(equals({})))) .thenAnswer((Invocation invocation) async { clearWasCalled = true; return null; }); await driver.clearTimeline(); expect(clearWasCalled, isTrue); }); }); group('traceAction', () { List log; setUp(() async { log = []; when(mockPeer.sendRequest('_clearVMTimeline', argThat(equals({})))) .thenAnswer((Invocation invocation) async { log.add('clear'); return null; }); when(mockPeer.sendRequest('_setVMTimelineFlags', argThat(equals({'recordedStreams': '[all]'})))) .thenAnswer((Invocation invocation) async { log.add('startTracing'); return null; }); when(mockPeer.sendRequest('_setVMTimelineFlags', argThat(equals({'recordedStreams': '[]'})))) .thenAnswer((Invocation invocation) async { log.add('stopTracing'); return null; }); when(mockPeer.sendRequest('_getVMTimeline')).thenAnswer((Invocation invocation) async { log.add('download'); return { 'traceEvents': [ { 'name': 'test event' } ], }; }); }); test('without clearing timeline', () async { final Timeline timeline = await driver.traceAction(() async { log.add('action'); }, retainPriorEvents: true); expect(log, const [ '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 [ '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({'recordedStreams': '[Dart, GC, Compiler]'})))) .thenAnswer((Invocation invocation) async { startTracingCalled = true; return null; }); when(mockPeer.sendRequest('_setVMTimelineFlags', argThat(equals({'recordedStreams': '[]'})))) .thenAnswer((Invocation invocation) async { stopTracingCalled = true; return null; }); when(mockPeer.sendRequest('_getVMTimeline')).thenAnswer((Invocation invocation) async { return { 'traceEvents': [ { 'name': 'test event' } ], }; }); final Timeline timeline = await driver.traceAction(() async { actionCalled = true; }, streams: const [ 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 { when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) { // completer never competed to trigger timeout return new Completer>().future; }); try { await driver.waitFor(find.byTooltip('foo'), timeout: const Duration(milliseconds: 100)); fail('expected an exception'); } catch (error) { expect(error is DriverError, isTrue); expect(error.message, 'Failed to fulfill WaitFor: Flutter application not responding'); } }); test('remote error', () async { when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) { return makeMockResponse({ '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}'); } }); }); }); } Future> makeMockResponse( Map response, {bool isError = false}) { return new Future>.value({ '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 { }