[web] replace browser-related conditional logic with BrowserEnvironment (flutter/engine#27084)

* [web] replace browser-related conditional logic with BrowserEnvironment
This commit is contained in:
Yegor 2021-07-02 12:04:45 -07:00 committed by GitHub
parent 203d76f26f
commit 2abe536ed8
10 changed files with 424 additions and 330 deletions

View File

@ -5,11 +5,50 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math' as math;
import 'package:image/image.dart';
import 'package:pedantic/pedantic.dart';
import 'package:stack_trace/stack_trace.dart';
import 'package:test_api/src/backend/runtime.dart';
import 'package:typed_data/typed_buffers.dart';
/// Provides the environment for a specific web browser.
abstract class BrowserEnvironment {
/// The [Runtime] used by `package:test` to identify this browser type.
Runtime get packageTestRuntime;
/// The name of the configuration YAML file used to configure `package:test`.
///
/// The configuration file is expected to be a direct child of the `web_ui`
/// directory.
String get packageTestConfigurationYamlFile;
/// Prepares the OS environment to run tests for this browser.
///
/// This may include things like staring web drivers, iOS Simulators, and/or
/// Android emulators.
///
/// Typically the browser environment is prepared once and supports multiple
/// browser instances.
Future<void> prepareEnvironment();
/// Launches a browser instance.
///
/// The browser will be directed to open the provided [url].
///
/// If [debug] is true and the browser supports debugging, launches the
/// browser in debug mode by pausing test execution after the code is loaded
/// but before calling the `main()` function of the test, giving the
/// developer a chance to set breakpoints.
Browser launchBrowserInstance(Uri url, {bool debug = false});
/// Returns the screenshot manager used by this browser.
///
/// If the browser does not support screenshots, returns null.
ScreenshotManager? getScreenshotManager();
}
/// An interface for running browser instances.
///
/// This is intentionally coarse-grained: browsers are controlled primary from
@ -147,3 +186,18 @@ abstract class Browser {
return onExit.catchError((dynamic _) {});
}
}
/// Interface for capturing screenshots from a browser.
abstract class ScreenshotManager {
/// Capture a screenshot.
///
/// Please read the details for the implementing classes.
Future<Image> capture(math.Rectangle region);
/// Suffix to be added to the end of the filename.
///
/// Example file names:
/// - Chrome, no-suffix: backdrop_filter_clip_moved.actual.png
/// - iOS Safari: backdrop_filter_clip_moved.iOS_Safari.actual.png
String get filenameSuffix;
}

View File

@ -5,15 +5,44 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math' as math;
import 'package:image/image.dart';
import 'package:pedantic/pedantic.dart';
import 'package:test_api/src/backend/runtime.dart';
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'
as wip;
import 'browser.dart';
import 'chrome_installer.dart';
import 'common.dart';
import 'environment.dart';
/// A class for running an instance of Chrome.
/// Provides an environment for desktop Chrome.
class ChromeEnvironment implements BrowserEnvironment {
@override
Browser launchBrowserInstance(Uri url, {bool debug = false}) {
return Chrome(url, debug: debug);
}
@override
Runtime get packageTestRuntime => Runtime.chrome;
@override
Future<void> prepareEnvironment() async {
// Chrome doesn't need any special prep.
}
@override
ScreenshotManager? getScreenshotManager() {
return ChromeScreenshotManager();
}
@override
String get packageTestConfigurationYamlFile => 'dart_test_chrome.yaml';
}
/// Runs desktop Chrome.
///
/// Most of the communication with the browser is expected to happen via HTTP,
/// so this exposes a bare-bones API. The browser starts as soon as the class is
@ -174,3 +203,64 @@ Future<Uri> getRemoteDebuggerUrl(Uri base) async {
return base;
}
}
/// [ScreenshotManager] implementation for Chrome.
///
/// This manager can be used for both macOS and Linux.
// TODO: https://github.com/flutter/flutter/issues/65673
class ChromeScreenshotManager extends ScreenshotManager {
String get filenameSuffix => '';
/// Capture a screenshot of the web content.
///
/// Uses Webkit Inspection Protocol server's `captureScreenshot` API.
///
/// [region] is used to decide which part of the web content will be used in
/// test image. It includes starting coordinate x,y as well as height and
/// width of the area to capture.
Future<Image> capture(math.Rectangle? region) async {
final wip.ChromeConnection chromeConnection =
wip.ChromeConnection('localhost', kDevtoolsPort);
final wip.ChromeTab? chromeTab = await chromeConnection.getTab(
(wip.ChromeTab chromeTab) => chromeTab.url.contains('localhost'));
if (chromeTab == null) {
throw StateError(
'Failed locate Chrome tab with the test page',
);
}
final wip.WipConnection wipConnection = await chromeTab.connect();
Map<String, dynamic>? captureScreenshotParameters = null;
if (region != null) {
captureScreenshotParameters = <String, dynamic>{
'format': 'png',
'clip': <String, dynamic>{
'x': region.left,
'y': region.top,
'width': region.width,
'height': region.height,
'scale':
// This is NOT the DPI of the page, instead it's the "zoom level".
1,
},
};
}
// Setting hardware-independent screen parameters:
// https://chromedevtools.github.io/devtools-protocol/tot/Emulation
await wipConnection
.sendCommand('Emulation.setDeviceMetricsOverride', <String, dynamic>{
'width': kMaxScreenshotWidth,
'height': kMaxScreenshotHeight,
'deviceScaleFactor': 1,
'mobile': false,
});
final wip.WipResponse response = await wipConnection.sendCommand(
'Page.captureScreenshot', captureScreenshotParameters);
final Image screenshot =
decodePng(base64.decode(response.result!['data'] as String))!;
return screenshot;
}
}

