mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
* carrying code * more changes for carrying the code * rebase changes onto ios-screenshot tests * adding screenshot capability to text_editing e2e test * address some comments * change enable flag for isUnitTestsScreenshotsAvailable * addressing the reviewer comments * change the dependency for path * add to licencense file * changing goldens commit no. the new commit has the screenshot goldens * update readme file * firefox tests needs LUCI changes * change to release mode since screenshots were taken in release mode * change window size * some argument changes * small comment change * test the chrome linux tests again * use roboto font instead of default font * addressing reviewer comments * change commit for goldens
774 lines
25 KiB
Dart
774 lines
25 KiB
Dart
// 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.
|
|
|
|
// @dart = 2.6
|
|
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
import 'dart:isolate';
|
|
import 'dart:math';
|
|
|
|
import 'package:async/async.dart';
|
|
import 'package:http_multi_server/http_multi_server.dart';
|
|
import 'package:image/image.dart';
|
|
import 'package:package_config/package_config.dart';
|
|
import 'package:path/path.dart' as p;
|
|
import 'package:pool/pool.dart';
|
|
import 'package:shelf/shelf.dart' as shelf;
|
|
import 'package:shelf/shelf_io.dart' as shelf_io;
|
|
import 'package:shelf_static/shelf_static.dart';
|
|
import 'package:shelf_web_socket/shelf_web_socket.dart';
|
|
import 'package:shelf_packages_handler/shelf_packages_handler.dart';
|
|
import 'package:stream_channel/stream_channel.dart';
|
|
import 'package:web_socket_channel/web_socket_channel.dart';
|
|
import 'package:web_test_utils/goldens.dart';
|
|
import 'package:web_test_utils/image_compare.dart';
|
|
|
|
import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports
|
|
import 'package:test_api/src/backend/suite_platform.dart'; // ignore: implementation_imports
|
|
import 'package:test_core/src/runner/runner_suite.dart'; // ignore: implementation_imports
|
|
import 'package:test_core/src/runner/platform.dart'; // ignore: implementation_imports
|
|
import 'package:test_core/src/util/stack_trace_mapper.dart'; // ignore: implementation_imports
|
|
import 'package:test_api/src/utils.dart'; // ignore: implementation_imports
|
|
import 'package:test_core/src/runner/suite.dart'; // ignore: implementation_imports
|
|
import 'package:test_core/src/runner/plugin/platform_helpers.dart'; // ignore: implementation_imports
|
|
import 'package:test_core/src/runner/environment.dart'; // ignore: implementation_imports
|
|
|
|
import 'package:test_core/src/util/io.dart'; // ignore: implementation_imports
|
|
import 'package:test_core/src/runner/configuration.dart'; // ignore: implementation_imports
|
|
|
|
import 'browser.dart';
|
|
import 'common.dart';
|
|
import 'environment.dart' as env;
|
|
import 'screenshot_manager.dart';
|
|
import 'supported_browsers.dart';
|
|
|
|
class BrowserPlatform extends PlatformPlugin {
|
|
/// Starts the server.
|
|
///
|
|
/// [root] is the root directory that the server should serve. It defaults to
|
|
/// the working directory.
|
|
static Future<BrowserPlatform> start(String name,
|
|
{String root, bool doUpdateScreenshotGoldens: false}) async {
|
|
assert(SupportedBrowsers.instance.supportedBrowserNames.contains(name));
|
|
var server = shelf_io.IOServer(await HttpMultiServer.loopback(0));
|
|
return BrowserPlatform._(
|
|
name,
|
|
server,
|
|
Configuration.current,
|
|
p.fromUri(await Isolate.resolvePackageUri(
|
|
Uri.parse('package:test/src/runner/browser/static/favicon.ico'))),
|
|
root: root,
|
|
doUpdateScreenshotGoldens: doUpdateScreenshotGoldens,
|
|
);
|
|
}
|
|
|
|
/// The test runner configuration.
|
|
final Configuration _config;
|
|
|
|
/// The underlying server.
|
|
final shelf.Server _server;
|
|
|
|
/// Name for the running browser. Not final on purpose can be mutated later.
|
|
String browserName;
|
|
|
|
/// A randomly-generated secret.
|
|
///
|
|
/// This is used to ensure that other users on the same system can't snoop
|
|
/// on data being served through this server.
|
|
final _secret = Uri.encodeComponent(randomBase64(24));
|
|
|
|
/// The URL for this server.
|
|
Uri get url => _server.url.resolve(_secret + '/');
|
|
|
|
/// A [OneOffHandler] for servicing WebSocket connections for
|
|
/// [BrowserManager]s.
|
|
///
|
|
/// This is one-off because each [BrowserManager] can only connect to a single
|
|
/// WebSocket,
|
|
final OneOffHandler _webSocketHandler = OneOffHandler();
|
|
|
|
/// A [PathHandler] used to serve compiled JS.
|
|
final PathHandler _jsHandler = PathHandler();
|
|
|
|
/// The root directory served statically by this server.
|
|
final String _root;
|
|
|
|
/// The HTTP client to use when caching JS files in `pub serve`.
|
|
final HttpClient _http;
|
|
|
|
/// Handles taking screenshots during tests.
|
|
///
|
|
/// Implementation will differ depending on the browser.
|
|
ScreenshotManager _screenshotManager;
|
|
|
|
/// Whether [close] has been called.
|
|
bool get _closed => _closeMemo.hasRun;
|
|
|
|
/// Whether to update screenshot golden files.
|
|
final bool doUpdateScreenshotGoldens;
|
|
|
|
BrowserPlatform._(
|
|
String name, this._server, Configuration config, String faviconPath,
|
|
{String root, this.doUpdateScreenshotGoldens})
|
|
: this.browserName = name,
|
|
_config = config,
|
|
_root = root == null ? p.current : root,
|
|
_http = config.pubServeUrl == null ? null : HttpClient() {
|
|
var cascade = shelf.Cascade().add(_webSocketHandler.handler);
|
|
|
|
if (_config.pubServeUrl == null) {
|
|
// We server static files from here (JS, HTML, etc)
|
|
final String staticFilePath =
|
|
config.suiteDefaults.precompiledPath ?? _root;
|
|
cascade = cascade
|
|
.add(packagesDirHandler())
|
|
.add(_jsHandler.handler)
|
|
.add(createStaticHandler(staticFilePath,
|
|
// Precompiled directories often contain symlinks
|
|
serveFilesOutsidePath:
|
|
config.suiteDefaults.precompiledPath != null))
|
|
.add(_wrapperHandler);
|
|
// Screenshot tests are only enabled in Chrome and Safari iOS for now.
|
|
if (browserName == 'chrome' || browserName == 'ios-safari') {
|
|
cascade = cascade.add(_screeshotHandler);
|
|
_screenshotManager = ScreenshotManager.choose(browserName);
|
|
}
|
|
}
|
|
|
|
var pipeline = shelf.Pipeline()
|
|
.addMiddleware(PathHandler.nestedIn(_secret))
|
|
.addHandler(cascade.handler);
|
|
|
|
_server.mount(shelf.Cascade()
|
|
.add(createFileHandler(faviconPath))
|
|
.add(pipeline)
|
|
.handler);
|
|
}
|
|
|
|
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');
|
|
}
|
|
|
|
final String payload = await request.readAsString();
|
|
final Map<String, dynamic> requestData =
|
|
json.decode(payload) as Map<String, dynamic>;
|
|
final String filename = requestData['filename'] as String;
|
|
final bool write = requestData['write'] as bool;
|
|
final double maxDiffRate = requestData.containsKey('maxdiffrate')
|
|
? (requestData['maxdiffrate'] as num)
|
|
.toDouble() // can be parsed as either int or double
|
|
: kMaxDiffRateFailure;
|
|
final Map<String, dynamic> region =
|
|
requestData['region'] as Map<String, dynamic>;
|
|
final PixelComparison pixelComparison = PixelComparison.values.firstWhere(
|
|
(value) => value.toString() == requestData['pixelComparison']);
|
|
final String result = await _diffScreenshot(
|
|
filename, write, maxDiffRate, region, pixelComparison);
|
|
return shelf.Response.ok(json.encode(result));
|
|
}
|
|
|
|
Future<String> _diffScreenshot(
|
|
String filename,
|
|
bool write,
|
|
double maxDiffRateFailure,
|
|
Map<String, dynamic> region,
|
|
PixelComparison pixelComparison) async {
|
|
if (doUpdateScreenshotGoldens) {
|
|
write = true;
|
|
}
|
|
|
|
filename =
|
|
filename.replaceAll('.png', '${_screenshotManager.filenameSuffix}.png');
|
|
|
|
String goldensDirectory;
|
|
if (filename.startsWith('__local__')) {
|
|
filename = filename.substring('__local__/'.length);
|
|
goldensDirectory = p.join(
|
|
env.environment.webUiRootDir.path,
|
|
'test',
|
|
'golden_files',
|
|
);
|
|
} else {
|
|
goldensDirectory = p.join(
|
|
env.environment.webUiGoldensRepositoryDirectory.path,
|
|
'engine',
|
|
'web',
|
|
);
|
|
}
|
|
|
|
final Rectangle regionAsRectange = Rectangle(
|
|
region['x'] as num,
|
|
region['y'] as num,
|
|
region['width'] as num,
|
|
region['height'] as num,
|
|
);
|
|
|
|
// Take screenshot.
|
|
final Image screenshot = await _screenshotManager.capture(regionAsRectange);
|
|
|
|
return compareImage(
|
|
screenshot,
|
|
doUpdateScreenshotGoldens,
|
|
filename,
|
|
pixelComparison,
|
|
maxDiffRateFailure,
|
|
goldensDirectory: goldensDirectory,
|
|
write: write);
|
|
}
|
|
|
|
/// A handler that serves wrapper files used to bootstrap tests.
|
|
shelf.Response _wrapperHandler(shelf.Request request) {
|
|
var path = p.fromUri(request.url);
|
|
|
|
if (path.endsWith('.html')) {
|
|
var test = p.withoutExtension(path) + '.dart';
|
|
|
|
// Link to the Dart wrapper.
|
|
var scriptBase = htmlEscape.convert(p.basename(test));
|
|
var link = '<link rel="x-dart-test" href="$scriptBase">';
|
|
|
|
return shelf.Response.ok('''
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>${htmlEscape.convert(test)} Test</title>
|
|
$link
|
|
<script src="packages/test/dart.js"></script>
|
|
</head>
|
|
</html>
|
|
''', headers: {'Content-Type': 'text/html'});
|
|
}
|
|
|
|
return shelf.Response.notFound('Not found.');
|
|
}
|
|
|
|
/// Loads the test suite at [path] on the platform [platform].
|
|
///
|
|
/// This will start a browser to load the suite if one isn't already running.
|
|
/// Throws an [ArgumentError] if `platform.platform` isn't a browser.
|
|
Future<RunnerSuite> load(String path, SuitePlatform platform,
|
|
SuiteConfiguration suiteConfig, Object message) async {
|
|
if (suiteConfig.precompiledPath == null) {
|
|
throw Exception('This test platform only supports precompiled JS.');
|
|
}
|
|
var browser = platform.runtime;
|
|
assert(suiteConfig.runtimes.contains(browser.identifier));
|
|
|
|
if (!browser.isBrowser) {
|
|
throw ArgumentError('$browser is not a browser.');
|
|
}
|
|
|
|
if (_closed) {
|
|
return null;
|
|
}
|
|
Uri suiteUrl = url.resolveUri(
|
|
p.toUri(p.withoutExtension(p.relative(path, from: _root)) + '.html'));
|
|
|
|
if (_closed) {
|
|
return null;
|
|
}
|
|
|
|
var browserManager = await _browserManagerFor(browser);
|
|
if (_closed || browserManager == null) {
|
|
return null;
|
|
}
|
|
|
|
var suite = await browserManager.load(path, suiteUrl, suiteConfig, message);
|
|
if (_closed) {
|
|
return null;
|
|
}
|
|
return suite;
|
|
}
|
|
|
|
StreamChannel loadChannel(String path, SuitePlatform platform) =>
|
|
throw UnimplementedError();
|
|
|
|
Future<BrowserManager> _browserManager;
|
|
|
|
/// Returns the [BrowserManager] for [runtime], which should be a browser.
|
|
///
|
|
/// If no browser manager is running yet, starts one.
|
|
Future<BrowserManager> _browserManagerFor(Runtime browser) {
|
|
if (_browserManager != null) {
|
|
return _browserManager;
|
|
}
|
|
|
|
var completer = Completer<WebSocketChannel>.sync();
|
|
var path = _webSocketHandler.create(webSocketHandler(completer.complete));
|
|
var webSocketUrl = url.replace(scheme: 'ws').resolve(path);
|
|
var hostUrl = (_config.pubServeUrl == null ? url : _config.pubServeUrl)
|
|
.resolve('packages/web_engine_tester/static/index.html')
|
|
.replace(queryParameters: <String, dynamic>{
|
|
'managerUrl': webSocketUrl.toString(),
|
|
'debug': _config.pauseAfterLoad.toString()
|
|
});
|
|
|
|
var future = BrowserManager.start(browser, hostUrl, completer.future,
|
|
debug: _config.pauseAfterLoad);
|
|
|
|
// Store null values for browsers that error out so we know not to load them
|
|
// again.
|
|
_browserManager = future.catchError((dynamic _) => null);
|
|
|
|
return future;
|
|
}
|
|
|
|
/// Close all the browsers that the server currently has open.
|
|
///
|
|
/// Note that this doesn't close the server itself. Browser tests can still be
|
|
/// loaded, they'll just spawn new browsers.
|
|
Future<void> closeEphemeral() async {
|
|
final BrowserManager result = await _browserManager;
|
|
if (result != null) {
|
|
await result.close();
|
|
}
|
|
}
|
|
|
|
/// Closes the server and releases all its resources.
|
|
///
|
|
/// Returns a [Future] that completes once the server is closed and its
|
|
/// resources have been fully released.
|
|
Future<void> close() {
|
|
return _closeMemo.runOnce(() async {
|
|
final List<Future<void>> futures = <Future<void>>[];
|
|
futures.add(Future<void>.microtask(() async {
|
|
final BrowserManager result = await _browserManager;
|
|
if (result != null) {
|
|
await result.close();
|
|
}
|
|
}));
|
|
futures.add(_server.close());
|
|
|
|
await Future.wait(futures);
|
|
|
|
if (_config.pubServeUrl != null) {
|
|
_http.close();
|
|
}
|
|
});
|
|
}
|
|
|
|
final AsyncMemoizer<dynamic> _closeMemo = AsyncMemoizer<dynamic>();
|
|
}
|
|
|
|
/// A Shelf handler that provides support for one-time handlers.
|
|
///
|
|
/// This is useful for handlers that only expect to be hit once before becoming
|
|
/// invalid and don't need to have a persistent URL.
|
|
class OneOffHandler {
|
|
/// A map from URL paths to handlers.
|
|
final _handlers = Map<String, shelf.Handler>();
|
|
|
|
/// The counter of handlers that have been activated.
|
|
var _counter = 0;
|
|
|
|
/// The actual [shelf.Handler] that dispatches requests.
|
|
shelf.Handler get handler => _onRequest;
|
|
|
|
/// Creates a new one-off handler that forwards to [handler].
|
|
///
|
|
/// Returns a string that's the URL path for hitting this handler, relative to
|
|
/// the URL for the one-off handler itself.
|
|
///
|
|
/// [handler] will be unmounted as soon as it receives a request.
|
|
String create(shelf.Handler handler) {
|
|
var path = _counter.toString();
|
|
_handlers[path] = handler;
|
|
_counter++;
|
|
return path;
|
|
}
|
|
|
|
/// Dispatches [request] to the appropriate handler.
|
|
FutureOr<shelf.Response> _onRequest(shelf.Request request) {
|
|
var components = p.url.split(request.url.path);
|
|
if (components.isEmpty) {
|
|
return shelf.Response.notFound(null);
|
|
}
|
|
|
|
var path = components.removeAt(0);
|
|
var handler = _handlers.remove(path);
|
|
if (handler == null) {
|
|
return shelf.Response.notFound(null);
|
|
}
|
|
return handler(request.change(path: path));
|
|
}
|
|
}
|
|
|
|
/// A handler that routes to sub-handlers based on exact path prefixes.
|
|
class PathHandler {
|
|
/// A trie of path components to handlers.
|
|
final _paths = _Node();
|
|
|
|
/// The shelf handler.
|
|
shelf.Handler get handler => _onRequest;
|
|
|
|
/// Returns middleware that nests all requests beneath the URL prefix
|
|
/// [beneath].
|
|
static shelf.Middleware nestedIn(String beneath) {
|
|
return (handler) {
|
|
var pathHandler = PathHandler()..add(beneath, handler);
|
|
return pathHandler.handler;
|
|
};
|
|
}
|
|
|
|
/// Routes requests at or under [path] to [handler].
|
|
///
|
|
/// If [path] is a parent or child directory of another path in this handler,
|
|
/// the longest matching prefix wins.
|
|
void add(String path, shelf.Handler handler) {
|
|
var node = _paths;
|
|
for (var component in p.url.split(path)) {
|
|
node = node.children.putIfAbsent(component, () => _Node());
|
|
}
|
|
node.handler = handler;
|
|
}
|
|
|
|
FutureOr<shelf.Response> _onRequest(shelf.Request request) {
|
|
shelf.Handler handler;
|
|
int handlerIndex;
|
|
var node = _paths;
|
|
var components = p.url.split(request.url.path);
|
|
for (var i = 0; i < components.length; i++) {
|
|
node = node.children[components[i]];
|
|
if (node == null) {
|
|
break;
|
|
}
|
|
if (node.handler == null) {
|
|
continue;
|
|
}
|
|
handler = node.handler;
|
|
handlerIndex = i;
|
|
}
|
|
|
|
if (handler == null) {
|
|
return shelf.Response.notFound('Not found.');
|
|
}
|
|
|
|
return handler(
|
|
request.change(path: p.url.joinAll(components.take(handlerIndex + 1))));
|
|
}
|
|
}
|
|
|
|
/// A trie node.
|
|
class _Node {
|
|
shelf.Handler handler;
|
|
final children = Map<String, _Node>();
|
|
}
|
|
|
|
/// A class that 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.
|
|
class BrowserManager {
|
|
/// The browser instance that this is connected to via [_channel].
|
|
final Browser _browser;
|
|
|
|
/// The [Runtime] for [_browser].
|
|
final Runtime _runtime;
|
|
|
|
/// The channel used to communicate with the browser.
|
|
///
|
|
/// This is connected to a page running `static/host.dart`.
|
|
MultiChannel _channel;
|
|
|
|
/// A pool that ensures that limits the number of initial connections the
|
|
/// manager will wait for at once.
|
|
///
|
|
/// This isn't the *total* number of connections; any number of iframes may be
|
|
/// loaded in the same browser. However, the browser can only load so many at
|
|
/// once, and we want a timeout in case they fail so we only wait for so many
|
|
/// at once.
|
|
final _pool = Pool(8);
|
|
|
|
/// The ID of the next suite to be loaded.
|
|
///
|
|
/// This is used to ensure that the suites can be referred to consistently
|
|
/// across the client and server.
|
|
int _suiteID = 0;
|
|
|
|
/// Whether the channel to the browser has closed.
|
|
bool _closed = false;
|
|
|
|
/// The completer for [_BrowserEnvironment.displayPause].
|
|
///
|
|
/// This will be `null` as long as the browser isn't displaying a pause
|
|
/// screen.
|
|
CancelableCompleter _pauseCompleter;
|
|
|
|
/// The controller for [_BrowserEnvironment.onRestart].
|
|
final _onRestartController = StreamController<dynamic>.broadcast();
|
|
|
|
/// The environment to attach to each suite.
|
|
Future<_BrowserEnvironment> _environment;
|
|
|
|
/// Controllers for every suite in this browser.
|
|
///
|
|
/// These are used to mark suites as debugging or not based on the browser's
|
|
/// pings.
|
|
final _controllers = Set<RunnerSuiteController>();
|
|
|
|
// A timer that's reset whenever we receive a message from the browser.
|
|
//
|
|
// Because the browser stops running code when the user is actively debugging,
|
|
// this lets us detect whether they're debugging reasonably accurately.
|
|
RestartableTimer _timer;
|
|
|
|
/// Starts the browser identified by [runtime] and has it connect to [url].
|
|
///
|
|
/// [url] should serve a page that establishes a WebSocket connection with
|
|
/// this process. That connection, once established, should be emitted via
|
|
/// [future]. If [debug] is true, starts the browser in debug mode, with its
|
|
/// debugger interfaces on and detected.
|
|
///
|
|
/// The [settings] indicate how to invoke this browser's executable.
|
|
///
|
|
/// Returns the browser manager, or throws an [Exception] if a
|
|
/// connection fails to be established.
|
|
static Future<BrowserManager> start(
|
|
Runtime runtime, Uri url, Future<WebSocketChannel> future,
|
|
{bool debug = false}) {
|
|
var browser = _newBrowser(url, runtime, debug: debug);
|
|
|
|
var completer = Completer<BrowserManager>();
|
|
|
|
// For the cases where we use a delegator such as `adb` (for Android) or
|
|
// `xcrun` (for IOS), these delegator processes can shut down before the
|
|
// websocket is available. Therefore do not throw an error if proccess
|
|
// exits with exitCode 0. Note that `browser` will throw and error if the
|
|
// exit code was not 0, which will be processed by the next callback.
|
|
browser.onExit.catchError((dynamic error, StackTrace stackTrace) {
|
|
if (completer.isCompleted) {
|
|
return;
|
|
}
|
|
completer.completeError(error, stackTrace);
|
|
});
|
|
|
|
future.then((webSocket) {
|
|
if (completer.isCompleted) {
|
|
return;
|
|
}
|
|
completer.complete(BrowserManager._(browser, runtime, webSocket));
|
|
}).catchError((dynamic error, StackTrace stackTrace) {
|
|
browser.close();
|
|
if (completer.isCompleted) {
|
|
return;
|
|
}
|
|
completer.completeError(error, stackTrace);
|
|
});
|
|
|
|
return completer.future;
|
|
}
|
|
|
|
/// Starts the browser identified by [browser] using [settings] and has it load [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);
|
|
}
|
|
|
|
/// Creates a new BrowserManager that communicates with [browser] over
|
|
/// [webSocket].
|
|
BrowserManager._(this._browser, this._runtime, 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.
|
|
//
|
|
// Start this canceled because we don't want it to start ticking until we
|
|
// get some response from the iframe.
|
|
_timer = RestartableTimer(Duration(seconds: 3), () {
|
|
for (var controller in _controllers) {
|
|
controller.setDebugging(true);
|
|
}
|
|
})
|
|
..cancel();
|
|
|
|
// Whenever we get a message, no matter which child channel it's for, we the
|
|
// know browser is still running code which means the user isn't debugging.
|
|
_channel = MultiChannel<dynamic>(
|
|
webSocket.cast<String>().transform(jsonDocument).changeStream((stream) {
|
|
return stream.map((message) {
|
|
if (!_closed) {
|
|
_timer.reset();
|
|
}
|
|
for (var controller in _controllers) {
|
|
controller.setDebugging(false);
|
|
}
|
|
|
|
return message;
|
|
});
|
|
}));
|
|
|
|
_environment = _loadBrowserEnvironment();
|
|
_channel.stream
|
|
.listen((dynamic message) => _onMessage(message as Map), onDone: close);
|
|
}
|
|
|
|
/// Loads [_BrowserEnvironment].
|
|
Future<_BrowserEnvironment> _loadBrowserEnvironment() async {
|
|
return _BrowserEnvironment(this, await _browser.observatoryUrl,
|
|
await _browser.remoteDebuggerUrl, _onRestartController.stream);
|
|
}
|
|
|
|
/// Tells the browser the load a test suite from the URL [url].
|
|
///
|
|
/// [url] should be an HTML page with a reference to the JS-compiled test
|
|
/// suite. [path] is the path of the original test suite file, which is used
|
|
/// for reporting. [suiteConfig] is the configuration for the test suite.
|
|
Future<RunnerSuite> load(String path, Uri url, SuiteConfiguration suiteConfig,
|
|
Object message) async {
|
|
url = url.replace(
|
|
fragment: Uri.encodeFull(jsonEncode(<String, dynamic>{
|
|
'metadata': suiteConfig.metadata.serialize(),
|
|
'browser': _runtime.identifier
|
|
})));
|
|
|
|
var suiteID = _suiteID++;
|
|
RunnerSuiteController controller;
|
|
void closeIframe() {
|
|
if (_closed) {
|
|
return;
|
|
}
|
|
_controllers.remove(controller);
|
|
_channel.sink.add({'command': 'closeSuite', 'id': suiteID});
|
|
}
|
|
|
|
// The virtual channel will be closed when the suite is closed, in which
|
|
// case we should unload the iframe.
|
|
var virtualChannel = _channel.virtualChannel();
|
|
var suiteChannelID = virtualChannel.id;
|
|
var suiteChannel = virtualChannel.transformStream(
|
|
StreamTransformer<dynamic, dynamic>.fromHandlers(handleDone: (sink) {
|
|
closeIframe();
|
|
sink.close();
|
|
}));
|
|
|
|
return await _pool.withResource<RunnerSuite>(() async {
|
|
_channel.sink.add({
|
|
'command': 'loadSuite',
|
|
'url': url.toString(),
|
|
'id': suiteID,
|
|
'channel': suiteChannelID
|
|
});
|
|
|
|
try {
|
|
controller = deserializeSuite(path, currentPlatform(_runtime),
|
|
suiteConfig, await _environment, suiteChannel, message);
|
|
|
|
final String sourceMapFileName =
|
|
'${p.basename(path)}.browser_test.dart.js.map';
|
|
final String pathToTest = p.dirname(path);
|
|
|
|
final String mapPath = p.join(env.environment.webUiRootDir.path,
|
|
'build', pathToTest, sourceMapFileName);
|
|
|
|
PackageConfig packageConfig =
|
|
await loadPackageConfigUri(await Isolate.packageConfig);
|
|
Map<String, Uri> packageMap = {
|
|
for (var p in packageConfig.packages) p.name: p.packageUriRoot
|
|
};
|
|
final JSStackTraceMapper mapper = JSStackTraceMapper(
|
|
await File(mapPath).readAsString(),
|
|
mapUrl: p.toUri(mapPath),
|
|
packageMap: packageMap,
|
|
sdkRoot: p.toUri(sdkDir),
|
|
);
|
|
|
|
controller.channel('test.browser.mapper').sink.add(mapper.serialize());
|
|
|
|
_controllers.add(controller);
|
|
return await controller.suite;
|
|
} catch (_) {
|
|
closeIframe();
|
|
rethrow;
|
|
}
|
|
});
|
|
}
|
|
|
|
/// An implementation of [Environment.displayPause].
|
|
CancelableOperation _displayPause() {
|
|
if (_pauseCompleter != null) {
|
|
return _pauseCompleter.operation;
|
|
}
|
|
|
|
_pauseCompleter = CancelableCompleter<void>(onCancel: () {
|
|
_channel.sink.add({'command': 'resume'});
|
|
_pauseCompleter = null;
|
|
});
|
|
|
|
_pauseCompleter.operation.value.whenComplete(() {
|
|
_pauseCompleter = null;
|
|
});
|
|
|
|
_channel.sink.add({'command': 'displayPause'});
|
|
|
|
return _pauseCompleter.operation;
|
|
}
|
|
|
|
/// The callback for handling messages received from the host page.
|
|
void _onMessage(Map message) {
|
|
switch (message['command'] as String) {
|
|
case 'ping':
|
|
break;
|
|
|
|
case 'restart':
|
|
_onRestartController.add(null);
|
|
break;
|
|
|
|
case 'resume':
|
|
if (_pauseCompleter != null) {
|
|
_pauseCompleter.complete();
|
|
}
|
|
break;
|
|
|
|
default:
|
|
// Unreachable.
|
|
assert(false);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/// Closes the manager and releases any resources it owns, including closing
|
|
/// the browser.
|
|
Future close() => _closeMemoizer.runOnce(() {
|
|
_closed = true;
|
|
_timer.cancel();
|
|
if (_pauseCompleter != null) {
|
|
_pauseCompleter.complete();
|
|
}
|
|
_pauseCompleter = null;
|
|
_controllers.clear();
|
|
return _browser.close();
|
|
});
|
|
final AsyncMemoizer<dynamic> _closeMemoizer = AsyncMemoizer<dynamic>();
|
|
}
|
|
|
|
/// An implementation of [Environment] for the browser.
|
|
///
|
|
/// All methods forward directly to [BrowserManager].
|
|
class _BrowserEnvironment implements Environment {
|
|
final BrowserManager _manager;
|
|
|
|
final supportsDebugging = true;
|
|
|
|
final Uri observatoryUrl;
|
|
|
|
final Uri remoteDebuggerUrl;
|
|
|
|
final Stream onRestart;
|
|
|
|
_BrowserEnvironment(this._manager, this.observatoryUrl,
|
|
this.remoteDebuggerUrl, this.onRestart);
|
|
|
|
CancelableOperation displayPause() => _manager._displayPause();
|
|
}
|
|
|
|
bool get isCirrus => Platform.environment['CIRRUS_CI'] == 'true';
|