blob: 582efa74fcba5393055baaf562d31bdbab8d8025 [file] [log] [blame]
// Copyright 2014 The Flutter 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:io';
import 'package:matcher/matcher.dart';
import 'package:meta/meta.dart';
import 'package:vm_service_client/vm_service_client.dart';
import 'package:webdriver/sync_io.dart' as sync_io;
import 'package:webdriver/support/async.dart';
import '../common/error.dart';
import '../common/message.dart';
import 'driver.dart';
import 'timeline.dart';
/// An implementation of the Flutter Driver using the WebDriver.
///
/// Example of how to test WebFlutterDriver:
/// 1. Have Selenium server (https://bit.ly/2TlkRyu) and WebDriver binary (https://chromedriver.chromium.org/downloads) downloaded and placed under the same folder
/// 2. Launch WebDriver Server: java -jar selenium-server-standalone-3.141.59.jar
/// 3. Launch Flutter Web application: flutter run -v -d chrome --target=test_driver/scroll_perf_web.dart
/// 4. Run test script: flutter drive --target=test_driver/scroll_perf_web.dart -v --use-existing-app=/application address/
class WebFlutterDriver extends FlutterDriver {
/// Creates a driver that uses a connection provided by the given
/// [_connection].
WebFlutterDriver.connectedTo(this._connection) :
_startTime = DateTime.now();
final FlutterWebConnection _connection;
DateTime _startTime;
/// Start time for tracing
@visibleForTesting
DateTime get startTime => _startTime;
@override
VMIsolate get appIsolate => throw UnsupportedError('WebFlutterDriver does not support appIsolate');
@override
VMServiceClient get serviceClient => throw UnsupportedError('WebFlutterDriver does not support serviceClient');
/// Creates a driver that uses a connection provided by the given
/// [hostUrl] which would fallback to environment variable VM_SERVICE_URL.
/// Driver also depends on environment variables DRIVER_SESSION_ID,
/// BROWSER_SUPPORTS_TIMELINE, DRIVER_SESSION_URI, DRIVER_SESSION_SPEC
/// and DRIVER_SESSION_CAPABILITIES for configurations.
static Future<FlutterDriver> connectWeb(
{String hostUrl, Duration timeout}) async {
hostUrl ??= Platform.environment['VM_SERVICE_URL'];
final Map<String, dynamic> settings = <String, dynamic>{
'support-timeline-action': Platform.environment['SUPPORT_TIMELINE_ACTION'] == 'true',
'session-id': Platform.environment['DRIVER_SESSION_ID'],
'session-uri': Platform.environment['DRIVER_SESSION_URI'],
'session-spec': Platform.environment['DRIVER_SESSION_SPEC'],
'session-capabilities': Platform.environment['DRIVER_SESSION_CAPABILITIES'],
};
final FlutterWebConnection connection = await FlutterWebConnection.connect
(hostUrl, settings, timeout: timeout);
return WebFlutterDriver.connectedTo(connection);
}
@override
Future<Map<String, dynamic>> sendCommand(Command command) async {
Map<String, dynamic> response;
final Map<String, String> serialized = command.serialize();
try {
final dynamic data = await _connection.sendCommand('window.\$flutterDriver(\'${jsonEncode(serialized)}\')', command.timeout);
response = data != null ? json.decode(data as String) as Map<String, dynamic> : <String, dynamic>{};
} catch (error, stackTrace) {
throw DriverError('Failed to respond to $command due to remote error\n : \$flutterDriver(\'${jsonEncode(serialized)}\')',
error,
stackTrace
);
}
if (response['isError'] == true)
throw DriverError('Error in Flutter application: ${response['response']}');
return response['response'] as Map<String, dynamic>;
}
@override
Future<void> close() => _connection.close();
@override
Future<void> waitUntilFirstFrameRasterized() async {
throw UnimplementedError();
}
@override
Future<List<int>> screenshot() async {
await Future<void>.delayed(const Duration(seconds: 2));
return _connection.screenshot();
}
@override
Future<void> startTracing({
List<TimelineStream> streams = const <TimelineStream>[TimelineStream.all],
Duration timeout = kUnusuallyLongTimeout,
}) async {
_checkBrowserSupportsTimeline();
}
@override
Future<Timeline> stopTracingAndDownloadTimeline({Duration timeout = kUnusuallyLongTimeout}) async {
_checkBrowserSupportsTimeline();
final List<Map<String, dynamic>> events = <Map<String, dynamic>>[];
for (final sync_io.LogEntry entry in _connection.logs) {
if (_startTime.isBefore(entry.timestamp)) {
final Map<String, dynamic> data = jsonDecode(entry.message)['message'] as Map<String, dynamic>;
if (data['method'] == 'Tracing.dataCollected') {
// 'ts' data collected from Chrome is in double format, conversion needed
try {
data['params']['ts'] =
double.parse(data['params']['ts'].toString()).toInt();
} on FormatException catch (_) {
// data is corrupted, skip
continue;
}
events.add(data['params'] as Map<String, dynamic>);
}
}
}
final Map<String, dynamic> json = <String, dynamic>{
'traceEvents': events,
};
return Timeline.fromJson(json);
}
@override
Future<Timeline> traceAction(Future<dynamic> Function() action, {
List<TimelineStream> streams = const <TimelineStream>[TimelineStream.all],
bool retainPriorEvents = false,
}) async {
_checkBrowserSupportsTimeline();
if (!retainPriorEvents) {
await clearTimeline();
}
await startTracing(streams: streams);
await action();
return stopTracingAndDownloadTimeline();
}
@override
Future<void> clearTimeline({Duration timeout = kUnusuallyLongTimeout}) async {
_checkBrowserSupportsTimeline();
// Reset start time
_startTime = DateTime.now();
}
/// Checks whether browser supports Timeline related operations
void _checkBrowserSupportsTimeline() {
if (_connection.supportsTimelineAction) {
throw UnsupportedError('Timeline action is not supported by current testing browser');
}
}
}
/// Encapsulates connection information to an instance of a Flutter Web application.
class FlutterWebConnection {
/// Creates a FlutterWebConnection with WebDriver
/// and whether the WebDriver supports timeline action
FlutterWebConnection(this._driver, this._supportsTimelineAction);
final sync_io.WebDriver _driver;
bool _supportsTimelineAction;
/// Whether the connected WebDriver supports timeline action for Flutter Web Driver
// ignore: unnecessary_getters_setters
bool get supportsTimelineAction => _supportsTimelineAction;
/// Setter for _supportsTimelineAction
@visibleForTesting
// ignore: unnecessary_getters_setters
set supportsTimelineAction(bool value) {
_supportsTimelineAction = value;
}
/// Starts WebDriver with the given [capabilities] and
/// establishes the connection to Flutter Web application.
static Future<FlutterWebConnection> connect(
String url,
Map<String, dynamic> settings,
{Duration timeout}) async {
// Use sync WebDriver because async version will create a 15 seconds
// overhead when quitting.
final sync_io.WebDriver driver = sync_io.fromExistingSession(
settings['session-id'].toString(),
uri: Uri.parse(settings['session-uri'].toString()),
spec: _convertToSpec(settings['session-spec'].toString().toLowerCase()),
capabilities: jsonDecode(settings['session-capabilities'].toString()) as Map<String, dynamic>);
driver.get(url);
await waitUntilExtensionInstalled(driver, timeout);
return FlutterWebConnection(driver, settings['support-timeline-action'] as bool);
}
/// Sends command via WebDriver to Flutter web application
Future<dynamic> sendCommand(String script, Duration duration) async {
dynamic result;
try {
_driver.execute(script, <void>[]);
} catch (_) {
// In case there is an exception, do nothing
}
try {
result = await waitFor<dynamic>(
() => _driver.execute(r'return $flutterDriverResult', <String>[]),
matcher: isNotNull,
timeout: duration ?? const Duration(days: 30),
);
} catch (_) {
// Returns null if exception thrown.
return null;
} finally {
// Resets the result.
_driver.execute(r'''
$flutterDriverResult = null
''', <void>[]);
}
return result;
}
/// Gets performance log from WebDriver.
List<sync_io.LogEntry> get logs => _driver.logs.get(sync_io.LogType.performance);
/// Takes screenshot via WebDriver.
List<int> screenshot() => _driver.captureScreenshotAsList();
/// Closes the WebDriver.
Future<void> close() async {
_driver.quit(closeSession: false);
}
}
/// Waits until extension is installed.
Future<void> waitUntilExtensionInstalled(sync_io.WebDriver driver, Duration timeout) async {
await waitFor<void>(() =>
driver.execute(r'return typeof(window.$flutterDriver)', <String>[]),
matcher: 'function',
timeout: timeout ?? const Duration(days: 365));
}
sync_io.WebDriverSpec _convertToSpec(String specString) {
switch (specString.toLowerCase()) {
case 'webdriverspec.w3c':
return sync_io.WebDriverSpec.W3c;
case 'webdriverspec.jsonwire':
return sync_io.WebDriverSpec.JsonWire;
default:
return sync_io.WebDriverSpec.Auto;
}
}