View File

@ -5,11 +5,35 @@
import 'dart:async';
import 'dart:io';
import 'package:test_api/src/backend/runtime.dart';
import 'browser.dart';
import 'common.dart';
import 'edge_installation.dart';
/// A class for running an instance of Edge.
/// Provides an environment for the desktop Microsoft Edge (Chromium-based).
class EdgeEnvironment implements BrowserEnvironment {
@override
Browser launchBrowserInstance(Uri url, {bool debug = false}) {
return Edge(url, debug: debug);
}
@override
Runtime get packageTestRuntime => Runtime.internetExplorer;
@override
Future<void> prepareEnvironment() async {
// Edge doesn't need any special prep.
}
@override
ScreenshotManager? getScreenshotManager() => null;
@override
String get packageTestConfigurationYamlFile => 'dart_test_edge.yaml';
}
/// Runs desktop Edge.
///
/// Most of the communication with the browser is expected to happen via HTTP,
/// so this exposes a bare-bones API. The browser starts as soon as the class is

View File

@ -8,6 +8,7 @@ import 'dart:io';
import 'package:pedantic/pedantic.dart';
import 'package:path/path.dart' as path;
import 'package:test_api/src/backend/runtime.dart';
import 'package:test_core/src/util/io.dart';
import 'browser.dart';
@ -15,7 +16,29 @@ import 'common.dart';
import 'environment.dart';
import 'firefox_installer.dart';
/// A class for running an instance of Firefox.
/// Provides an environment for the desktop Firefox.
class FirefoxEnvironment implements BrowserEnvironment {
@override
Browser launchBrowserInstance(Uri url, {bool debug = false}) {
return Firefox(url, debug: debug);
}
@override
Runtime get packageTestRuntime => Runtime.firefox;
@override
Future<void> prepareEnvironment() async {
// Firefox doesn't need any special prep.
}
@override
String get packageTestConfigurationYamlFile => 'dart_test_firefox.yaml';
@override
ScreenshotManager? getScreenshotManager() => null;
}
/// Runs desktop Firefox.
///
/// Most of the communication with the browser is expected to happen via HTTP,
/// so this exposes a bare-bones API. The browser starts as soon as the class is

View File

@ -1,74 +0,0 @@
// Copyright 2013 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:io';
import 'browser.dart';
import 'safari_installation.dart';
import 'common.dart';
/// A class for running an instance of Safari.
///
/// Most of the communication with the browser is expected to happen via HTTP,
/// so this exposes a bare-bones API. The browser starts as soon as the class is
/// constructed, and is killed when [close] is called.
///
/// Any errors starting or running the process are reported through [onExit].
class Safari extends Browser {
@override
final name = 'Safari';
/// Starts a new instance of Safari open to the given [url], which may be a
/// [Uri] or a [String].
factory Safari(Uri url, {bool debug = false}) {
final String version = SafariArgParser.instance.version;
final bool isMobileBrowser = SafariArgParser.instance.isMobileBrowser;
return Safari._(() async {
if (isMobileBrowser) {
// iOS-Safari
// Uses `xcrun simctl`. It is a command line utility to control the
// Simulator. For more details on interacting with the simulator:
// https://developer.apple.com/library/archive/documentation/IDEs/Conceptual/iOS_Simulator_Guide/InteractingwiththeiOSSimulator/InteractingwiththeiOSSimulator.html
var process = await Process.start('xcrun', [
'simctl',
'openurl', // Opens the url on Safari installed on the simulator.
'booted', // The simulator is already booted.
'${url.toString()}'
]);
return process;
} else {
// Desktop-Safari
// TODO(nurhan): Configure info log for LUCI.
final BrowserInstallation installation = await getOrInstallSafari(
version,
infoLog: DevNull(),
);
// In the macOS Catalina opening Safari browser with a file brings
// a popup which halts the test.
// The following list of arguments needs to be provided to the `open`
// command to open Safari for a given URL. In summary, `open` tool opens
// a new Safari browser (even if one is already open), opens it with no
// persistent state and wait until it opens.
// The details copied from `man open` on macOS.
// TODO(nurhan): https://github.com/flutter/flutter/issues/50809
var process = await Process.start(installation.executable, [
// These are flags for `open` command line tool.
'-F', // Open a fresh Safari with no persistent state.
'-W', // Wait until the Safari opens.
'-n', // Open a new instance of the Safari even another one is open.
'-b', // Specifies the bundle identifier for the application to use.
'com.apple.Safari', // Bundle identifier for Safari.
'${url.toString()}'
]);
return process;
}
});
}
Safari._(Future<Process> startBrowser()) : super(startBrowser);
}

