mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
350 lines
11 KiB
Dart
350 lines
11 KiB
Dart
// 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:math' as math;
|
|
|
|
import 'package:file/file.dart';
|
|
import 'package:meta/meta.dart';
|
|
import 'package:package_config/package_config.dart';
|
|
import 'package:webdriver/async_io.dart' as async_io;
|
|
|
|
import '../base/common.dart';
|
|
import '../base/io.dart';
|
|
import '../base/logger.dart';
|
|
import '../base/process.dart';
|
|
import '../build_info.dart';
|
|
import '../convert.dart';
|
|
import '../device.dart';
|
|
import '../globals.dart' as globals;
|
|
import '../project.dart';
|
|
import '../resident_runner.dart';
|
|
import '../web/web_runner.dart';
|
|
import 'drive_service.dart';
|
|
|
|
/// An implementation of the driver service for web debug and release applications.
|
|
class WebDriverService extends DriverService {
|
|
WebDriverService({
|
|
required ProcessUtils processUtils,
|
|
required String dartSdkPath,
|
|
required Logger logger,
|
|
}) : _processUtils = processUtils,
|
|
_dartSdkPath = dartSdkPath,
|
|
_logger = logger;
|
|
|
|
final ProcessUtils _processUtils;
|
|
final String _dartSdkPath;
|
|
final Logger _logger;
|
|
|
|
late ResidentRunner _residentRunner;
|
|
Uri? _webUri;
|
|
|
|
/// The result of [ResidentRunner.run].
|
|
///
|
|
/// This is expected to stay `null` throughout the test, as the application
|
|
/// must be running until [stop] is called. If it becomes non-null, it likely
|
|
/// indicates a bug.
|
|
int? _runResult;
|
|
|
|
@override
|
|
Future<void> start(
|
|
BuildInfo buildInfo,
|
|
Device device,
|
|
DebuggingOptions debuggingOptions,
|
|
bool ipv6, {
|
|
File? applicationBinary,
|
|
String? route,
|
|
String? userIdentifier,
|
|
String? mainPath,
|
|
Map<String, Object> platformArgs = const <String, Object>{},
|
|
}) async {
|
|
final FlutterDevice flutterDevice = await FlutterDevice.create(
|
|
device,
|
|
target: mainPath,
|
|
buildInfo: buildInfo,
|
|
platform: globals.platform,
|
|
);
|
|
_residentRunner = webRunnerFactory!.createWebRunner(
|
|
flutterDevice,
|
|
target: mainPath,
|
|
ipv6: ipv6,
|
|
debuggingOptions: buildInfo.isRelease ?
|
|
DebuggingOptions.disabled(
|
|
buildInfo,
|
|
port: debuggingOptions.port,
|
|
)
|
|
: DebuggingOptions.enabled(
|
|
buildInfo,
|
|
port: debuggingOptions.port,
|
|
disablePortPublication: debuggingOptions.disablePortPublication,
|
|
),
|
|
stayResident: true,
|
|
urlTunneller: null,
|
|
flutterProject: FlutterProject.current(),
|
|
fileSystem: globals.fs,
|
|
usage: globals.flutterUsage,
|
|
logger: _logger,
|
|
systemClock: globals.systemClock,
|
|
);
|
|
final Completer<void> appStartedCompleter = Completer<void>.sync();
|
|
final Future<int?> runFuture = _residentRunner.run(
|
|
appStartedCompleter: appStartedCompleter,
|
|
route: route,
|
|
);
|
|
|
|
bool isAppStarted = false;
|
|
await Future.any<Object>(<Future<Object>>[
|
|
runFuture.then((int? result) {
|
|
_runResult = result;
|
|
return null;
|
|
} as FutureOr<Object> Function(int?)),
|
|
appStartedCompleter.future.then((_) {
|
|
isAppStarted = true;
|
|
return null;
|
|
} as FutureOr<Object> Function(void)),
|
|
]);
|
|
|
|
if (_runResult != null) {
|
|
throw ToolExit(
|
|
'Application exited before the test started. Check web driver logs '
|
|
'for possible application-side errors.'
|
|
);
|
|
}
|
|
|
|
if (!isAppStarted) {
|
|
throw ToolExit('Failed to start application');
|
|
}
|
|
|
|
_webUri = _residentRunner.uri;
|
|
|
|
if (_webUri == null) {
|
|
throw ToolExit('Unable to connect to the app. URL not available.');
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<int> startTest(
|
|
String testFile,
|
|
List<String> arguments,
|
|
Map<String, String> environment,
|
|
PackageConfig packageConfig, {
|
|
bool? headless,
|
|
String? chromeBinary,
|
|
String? browserName,
|
|
bool? androidEmulator,
|
|
int? driverPort,
|
|
List<String> webBrowserFlags = const <String>[],
|
|
List<String>? browserDimension,
|
|
String? profileMemory,
|
|
}) async {
|
|
late async_io.WebDriver webDriver;
|
|
final Browser browser = _browserNameToEnum(browserName);
|
|
try {
|
|
webDriver = await async_io.createDriver(
|
|
uri: Uri.parse('http://localhost:$driverPort/'),
|
|
desired: getDesiredCapabilities(
|
|
browser,
|
|
headless,
|
|
webBrowserFlags: webBrowserFlags,
|
|
chromeBinary: chromeBinary,
|
|
),
|
|
);
|
|
} on SocketException catch (error) {
|
|
_logger.printTrace('$error');
|
|
throwToolExit(
|
|
'Unable to start a WebDriver session for web testing.\n'
|
|
'Make sure you have the correct WebDriver server (e.g. chromedriver) running at $driverPort.\n'
|
|
'For instructions on how to obtain and run a WebDriver server, see:\n'
|
|
'https://flutter.dev/docs/testing/integration-tests#running-in-a-browser\n'
|
|
);
|
|
}
|
|
|
|
final bool isAndroidChrome = browser == Browser.androidChrome;
|
|
// Do not set the window size for android chrome browser.
|
|
if (!isAndroidChrome) {
|
|
assert(browserDimension!.length == 2);
|
|
late int x;
|
|
late int y;
|
|
try {
|
|
x = int.parse(browserDimension![0]);
|
|
y = int.parse(browserDimension[1]);
|
|
} on FormatException catch (ex) {
|
|
throwToolExit('Dimension provided to --browser-dimension is invalid: $ex');
|
|
}
|
|
final async_io.Window window = await webDriver.window;
|
|
await window.setLocation(const math.Point<int>(0, 0));
|
|
await window.setSize(math.Rectangle<int>(0, 0, x, y));
|
|
}
|
|
final int result = await _processUtils.stream(<String>[
|
|
_dartSdkPath,
|
|
...arguments,
|
|
testFile,
|
|
'-rexpanded',
|
|
], environment: <String, String>{
|
|
'VM_SERVICE_URL': _webUri.toString(),
|
|
..._additionalDriverEnvironment(webDriver, browserName, androidEmulator),
|
|
...environment,
|
|
});
|
|
await webDriver.quit();
|
|
return result;
|
|
}
|
|
|
|
@override
|
|
Future<void> stop({File? writeSkslOnExit, String? userIdentifier}) async {
|
|
final bool appDidFinishPrematurely = _runResult != null;
|
|
await _residentRunner.exitApp();
|
|
await _residentRunner.cleanupAtFinish();
|
|
|
|
if (appDidFinishPrematurely) {
|
|
throw ToolExit(
|
|
'Application exited before the test finished. Check web driver logs '
|
|
'for possible application-side errors.'
|
|
);
|
|
}
|
|
}
|
|
|
|
Map<String, String> _additionalDriverEnvironment(async_io.WebDriver webDriver, String? browserName, bool? androidEmulator) {
|
|
return <String, String>{
|
|
'DRIVER_SESSION_ID': webDriver.id,
|
|
'DRIVER_SESSION_URI': webDriver.uri.toString(),
|
|
'DRIVER_SESSION_SPEC': webDriver.spec.toString(),
|
|
'DRIVER_SESSION_CAPABILITIES': json.encode(webDriver.capabilities),
|
|
'SUPPORT_TIMELINE_ACTION': (_browserNameToEnum(browserName) == Browser.chrome).toString(),
|
|
'FLUTTER_WEB_TEST': 'true',
|
|
'ANDROID_CHROME_ON_EMULATOR': (_browserNameToEnum(browserName) == Browser.androidChrome && androidEmulator!).toString(),
|
|
};
|
|
}
|
|
|
|
@override
|
|
Future<void> reuseApplication(Uri vmServiceUri, Device device, DebuggingOptions debuggingOptions, bool ipv6) async {
|
|
throwToolExit('--use-existing-app is not supported with flutter web driver');
|
|
}
|
|
}
|
|
|
|
/// A list of supported browsers.
|
|
enum Browser {
|
|
/// Chrome on Android: https://developer.chrome.com/multidevice/android/overview
|
|
androidChrome,
|
|
/// Chrome: https://www.google.com/chrome/
|
|
chrome,
|
|
/// Edge: https://www.microsoft.com/en-us/windows/microsoft-edge
|
|
edge,
|
|
/// Firefox: https://www.mozilla.org/en-US/firefox/
|
|
firefox,
|
|
/// Safari in iOS: https://www.apple.com/safari/
|
|
iosSafari,
|
|
/// Safari in macOS: https://www.apple.com/safari/
|
|
safari,
|
|
}
|
|
|
|
/// Returns desired capabilities for given [browser], [headless], [chromeBinary]
|
|
/// and [webBrowserFlags].
|
|
@visibleForTesting
|
|
Map<String, dynamic> getDesiredCapabilities(
|
|
Browser browser,
|
|
bool? headless, {
|
|
List<String> webBrowserFlags = const <String>[],
|
|
String? chromeBinary,
|
|
}) {
|
|
switch (browser) {
|
|
case Browser.chrome:
|
|
return <String, dynamic>{
|
|
'acceptInsecureCerts': true,
|
|
'browserName': 'chrome',
|
|
'goog:loggingPrefs': <String, String>{
|
|
async_io.LogType.browser: 'INFO',
|
|
async_io.LogType.performance: 'ALL',
|
|
},
|
|
'chromeOptions': <String, dynamic>{
|
|
if (chromeBinary != null)
|
|
'binary': chromeBinary,
|
|
'w3c': false,
|
|
'args': <String>[
|
|
'--bwsi',
|
|
'--disable-background-timer-throttling',
|
|
'--disable-default-apps',
|
|
'--disable-extensions',
|
|
'--disable-popup-blocking',
|
|
'--disable-translate',
|
|
'--no-default-browser-check',
|
|
'--no-sandbox',
|
|
'--no-first-run',
|
|
if (headless!) '--headless',
|
|
...webBrowserFlags,
|
|
],
|
|
'perfLoggingPrefs': <String, String>{
|
|
'traceCategories':
|
|
'devtools.timeline,'
|
|
'v8,blink.console,benchmark,blink,'
|
|
'blink.user_timing',
|
|
},
|
|
},
|
|
};
|
|
case Browser.firefox:
|
|
return <String, dynamic>{
|
|
'acceptInsecureCerts': true,
|
|
'browserName': 'firefox',
|
|
'moz:firefoxOptions' : <String, dynamic>{
|
|
'args': <String>[
|
|
if (headless!) '-headless',
|
|
...webBrowserFlags,
|
|
],
|
|
'prefs': <String, dynamic>{
|
|
'dom.file.createInChild': true,
|
|
'dom.timeout.background_throttling_max_budget': -1,
|
|
'media.autoplay.default': 0,
|
|
'media.gmp-manager.url': '',
|
|
'media.gmp-provider.enabled': false,
|
|
'network.captive-portal-service.enabled': false,
|
|
'security.insecure_field_warning.contextual.enabled': false,
|
|
'test.currentTimeOffsetSeconds': 11491200,
|
|
},
|
|
'log': <String, String>{'level': 'trace'},
|
|
},
|
|
};
|
|
case Browser.edge:
|
|
return <String, dynamic>{
|
|
'acceptInsecureCerts': true,
|
|
'browserName': 'edge',
|
|
};
|
|
case Browser.safari:
|
|
return <String, dynamic>{
|
|
'browserName': 'safari',
|
|
};
|
|
case Browser.iosSafari:
|
|
return <String, dynamic>{
|
|
'platformName': 'ios',
|
|
'browserName': 'safari',
|
|
'safari:useSimulator': true,
|
|
};
|
|
case Browser.androidChrome:
|
|
return <String, dynamic>{
|
|
'browserName': 'chrome',
|
|
'platformName': 'android',
|
|
'goog:chromeOptions': <String, dynamic>{
|
|
'androidPackage': 'com.android.chrome',
|
|
'args': <String>[
|
|
'--disable-fullscreen',
|
|
...webBrowserFlags,
|
|
],
|
|
},
|
|
};
|
|
}
|
|
}
|
|
|
|
/// Converts [browserName] string to [Browser]
|
|
Browser _browserNameToEnum(String? browserName) {
|
|
switch (browserName) {
|
|
case 'android-chrome': return Browser.androidChrome;
|
|
case 'chrome': return Browser.chrome;
|
|
case 'edge': return Browser.edge;
|
|
case 'firefox': return Browser.firefox;
|
|
case 'ios-safari': return Browser.iosSafari;
|
|
case 'safari': return Browser.safari;
|
|
}
|
|
throw UnsupportedError('Browser $browserName not supported');
|
|
}
|