blob: 3927cbc397b50542faa76af31764270b4d44b25c [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:convert';
import 'dart:io';
import 'package:file/file.dart';
import 'package:matcher/matcher.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;
import 'package:vm_service/vm_service.dart' as vms;
import 'package:webdriver/async_io.dart' as async_io;
import 'package:webdriver/support/async.dart';
import '../common/error.dart';
import '../common/message.dart';
import 'common.dart';
import 'driver.dart';
import 'timeline.dart';
/// An implementation of the Flutter Driver using the WebDriver.
///
/// Example of how to test WebFlutterDriver:
/// 1. Launch WebDriver binary: ./chromedriver --port=4444
/// 2. Run test script: flutter drive --target=test_driver/scroll_perf_web.dart -d web-server --release
class WebFlutterDriver extends FlutterDriver {
/// Creates a driver that uses a connection provided by the given
/// [_connection].
WebFlutterDriver.connectedTo(
this._connection, {
bool printCommunication = false,
bool logCommunicationToFile = true,
}) : _printCommunication = printCommunication,
_logCommunicationToFile = logCommunicationToFile,
_startTime = DateTime.now(),
_driverId = _nextDriverId++
{
_logFilePathName = path.join(testOutputsDirectory, 'flutter_driver_commands_$_driverId.log');
}
final FlutterWebConnection _connection;
DateTime _startTime;
static int _nextDriverId = 0;
/// The unique ID of this driver instance.
final int _driverId;
/// Start time for tracing.
@visibleForTesting
DateTime get startTime => _startTime;
@override
vms.Isolate get appIsolate => throw UnsupportedError('WebFlutterDriver does not support appIsolate');
@override
vms.VmService get serviceClient => throw UnsupportedError('WebFlutterDriver does not support serviceClient');
@override
async_io.WebDriver get webDriver => _connection._driver;
/// Whether to print communication between host and app to `stdout`.
final bool _printCommunication;
/// Whether to log communication between host and app to `flutter_driver_commands.log`.
final bool _logCommunicationToFile;
/// Logs are written here when _logCommunicationToFile is true.
late final String _logFilePathName;
/// Getter for file pathname where logs are written when _logCommunicationToFile is true
String get logFilePathName => _logFilePathName;
/// 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,
/// DRIVER_SESSION_CAPABILITIES and ANDROID_CHROME_ON_EMULATOR for
/// configurations.
///
/// See [FlutterDriver.connect] for more documentation.
static Future<FlutterDriver> connectWeb({
String? hostUrl,
bool printCommunication = false,
bool logCommunicationToFile = true,
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'],
'android-chrome-on-emulator': Platform.environment['ANDROID_CHROME_ON_EMULATOR'] == 'true',
'session-capabilities': Platform.environment['DRIVER_SESSION_CAPABILITIES'],
};
final FlutterWebConnection connection = await FlutterWebConnection.connect
(hostUrl!, settings, timeout: timeout);
return WebFlutterDriver.connectedTo(
connection,
printCommunication: printCommunication,
logCommunicationToFile: logCommunicationToFile,
);
}
static DriverError _createMalformedExtensionResponseError(Object? data) {
throw DriverError(
'Received malformed response from the FlutterDriver extension.\n'
'Expected a JSON map containing a "response" field and, optionally, an '
'"isError" field, but got ${data.runtimeType}: $data'
);
}
@override
Future<Map<String, dynamic>> sendCommand(Command command) async {
final Map<String, dynamic> response;
final Object? data;
final Map<String, String> serialized = command.serialize();
_logCommunication('>>> $serialized');
try {
data = await _connection.sendCommand("window.\$flutterDriver('${jsonEncode(serialized)}')", command.timeout);
// The returned data is expected to be a string. If it's null or anything
// other than a string, something's wrong.
if (data is! String) {
throw _createMalformedExtensionResponseError(data);
}
final Object? decoded = json.decode(data);
if (decoded is! Map<String, dynamic>) {
throw _createMalformedExtensionResponseError(data);
} else {
response = decoded;
}
_logCommunication('<<< $response');
} on DriverError catch(_) {
rethrow;
} catch (error, stackTrace) {
throw DriverError(
'FlutterDriver command ${command.runtimeType} failed due to a remote error.\n'
'Command sent: ${jsonEncode(serialized)}',
error,
stackTrace
);
}
final Object? isError = response['isError'];
final Object? responseData = response['response'];
if (isError is! bool?) {
throw _createMalformedExtensionResponseError(data);
} else if (isError ?? false) {
throw DriverError('Error in Flutter application: $responseData');
}
if (responseData is! Map<String, dynamic>) {
throw _createMalformedExtensionResponseError(data);
}
return responseData;
}
@override
Future<void> close() => _connection.close();
@override
Future<void> waitUntilFirstFrameRasterized() async {
throw UnimplementedError();
}
void _logCommunication(String message) {
if (_printCommunication) {
driverLog('WebFlutterDriver', message);
}
if (_logCommunicationToFile) {
assert(_logFilePathName != null);
final File file = fs.file(_logFilePathName);
file.createSync(recursive: true); // no-op if file exists
file.writeAsStringSync('${DateTime.now()} $message\n', mode: FileMode.append, flush: true);
}
}
@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 async_io.LogEntry entry in await _connection.logs.toList()) {
if (_startTime.isBefore(entry.timestamp)) {
final Map<String, dynamic> data = (jsonDecode(entry.message!) as Map<String, dynamic>)['message'] as Map<String, dynamic>;
if (data['method'] == 'Tracing.dataCollected') {
// 'ts' data collected from Chrome is in double format, conversion needed
try {
final Map<String, dynamic> params = data['params'] as Map<String, dynamic>;
params['ts'] = double.parse(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 async_io.WebDriver _driver;
/// Whether the connected WebDriver supports timeline action for Flutter Web Driver.
bool supportsTimelineAction;
/// Starts WebDriver with the given [settings] and
/// establishes the connection to Flutter Web application.
static Future<FlutterWebConnection> connect(
String url,
Map<String, dynamic> settings,
{Duration? timeout}) async {
final String sessionId = settings['session-id'].toString();
final Uri sessionUri = Uri.parse(settings['session-uri'].toString());
final async_io.WebDriver driver = async_io.WebDriver(
sessionUri,
sessionId,
json.decode(settings['session-capabilities'] as String) as Map<String, dynamic>,
async_io.AsyncIoRequestClient(sessionUri.resolve('session/$sessionId/')),
_convertToSpec(settings['session-spec'].toString().toLowerCase()));
if (settings['android-chrome-on-emulator'] == true) {
final Uri localUri = Uri.parse(url);
// Converts to Android Emulator Uri.
// Hardcode the host to 10.0.2.2 based on
// https://developer.android.com/studio/run/emulator-networking
url = Uri(scheme: localUri.scheme, host: '10.0.2.2', port:localUri.port).toString();
}
await 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 {
// This code should not be reachable before the VM service extension is
// initialized. The VM service extension is expected to initialize both
// `$flutterDriverResult` and `$flutterDriver` variables before attempting
// to send commands. This part checks that `$flutterDriverResult` is present.
// `$flutterDriver` is not checked because it is covered by the `script`
// that's executed next.
try {
await _driver.execute(r'return $flutterDriverResult', <String>[]);
} catch (error, stackTrace) {
throw DriverError(
'Driver extension has not been initialized correctly.\n'
'If the test uses a custom VM service extension, make sure it conforms '
'to the protocol used by package:integration_test and '
'package:flutter_driver.\n'
'If the test uses VM service extensions provided by the Flutter SDK, '
'then this error is likely caused by a bug in Flutter. Please report it '
'by filing a bug on GitHub:\n'
' https://github.com/flutter/flutter/issues/new?template=2_bug.md',
error,
stackTrace,
);
}
String phase = 'executing';
try {
// Execute the script, which should leave the result in the `$flutterDriverResult` global variable.
await _driver.execute(script, <void>[]);
// Read the result.
phase = 'reading';
final dynamic result = await waitFor<dynamic>(
() => _driver.execute(r'return $flutterDriverResult', <String>[]),
matcher: isNotNull,
timeout: duration ?? const Duration(days: 30),
);
// Reset the result to null to avoid polluting the results of future commands.
phase = 'resetting';
await _driver.execute(r'$flutterDriverResult = null', <void>[]);
return result;
} catch (error, stackTrace) {
throw DriverError(
'Error while $phase FlutterDriver result for command: $script',
error,
stackTrace,
);
}
}
/// Gets performance log from WebDriver.
Stream<async_io.LogEntry> get logs => _driver.logs.get(async_io.LogType.performance);
/// Takes screenshot via WebDriver.
Future<List<int>> screenshot() => _driver.captureScreenshotAsList();
/// Closes the WebDriver.
Future<void> close() async {
await _driver.quit(closeSession: false);
}
}
/// Waits until extension is installed.
Future<void> waitUntilExtensionInstalled(async_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));
}
async_io.WebDriverSpec _convertToSpec(String specString) {
switch (specString.toLowerCase()) {
case 'webdriverspec.w3c':
return async_io.WebDriverSpec.W3c;
case 'webdriverspec.jsonwire':
return async_io.WebDriverSpec.JsonWire;
default:
return async_io.WebDriverSpec.Auto;
}
}