View File

@ -2,89 +2,86 @@
// 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:io' as io;
import 'dart:convert';
import 'dart:math';
import 'dart:math' as math;
import 'package:image/image.dart';
import 'package:path/path.dart' as path;
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'
as wip;
import 'package:test_api/src/backend/runtime.dart';
import 'package:yaml/yaml.dart';
import 'browser.dart';
import 'common.dart';
import 'environment.dart';
import 'safari_installation.dart';
import 'utils.dart';
/// [ScreenshotManager] implementation for Chrome.
///
/// This manager can be used for both macOS and Linux.
// TODO: https://github.com/flutter/flutter/issues/65673
class ChromeScreenshotManager extends ScreenshotManager {
String get filenameSuffix => '';
/// Capture a screenshot of the web content.
///
/// Uses Webkit Inspection Protocol server's `captureScreenshot` API.
///
/// [region] is used to decide which part of the web content will be used in
/// test image. It includes starting coordinate x,y as well as height and
/// width of the area to capture.
Future<Image> capture(Rectangle? region) async {
final wip.ChromeConnection chromeConnection =
wip.ChromeConnection('localhost', kDevtoolsPort);
final wip.ChromeTab? chromeTab = await chromeConnection.getTab(
(wip.ChromeTab chromeTab) => chromeTab.url.contains('localhost'));
if (chromeTab == null) {
throw StateError(
'Failed locate Chrome tab with the test page',
);
}
final wip.WipConnection wipConnection = await chromeTab.connect();
Map<String, dynamic>? captureScreenshotParameters = null;
if (region != null) {
captureScreenshotParameters = <String, dynamic>{
'format': 'png',
'clip': <String, dynamic>{
'x': region.left,
'y': region.top,
'width': region.width,
'height': region.height,
'scale':
// This is NOT the DPI of the page, instead it's the "zoom level".
1,
},
};
}
// Setting hardware-independent screen parameters:
// https://chromedevtools.github.io/devtools-protocol/tot/Emulation
await wipConnection
.sendCommand('Emulation.setDeviceMetricsOverride', <String, dynamic>{
'width': kMaxScreenshotWidth,
'height': kMaxScreenshotHeight,
'deviceScaleFactor': 1,
'mobile': false,
});
final wip.WipResponse response = await wipConnection.sendCommand(
'Page.captureScreenshot', captureScreenshotParameters);
final Image screenshot =
decodePng(base64.decode(response.result!['data'] as String))!;
return screenshot;
/// Provides an environment for the mobile variant of Safari running in an iOS
/// simulator.
class SafariIosEnvironment implements BrowserEnvironment {
@override
Browser launchBrowserInstance(Uri url, {bool debug = false}) {
return SafariIos(url);
}
@override
Runtime get packageTestRuntime => Runtime.safari;
@override
Future<void> prepareEnvironment() async {
await IosSafariArgParser.instance.initIosSimulator();
}
@override
ScreenshotManager? getScreenshotManager() {
return SafariIosScreenshotManager();
}
@override
String get packageTestConfigurationYamlFile => 'dart_test_safari.yaml';
}
/// Runs an instance of Safari for iOS (i.e. mobile Safari).
///
/// Most of the communication with the browser is expected to happen via HTTP,
/// so this exposes a bare-bones API. The browser starts as soon as the class is
/// constructed, and is killed when [close] is called.
///
/// Any errors starting or running the process are reported through [onExit].
class SafariIos extends Browser {
@override
final name = 'Safari iOS';
/// Starts a new instance of Safari open to the given [url], which may be a
/// [Uri].
factory SafariIos(Uri url) {
return SafariIos._(() async {
// iOS-Safari
// Uses `xcrun simctl`. It is a command line utility to control the
// Simulator. For more details on interacting with the simulator:
// https://developer.apple.com/library/archive/documentation/IDEs/Conceptual/iOS_Simulator_Guide/InteractingwiththeiOSSimulator/InteractingwiththeiOSSimulator.html
var process = await io.Process.start('xcrun', [
'simctl',
'openurl', // Opens the url on Safari installed on the simulator.
'booted', // The simulator is already booted.
'${url.toString()}',
]);
return process;
});
}
SafariIos._(Future<io.Process> startBrowser()) : super(startBrowser);
}
/// [ScreenshotManager] implementation for Safari.
///
/// This manager will only be created/used for macOS.
class IosSafariScreenshotManager extends ScreenshotManager {
class SafariIosScreenshotManager extends ScreenshotManager {
String get filenameSuffix => '.iOS_Safari';
IosSafariScreenshotManager() {
SafariIosScreenshotManager() {
final YamlMap browserLock = BrowserLock.instance.configuration;
_heightOfHeader = browserLock['ios-safari']['heightOfHeader'] as int;
_heightOfFooter = browserLock['ios-safari']['heightOfFooter'] as int;
@ -157,7 +154,7 @@ class IosSafariScreenshotManager extends ScreenshotManager {
/// width of the area to capture.
///
/// Uses simulator tool `xcrun simctl`'s 'screenshot' command.
Future<Image> capture(Rectangle? region) async {
Future<Image> capture(math.Rectangle? region) async {
final String filename = 'screenshot${_fileNameCounter}.png';
_fileNameCounter++;
@ -189,7 +186,7 @@ class IosSafariScreenshotManager extends ScreenshotManager {
if (region == null) {
return content;
} else {
final Rectangle scaledRegion = _scaleScreenshotRegion(region);
final math.Rectangle scaledRegion = _scaleScreenshotRegion(region);
return copyCrop(
content,
scaledRegion.left.toInt(),
@ -203,8 +200,8 @@ class IosSafariScreenshotManager extends ScreenshotManager {
/// Perform a linear transform on the screenshot region to convert its
/// dimensions from linear coordinated to coordinated on the phone screen.
/// This uniform/isotropic scaling is done using [_scaleFactor].
Rectangle _scaleScreenshotRegion(Rectangle region) {
return Rectangle(
math.Rectangle _scaleScreenshotRegion(math.Rectangle region) {
return math.Rectangle(
region.left * _scaleFactor,
region.top * _scaleFactor,
region.width * _scaleFactor,
@ -212,40 +209,3 @@ class IosSafariScreenshotManager extends ScreenshotManager {
);
}
}
const String _kBrowserChrome = 'chrome';
const String _kBrowserIOSSafari = 'ios-safari';
typedef ScreenshotManagerFactory = ScreenshotManager Function();
/// Abstract class for taking screenshots in one of the browsers.
abstract class ScreenshotManager {
static final Map<String, ScreenshotManagerFactory> _browserFactories =
<String, ScreenshotManagerFactory>{
_kBrowserChrome: () => ChromeScreenshotManager(),
_kBrowserIOSSafari: () => IosSafariScreenshotManager(),
};
static bool isBrowserSupported(String browser) =>
_browserFactories.containsKey(browser);
static ScreenshotManager choose(String browser) {
if (isBrowserSupported(browser)) {
return _browserFactories[browser]!();
}
throw StateError('Screenshot tests are only supported on Chrome and on '
'iOS Safari');
}
/// Capture a screenshot.
///
/// Please read the details for the implementing classes.
Future<Image> capture(Rectangle region);
/// Suffix to be added to the end of the filename.
///
/// Example file names:
/// - Chrome, no-suffix: backdrop_filter_clip_moved.actual.png
/// - iOS Safari: backdrop_filter_clip_moved.actual.iOS_Safari.png
String get filenameSuffix;
}

View File

@ -0,0 +1,82 @@
// Copyright 2013 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:io';
import 'package:test_api/src/backend/runtime.dart';
import 'browser.dart';
import 'common.dart';
import 'safari_installation.dart';
/// Provides an environment for the desktop variant of Safari running on macOS.
class SafariMacOsEnvironment implements BrowserEnvironment {
@override
Browser launchBrowserInstance(Uri url, {bool debug = false}) {
return SafariMacOs(url);
}
@override
Runtime get packageTestRuntime => Runtime.safari;
@override
Future<void> prepareEnvironment() async {
// Nothing extra to prepare for desktop Safari.
}
// We do not yet support screenshots on desktop Safari.
@override
ScreenshotManager? getScreenshotManager() => null;
@override
String get packageTestConfigurationYamlFile => 'dart_test_safari.yaml';
}
/// Runs an instance of Safari for macOS (i.e. desktop Safari).
///
/// Most of the communication with the browser is expected to happen via HTTP,
/// so this exposes a bare-bones API. The browser starts as soon as the class is
/// constructed, and is killed when [close] is called.
///
/// Any errors starting or running the process are reported through [onExit].
class SafariMacOs extends Browser {
@override
final name = 'Safari macOS';
/// Starts a new instance of Safari open to the given [url], which may be a
/// [Uri].
factory SafariMacOs(Uri url) {
final String version = SafariArgParser.instance.version;
return SafariMacOs._(() async {
// TODO(nurhan): Configure info log for LUCI.
final BrowserInstallation installation = await getOrInstallSafari(
version,
infoLog: DevNull(),
);
// In the macOS Catalina opening Safari browser with a file brings
// a popup which halts the test.
// The following list of arguments needs to be provided to the `open`
// command to open Safari for a given URL. In summary, `open` tool opens
// a new Safari browser (even if one is already open), opens it with no
// persistent state and wait until it opens.
// The details copied from `man open` on macOS.
// TODO(nurhan): https://github.com/flutter/flutter/issues/50809
var process = await Process.start(installation.executable, [
// These are flags for `open` command line tool.
'-F', // Open a fresh Safari with no persistent state.
'-W', // Wait until the Safari opens.
'-n', // Open a new instance of the Safari even another one is open.
'-b', // Specifies the bundle identifier for the application to use.
'com.apple.Safari', // Bundle identifier for Safari.
'${url.toString()}'
]);
return process;
});
}
SafariMacOs._(Future<Process> startBrowser()) : super(startBrowser);
}

View File

@ -1,91 +0,0 @@
// Copyright 2013 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 'package:test_api/src/backend/runtime.dart';
import 'browser.dart';
import 'chrome.dart';
import 'chrome_installer.dart';
import 'common.dart';
import 'edge.dart';
import 'environment.dart';
import 'edge_installation.dart';
import 'firefox.dart';
import 'firefox_installer.dart';
import 'safari.dart';
import 'safari_installation.dart';
/// Utilities for browsers, that tests are supported.
///
/// Extending [Browser] is not enough for supporting test.
///
/// Each new browser should be added to the [Runtime] map, to the [getBrowser]
/// method.
///
/// One should also implement [BrowserArgParser] and add it to the [argParsers].
class SupportedBrowsers {
final List<BrowserArgParser> argParsers = List.of([
ChromeArgParser.instance,
EdgeArgParser.instance,
FirefoxArgParser.instance,
SafariArgParser.instance
]);
final List<String> supportedBrowserNames = [
'chrome',
'edge',
'firefox',
'safari'
];
final Map<String, Runtime> supportedBrowsersToRuntimes = {
'chrome': Runtime.chrome,
'edge': Runtime.internetExplorer,
'firefox': Runtime.firefox,
'safari': Runtime.safari,
'ios-safari': Runtime.safari,
};
final Map<String, String> supportedBrowserToPlatform = {
'chrome': 'chrome',
'edge': 'ie',
'firefox': 'firefox',
'safari': 'safari',
'ios-safari': 'safari',
};
final Map<String, String> browserToConfiguration = {
'chrome':
'--configuration=${environment.webUiRootDir.path}/dart_test_chrome.yaml',
'edge':
'--configuration=${environment.webUiRootDir.path}/dart_test_edge.yaml',
'firefox':
'--configuration=${environment.webUiRootDir.path}/dart_test_firefox.yaml',
'safari':
'--configuration=${environment.webUiRootDir.path}/dart_test_safari.yaml',
'ios-safari':
'--configuration=${environment.webUiRootDir.path}/dart_test_safari.yaml',
};
static final SupportedBrowsers _singletonInstance = SupportedBrowsers._();
/// The [SupportedBrowsers] singleton.
static SupportedBrowsers get instance => _singletonInstance;
SupportedBrowsers._();
Browser getBrowser(Runtime runtime, Uri url, {bool debug = false}) {
if (runtime == Runtime.chrome) {
return Chrome(url, debug: debug);
} else if (runtime == Runtime.internetExplorer) {
return Edge(url, debug: debug);
} else if (runtime == Runtime.firefox) {
return Firefox(url, debug: debug);
} else if (runtime == Runtime.safari) {
return Safari(url, debug: debug);
} else {
throw new UnsupportedError('The browser type not supported in tests');
}
}
}

View File

@ -39,26 +39,22 @@ import 'package:test_core/src/runner/configuration.dart';
import 'browser.dart';
import 'common.dart';
import 'environment.dart' as env;
import 'screenshot_manager.dart';
import 'supported_browsers.dart';
/// Custom test platform that serves web engine unit tests.
class BrowserPlatform extends PlatformPlugin {
/// Starts the server.
///
/// [browserName] is the name of the browser that's used to run the test. It
/// must be supported by [SupportedBrowsers].
/// [browserEnvironment] provides the browser environment to run the test.
///
/// If [doUpdateScreenshotGoldens] is true updates screenshot golden files
/// instead of failing the test on screenshot mismatches.
static Future<BrowserPlatform> start({
required String browserName,
required BrowserEnvironment browserEnvironment,
required bool doUpdateScreenshotGoldens,
}) async {
assert(SupportedBrowsers.instance.supportedBrowserNames.contains(browserName));
final shelf_io.IOServer server = shelf_io.IOServer(await HttpMultiServer.loopback(0));
return BrowserPlatform._(
browserName: browserName,
browserEnvironment: browserEnvironment,
server: server,
isDebug: Configuration.current.pauseAfterLoad,
faviconPath: p.fromUri(await Isolate.resolvePackageUri(
@ -76,8 +72,8 @@ class BrowserPlatform extends PlatformPlugin {
/// The underlying server.
final shelf.Server server;
/// Name for the running browser. Not final on purpose can be mutated later.
String browserName;
/// Provides the environment for the browser running tests.
final BrowserEnvironment browserEnvironment;
/// The URL for this server.
Uri get url => server.url.resolve('/');
@ -105,7 +101,7 @@ class BrowserPlatform extends PlatformPlugin {
final PackageConfig packageConfig;
BrowserPlatform._({
required this.browserName,
required this.browserEnvironment,
required this.server,
required this.isDebug,
required String faviconPath,
@ -154,10 +150,9 @@ class BrowserPlatform extends PlatformPlugin {
// This handler goes last, after all more specific handlers failed to handle the request.
.add(_createAbsolutePackageUrlHandler());
// Screenshot tests are only enabled in Chrome and Safari iOS for now.
if (browserName == 'chrome' || browserName == 'ios-safari') {
_screenshotManager = browserEnvironment.getScreenshotManager();
if (_screenshotManager != null) {
cascade = cascade.add(_screeshotHandler);
_screenshotManager = ScreenshotManager.choose(browserName);
}
server.mount(cascade.handler);
@ -238,11 +233,6 @@ class BrowserPlatform extends PlatformPlugin {
}
Future<shelf.Response> _screeshotHandler(shelf.Request request) async {
if (browserName != 'chrome' && browserName != 'ios-safari') {
throw Exception('Screenshots tests are only available in Chrome '
'and in Safari-iOS.');
}
if (!request.requestedUri.path.endsWith('/screenshot')) {
return shelf.Response.notFound(
'This request is not handled by the screenshot handler');
@ -370,7 +360,7 @@ class BrowserPlatform extends PlatformPlugin {
p.toUri(p.withoutExtension(p.relative(path, from: env.environment.webUiBuildDir.path)) + '.html'));
_checkNotClosed();
final BrowserManager? browserManager = await _browserManagerFor(browser);
final BrowserManager? browserManager = await _startBrowserManager();
if (browserManager == null) {
throw StateError('Failed to initialize browser manager for ${browser.name}');
}
@ -386,10 +376,10 @@ class BrowserPlatform extends PlatformPlugin {
Future<BrowserManager?>? _browserManager;
/// Returns the [BrowserManager] for [runtime], which should be a browser.
/// Starts a browser manager for the browser provided by [browserEnvironment];
///
/// If no browser manager is running yet, starts one.
Future<BrowserManager?> _browserManagerFor(Runtime browser) {
Future<BrowserManager?> _startBrowserManager() {
if (_browserManager != null) {
return _browserManager!;
}
@ -405,7 +395,7 @@ class BrowserPlatform extends PlatformPlugin {
});
final Future<BrowserManager?> future = BrowserManager.start(
runtime: browser,
browserEnvironment: browserEnvironment,
url: hostUrl,
future: completer.future,
packageConfig: packageConfig,
@ -495,7 +485,7 @@ class OneOffHandler {
}
}
/// A class that manages the connection to a single running browser.
/// Manages the connection to a single running browser.
///
/// This is in charge of telling the browser which test suites to load and
/// converting its responses into [Suite] objects.
@ -505,8 +495,8 @@ class BrowserManager {
/// The browser instance that this is connected to via [_channel].
final Browser _browser;
/// The [Runtime] for [_browser].
final Runtime _runtime;
/// The browser environment for this test.
final BrowserEnvironment _browserEnvironment;
/// The channel used to communicate with the browser.
///
@ -567,13 +557,13 @@ class BrowserManager {
/// Returns the browser manager, or throws an [Exception] if a
/// connection fails to be established.
static Future<BrowserManager?> start({
required Runtime runtime,
required BrowserEnvironment browserEnvironment,
required Uri url,
required Future<WebSocketChannel> future,
required PackageConfig packageConfig,
bool debug = false,
}) {
var browser = _newBrowser(url, runtime, debug: debug);
var browser = _newBrowser(url, browserEnvironment, debug: debug);
var completer = Completer<BrowserManager>();
@ -593,7 +583,7 @@ class BrowserManager {
if (completer.isCompleted) {
return;
}
completer.complete(BrowserManager._(packageConfig, browser, runtime, webSocket));
completer.complete(BrowserManager._(packageConfig, browser, browserEnvironment, webSocket));
}).catchError((Object error, StackTrace stackTrace) {
browser.close();
if (completer.isCompleted) {
@ -605,16 +595,16 @@ class BrowserManager {
return completer.future;
}
/// Starts the browser identified by [browser] using [settings] and has it load [url].
/// Starts the browser and requests that it load the test page at [url].
///
/// If [debug] is true, starts the browser in debug mode.
static Browser _newBrowser(Uri url, Runtime browser, {bool debug = false}) {
return SupportedBrowsers.instance.getBrowser(browser, url, debug: debug);
static Browser _newBrowser(Uri url, BrowserEnvironment browserEnvironment, {bool debug = false}) {
return browserEnvironment.launchBrowserInstance(url, debug: debug);
}
/// Creates a new BrowserManager that communicates with [browser] over
/// Creates a new BrowserManager that communicates with the browser over
/// [webSocket].
BrowserManager._(this.packageConfig, this._browser, this._runtime, WebSocketChannel webSocket) {
BrowserManager._(this.packageConfig, this._browser, this._browserEnvironment, WebSocketChannel webSocket) {
// The duration should be short enough that the debugging console is open as
// soon as the user is done setting breakpoints, but long enough that a test
// doing a lot of synchronous work doesn't trigger a false positive.
@ -665,7 +655,7 @@ class BrowserManager {
url = url.replace(
fragment: Uri.encodeFull(jsonEncode(<String, dynamic>{
'metadata': suiteConfig.metadata.serialize(),
'browser': _runtime.identifier
'browser': _browserEnvironment.packageTestRuntime.identifier
})));
var suiteID = _suiteID++;
@ -697,7 +687,7 @@ class BrowserManager {
});
try {
controller = deserializeSuite(path, currentPlatform(_runtime),
controller = deserializeSuite(path, currentPlatform(_browserEnvironment.packageTestRuntime),
suiteConfig, await _environment, suiteChannel, message);
final String sourceMapFileName =

View File

@ -28,13 +28,21 @@ import 'package:test_core/src/executable.dart'
as test;
import 'package:web_test_utils/goldens.dart';
import 'browser.dart';
import 'chrome.dart';
import 'chrome_installer.dart';
import 'common.dart';
import 'edge.dart';
import 'edge_installation.dart';
import 'environment.dart';
import 'exceptions.dart';
import 'firefox.dart';
import 'firefox_installer.dart';
import 'integration_tests_manager.dart';
import 'macos_info.dart';
import 'safari_installation.dart';
import 'supported_browsers.dart';
import 'safari_ios.dart';
import 'safari_macos.dart';
import 'test_platform.dart';
import 'utils.dart';
import 'watcher.dart';
@ -62,6 +70,29 @@ enum TestTypesRequested {
all,
}
/// Command-line argument parsers that parse browser-specific options.
final List<BrowserArgParser> _browserArgParsers = <BrowserArgParser>[
ChromeArgParser.instance,
EdgeArgParser.instance,
FirefoxArgParser.instance,
SafariArgParser.instance,
];
/// Creates an environment for a browser.
///
/// The [browserName] matches the browser name passed as the `--browser` option.
BrowserEnvironment _createBrowserEnvironment(String browserName) {
switch (browserName) {
case 'chrome': return ChromeEnvironment();
case 'edge': return EdgeEnvironment();
case 'firefox': return FirefoxEnvironment();
case 'safari': return SafariMacOsEnvironment();
case 'ios-safari': return SafariIosEnvironment();
}
throw UnsupportedError('Browser $browserName is not supported.');
}
/// Runs tests.
class TestCommand extends Command<bool> with ArgUtils {
TestCommand() {
argParser
@ -113,12 +144,11 @@ class TestCommand extends Command<bool> with ArgUtils {
'for example, when a new browser version affects pixels.',
)
..addFlag(
'fetch-goldens-repo',
defaultsTo: true,
negatable: true,
help: 'Whether to fetch the goldens repo. Set this to false to iterate '
'on golden tests without fearing that the fetcher will overwrite '
'your local changes.',
'skip-goldens-repo-fetch',
defaultsTo: false,
help: 'If set reuses the existig flutter/goldens repo clone. Use this '
'to avoid overwriting local changes when iterating on golden '
'tests. This is off by default.',
)
..addOption(
'browser',
@ -136,8 +166,9 @@ class TestCommand extends Command<bool> with ArgUtils {
'finish.',
);
SupportedBrowsers.instance.argParsers
.forEach((t) => t.populateOptions(argParser));
for (BrowserArgParser browserArgParser in _browserArgParsers) {
browserArgParser.populateOptions(argParser);
}
GeneralTestsArgumentParser.instance.populateOptions(argParser);
IntegrationTestsArgumentParser.instance.populateOptions(argParser);
}
@ -183,8 +214,9 @@ class TestCommand extends Command<bool> with ArgUtils {
@override
Future<bool> run() async {
SupportedBrowsers.instance
..argParsers.forEach((t) => t.parseOptions(argResults!));
for (BrowserArgParser browserArgParser in _browserArgParsers) {
browserArgParser.parseOptions(argResults!);
}
GeneralTestsArgumentParser.instance.parseOptions(argResults!);
/// Collect information on the bot.
@ -310,7 +342,7 @@ class TestCommand extends Command<bool> with ArgUtils {
environment.webUiTestResultsDirectory.createSync(recursive: true);
// If screenshot tests are available, fetch the screenshot goldens.
if (isScreenshotTestsAvailable && doFetchGoldensRepo) {
if (isScreenshotTestsAvailable && !skipGoldensRepoFetch) {
if (isVerboseLoggingEnabled) {
print('INFO: Fetching goldens repo');
}
@ -320,11 +352,7 @@ class TestCommand extends Command<bool> with ArgUtils {
await goldensRepoFetcher.fetch();
}
// In order to run iOS Safari unit tests we need to make sure iOS Simulator
// is booted.
if (isSafariIOS) {
await IosSafariArgParser.instance.initIosSimulator();
}
await browserEnvironment.prepareEnvironment();
_testPreparationReady = true;
}
@ -391,6 +419,10 @@ class TestCommand extends Command<bool> with ArgUtils {
/// The name of the browser to run tests in.
String get browser => stringArg('browser')!;
/// The browser environment for the [browser].
BrowserEnvironment get browserEnvironment => (_browserEnvironment ??= _createBrowserEnvironment(browser));
BrowserEnvironment? _browserEnvironment;
/// Whether [browser] is set to "chrome".
bool get isChrome => browser == 'chrome';
@ -455,7 +487,7 @@ class TestCommand extends Command<bool> with ArgUtils {
bool get doUpdateScreenshotGoldens => boolArg('update-screenshot-goldens')!;
/// Whether to fetch the goldens repo prior to running tests.
bool get doFetchGoldensRepo => boolArg('fetch-goldens-repo')!;
bool get skipGoldensRepoFetch => boolArg('skip-goldens-repo-fetch')!;
/// Runs all tests specified in [targets].
///
@ -692,6 +724,10 @@ class TestCommand extends Command<bool> with ArgUtils {
required int concurrency,
required bool expectFailure,
}) async {
final String configurationFilePath = path.join(
environment.webUiRootDir.path,
browserEnvironment.packageTestConfigurationYamlFile,
);
final List<String> testArgs = <String>[
...<String>['-r', 'compact'],
'--concurrency=$concurrency',
@ -699,9 +735,9 @@ class TestCommand extends Command<bool> with ArgUtils {
// Don't pollute logs with output from tests that are expected to fail.
if (expectFailure)
'--reporter=name-only',
'--platform=${SupportedBrowsers.instance.supportedBrowserToPlatform[browser]}',
'--platform=${browserEnvironment.packageTestRuntime.identifier}',
'--precompiled=${environment.webUiBuildDir.path}',
SupportedBrowsers.instance.browserToConfiguration[browser]!,
'--configuration=$configurationFilePath',
'--',
...testFiles.map((f) => f.relativeToWebUi).toList(),
];
@ -716,10 +752,10 @@ class TestCommand extends Command<bool> with ArgUtils {
}
hack.registerPlatformPlugin(<Runtime>[
SupportedBrowsers.instance.supportedBrowsersToRuntimes[browser]!
browserEnvironment.packageTestRuntime,
], () {
return BrowserPlatform.start(
browserName: browser,
browserEnvironment: browserEnvironment,
// It doesn't make sense to update a screenshot for a test that is
// expected to fail.
doUpdateScreenshotGoldens: !expectFailure && doUpdateScreenshotGoldens,