| Index: tools/testing/dart/browser_controller.dart
|
| diff --git a/tools/testing/dart/browser_controller.dart b/tools/testing/dart/browser_controller.dart
|
| index 27765697ea3b74db45fe86f46381cd51199c2df6..7094120661ad9d45ee880021dce68a5be28e1d92 100644
|
| --- a/tools/testing/dart/browser_controller.dart
|
| +++ b/tools/testing/dart/browser_controller.dart
|
| @@ -14,6 +14,9 @@ import 'http_server.dart';
|
| import 'path.dart';
|
| import 'utils.dart';
|
|
|
| +import 'reset_safari.dart' show
|
| + killAndResetSafari;
|
| +
|
| class BrowserOutput {
|
| final StringBuffer stdout = new StringBuffer();
|
| final StringBuffer stderr = new StringBuffer();
|
| @@ -53,10 +56,10 @@ abstract class Browser {
|
| String id;
|
|
|
| /**
|
| - * Delete the browser specific caches on startup.
|
| + * Reset the browser to a known configuration on start-up.
|
| * Browser specific implementations are free to ignore this.
|
| */
|
| - static bool deleteCache = false;
|
| + static bool resetBrowserConfiguration = false;
|
|
|
| /** Print everything (stdout, stderr, usageLog) whenever we add to it */
|
| bool debugPrint = false;
|
| @@ -105,6 +108,15 @@ abstract class Browser {
|
| 'ie10'
|
| ];
|
|
|
| + /// If [browserName] doesn't support Window.open, we use iframes instead.
|
| + static bool requiresIframe(String browserName) {
|
| + return !BROWSERS_WITH_WINDOW_SUPPORT.contains(browserName);
|
| + }
|
| +
|
| + static bool requiresFocus(String browserName) {
|
| + return browserName == "safari";
|
| + }
|
| +
|
| // TODO(kustermann): add standard support for chrome on android
|
| static bool supportedBrowser(String name) {
|
| return SUPPORTED_BROWSERS.contains(name);
|
| @@ -269,6 +281,13 @@ abstract class Browser {
|
|
|
| /** Starts the browser loading the given url */
|
| Future<bool> start(String url);
|
| +
|
| + /// Called when the driver page is requested, that is, when the browser first
|
| + /// contacts the test server. At this time, it's safe to assume that the
|
| + /// browser process has started and opened its first window.
|
| + ///
|
| + /// This is used by [Safari] to ensure the browser window has focus.
|
| + Future<Null> onDriverPageRequested() => new Future<Null>.value();
|
| }
|
|
|
| class Safari extends Browser {
|
| @@ -278,62 +297,48 @@ class Safari extends Browser {
|
| static const String versionFile =
|
| "/Applications/Safari.app/Contents/version.plist";
|
|
|
| - /**
|
| - * Directories where safari stores state. We delete these if the deleteCache
|
| - * is set
|
| - */
|
| - static const List<String> CACHE_DIRECTORIES = const [
|
| - "Library/Caches/com.apple.Safari",
|
| - "Library/Safari",
|
| - "Library/Saved Application State/com.apple.Safari.savedState",
|
| - "Library/Caches/Metadata/Safari"
|
| - ];
|
| + static const String safariBundleLocation = "/Applications/Safari.app/";
|
|
|
| - Future<bool> allowPopUps() {
|
| - var command = "defaults";
|
| - var args = [
|
| - "write",
|
| - "com.apple.safari",
|
| - "com.apple.Safari.ContentPageGroupIdentifier."
|
| - "WebKit2JavaScriptCanOpenWindowsAutomatically",
|
| - "1"
|
| - ];
|
| - return Process.run(command, args).then((result) {
|
| - if (result.exitCode != 0) {
|
| - _logEvent("Could not disable pop-up blocking for safari");
|
| - return false;
|
| - }
|
| - return true;
|
| - });
|
| - }
|
| + // Clears the cache if the static resetBrowserConfiguration flag is set.
|
| + // Returns false if the command to actually clear the cache did not complete.
|
| + Future<bool> resetConfiguration() async {
|
| + if (!Browser.resetBrowserConfiguration) return true;
|
|
|
| - Future<bool> deleteIfExists(Iterator<String> paths) {
|
| - if (!paths.moveNext()) return new Future.value(true);
|
| - Directory directory = new Directory(paths.current);
|
| - return directory.exists().then((exists) {
|
| - if (exists) {
|
| - _logEvent("Deleting ${paths.current}");
|
| - return directory
|
| - .delete(recursive: true)
|
| - .then((_) => deleteIfExists(paths))
|
| - .catchError((error) {
|
| - _logEvent("Failure trying to delete ${paths.current}: $error");
|
| - return false;
|
| - });
|
| + Completer completer = new Completer();
|
| + handleUncaughtError(error, StackTrace stackTrace) {
|
| + if (!completer.isCompleted) {
|
| + completer.completeError(error, stackTrace);
|
| } else {
|
| - _logEvent("${paths.current} is not present");
|
| - return deleteIfExists(paths);
|
| + throw new AsyncError(error, stackTrace);
|
| }
|
| - });
|
| - }
|
| + }
|
| + Zone parent = Zone.current;
|
| + ZoneSpecification specification = new ZoneSpecification(
|
| + print: (Zone self, ZoneDelegate delegate, Zone zone, String line) {
|
| + delegate.run(parent, () {
|
| + _logEvent(line);
|
| + });
|
| + });
|
| + Future zoneWrapper() {
|
| + Uri safariUri = Uri.base.resolve(safariBundleLocation);
|
| + return new Future(() => killAndResetSafari(bundle: safariUri))
|
| + .then(completer.complete);
|
| + }
|
|
|
| - // Clears the cache if the static deleteCache flag is set.
|
| - // Returns false if the command to actually clear the cache did not complete.
|
| - Future<bool> clearCache() {
|
| - if (!Browser.deleteCache) return new Future.value(true);
|
| - var home = Platform.environment['HOME'];
|
| - Iterator iterator = CACHE_DIRECTORIES.map((s) => "$home/$s").iterator;
|
| - return deleteIfExists(iterator);
|
| + // We run killAndResetSafari in a Zone as opposed to running an external
|
| + // process. The Zone allows us to collect its output, and protect the rest
|
| + // of the test infrastructure against errors in it.
|
| + runZoned(
|
| + zoneWrapper, zoneSpecification: specification,
|
| + onError: handleUncaughtError);
|
| +
|
| + try {
|
| + await completer.future;
|
| + return true;
|
| + } catch (error, st) {
|
| + _logEvent("Unable to reset Safari: $error$st");
|
| + return false;
|
| + }
|
| }
|
|
|
| Future<String> getVersion() {
|
| @@ -369,42 +374,58 @@ class Safari extends Browser {
|
| });
|
| }
|
|
|
| - void _createLaunchHTML(var path, var url) {
|
| + Future<Null> _createLaunchHTML(var path, var url) async {
|
| var file = new File("${path}/launch.html");
|
| - var randomFile = file.openSync(mode: FileMode.WRITE);
|
| + var randomFile = await file.open(mode: FileMode.WRITE);
|
| var content = '<script language="JavaScript">location = "$url"</script>';
|
| - randomFile.writeStringSync(content);
|
| - randomFile.close();
|
| + await randomFile.writeString(content);
|
| + await randomFile.close();
|
| }
|
|
|
| - Future<bool> start(String url) {
|
| + Future<bool> start(String url) async {
|
| _logEvent("Starting Safari browser on: $url");
|
| - return allowPopUps().then((success) {
|
| - if (!success) {
|
| - return false;
|
| - }
|
| - return clearCache().then((cleared) {
|
| - if (!cleared) {
|
| - _logEvent("Could not clear cache");
|
| - return false;
|
| - }
|
| - // Get the version and log that.
|
| - return getVersion().then((version) {
|
| - _logEvent("Got version: $version");
|
| - return Directory.systemTemp.createTemp().then((userDir) {
|
| - _cleanup = () {
|
| - userDir.deleteSync(recursive: true);
|
| - };
|
| - _createLaunchHTML(userDir.path, url);
|
| - var args = ["${userDir.path}/launch.html"];
|
| - return startBrowserProcess(_binary, args);
|
| - });
|
| - }).catchError((error) {
|
| - _logEvent("Running $_binary --version failed with $error");
|
| - return false;
|
| - });
|
| - });
|
| - });
|
| + if (!await resetConfiguration()) {
|
| + _logEvent("Could not clear cache");
|
| + return false;
|
| + }
|
| + String version;
|
| + try {
|
| + version = await getVersion();
|
| + } catch (error) {
|
| + _logEvent("Running $_binary --version failed with $error");
|
| + return false;
|
| + }
|
| + _logEvent("Got version: $version");
|
| + Directory userDir;
|
| + try {
|
| + userDir = await Directory.systemTemp.createTemp();
|
| + } catch (error) {
|
| + _logEvent("Error creating temporary directory: $error");
|
| + return false;
|
| + }
|
| + _cleanup = () {
|
| + userDir.deleteSync(recursive: true);
|
| + };
|
| + try {
|
| + await _createLaunchHTML(userDir.path, url);
|
| + } catch (error) {
|
| + _logEvent("Error creating launch HTML: $error");
|
| + return false;
|
| + }
|
| + var args = [
|
| + "-d", "-i", "-m", "-s", "-u", _binary,
|
| + "${userDir.path}/launch.html"];
|
| + try {
|
| + return startBrowserProcess("/usr/bin/caffeinate", args);
|
| + } catch (error) {
|
| + _logEvent("Error starting browser process: $error");
|
| + return false;
|
| + }
|
| + }
|
| +
|
| + Future<Null> onDriverPageRequested() async {
|
| + await Process.run("/usr/bin/osascript",
|
| + ['-e', 'tell application "Safari" to activate']);
|
| }
|
|
|
| String toString() => "Safari";
|
| @@ -477,16 +498,16 @@ class Chrome extends Browser {
|
| class SafariMobileSimulator extends Safari {
|
| /**
|
| * Directories where safari simulator stores state. We delete these if the
|
| - * deleteCache is set
|
| + * resetBrowserConfiguration is set
|
| */
|
| static const List<String> CACHE_DIRECTORIES = const [
|
| "Library/Application Support/iPhone Simulator/7.1/Applications"
|
| ];
|
|
|
| - // Clears the cache if the static deleteCache flag is set.
|
| + // Clears the cache if the static resetBrowserConfiguration flag is set.
|
| // Returns false if the command to actually clear the cache did not complete.
|
| - Future<bool> clearCache() {
|
| - if (!Browser.deleteCache) return new Future.value(true);
|
| + Future<bool> resetConfiguration() {
|
| + if (!Browser.resetBrowserConfiguration) return new Future.value(true);
|
| var home = Platform.environment['HOME'];
|
| Iterator iterator = CACHE_DIRECTORIES.map((s) => "$home/$s").iterator;
|
| return deleteIfExists(iterator);
|
| @@ -494,7 +515,7 @@ class SafariMobileSimulator extends Safari {
|
|
|
| Future<bool> start(String url) {
|
| _logEvent("Starting safari mobile simulator browser on: $url");
|
| - return clearCache().then((success) {
|
| + return resetConfiguration().then((success) {
|
| if (!success) {
|
| _logEvent("Could not clear cache, exiting");
|
| return false;
|
| @@ -561,9 +582,10 @@ class IE extends Browser {
|
| });
|
| }
|
|
|
| - // Clears the recovery cache if the static deleteCache flag is set.
|
| - Future<bool> clearCache() {
|
| - if (!Browser.deleteCache) return new Future.value(true);
|
| + // Clears the recovery cache if the static resetBrowserConfiguration flag is
|
| + // set.
|
| + Future<bool> resetConfiguration() {
|
| + if (!Browser.resetBrowserConfiguration) return new Future.value(true);
|
| var localAppData = Platform.environment['LOCALAPPDATA'];
|
|
|
| Directory dir = new Directory("$localAppData\\Microsoft\\"
|
| @@ -578,7 +600,7 @@ class IE extends Browser {
|
|
|
| Future<bool> start(String url) {
|
| _logEvent("Starting ie browser on: $url");
|
| - return clearCache().then((_) => getVersion()).then((version) {
|
| + return resetConfiguration().then((_) => getVersion()).then((version) {
|
| _logEvent("Got version: $version");
|
| return startBrowserProcess(_binary, [url]);
|
| });
|
| @@ -876,12 +898,12 @@ class BrowserTestRunner {
|
| static const Duration MIN_NONEMPTY_QUEUE_TIME = const Duration(seconds: 1);
|
|
|
| final Map configuration;
|
| - BrowserTestingServer testingServer;
|
| + final BrowserTestingServer testingServer;
|
|
|
| final String localIp;
|
| - String browserName;
|
| - int maxNumBrowsers;
|
| - bool checkedMode;
|
| + final String browserName;
|
| + final int maxNumBrowsers;
|
| + final bool checkedMode;
|
| int numBrowsers = 0;
|
| // Used to send back logs from the browser (start, stop etc)
|
| Function logger;
|
| @@ -928,27 +950,23 @@ class BrowserTestRunner {
|
| if (_currentStartingBrowserId == id) _currentStartingBrowserId = null;
|
| }
|
|
|
| - // If [browserName] doesn't support opening new windows, we use new iframes
|
| - // instead.
|
| - bool get useIframe =>
|
| - !Browser.BROWSERS_WITH_WINDOW_SUPPORT.contains(browserName);
|
| -
|
| - /// The optional testingServer parameter allows callers to pass in
|
| - /// a testing server with different behavior than the default
|
| - /// BrowserTestServer. The url handlers of the testingServer are
|
| - /// overwritten, so an existing handler can't be shared between instances.
|
| BrowserTestRunner(
|
| - this.configuration, this.localIp, this.browserName, this.maxNumBrowsers,
|
| - {BrowserTestingServer this.testingServer}) {
|
| - checkedMode = configuration['checked'];
|
| - if (browserName == 'ff') browserName = 'firefox';
|
| + Map configuration,
|
| + String localIp,
|
| + String browserName,
|
| + this.maxNumBrowsers)
|
| + : configuration = configuration,
|
| + localIp = localIp,
|
| + browserName = (browserName == 'ff') ? 'firefox' : browserName,
|
| + checkedMode = configuration['checked'],
|
| + testingServer = new BrowserTestingServer(
|
| + configuration, localIp,
|
| + Browser.requiresIframe(browserName),
|
| + Browser.requiresFocus(browserName)) {
|
| + testingServer.testRunner = this;
|
| }
|
|
|
| Future start() async {
|
| - if (testingServer == null) {
|
| - testingServer =
|
| - new BrowserTestingServer(configuration, localIp, useIframe);
|
| - }
|
| await testingServer.start();
|
| testingServer
|
| ..testDoneCallBack = handleResults
|
| @@ -1285,6 +1303,9 @@ class BrowserTestingServer {
|
| /// test
|
|
|
| final String localIp;
|
| + final bool useIframe;
|
| + final bool requiresFocus;
|
| + BrowserTestRunner testRunner;
|
|
|
| static const String driverPath = "/driver";
|
| static const String nextTestPath = "/next_test";
|
| @@ -1297,14 +1318,14 @@ class BrowserTestingServer {
|
| var testCount = 0;
|
| var errorReportingServer;
|
| bool underTermination = false;
|
| - bool useIframe = false;
|
|
|
| Function testDoneCallBack;
|
| Function testStatusUpdateCallBack;
|
| Function testStartedCallBack;
|
| Function nextTestCallBack;
|
|
|
| - BrowserTestingServer(this.configuration, this.localIp, this.useIframe);
|
| + BrowserTestingServer(
|
| + this.configuration, this.localIp, this.useIframe, this.requiresFocus);
|
|
|
| Future start() {
|
| var test_driver_error_port = configuration['test_driver_error_port'];
|
| @@ -1366,29 +1387,41 @@ class BrowserTestingServer {
|
| handleStarted(request, browserId(request, startedPath), testId(request));
|
| });
|
|
|
| - makeSendPageHandler(String prefix) => (HttpRequest request) {
|
| - noCache(request);
|
| - var textResponse = "";
|
| - if (prefix == driverPath) {
|
| - textResponse = getDriverPage(browserId(request, prefix));
|
| - request.response.headers.set('Content-Type', 'text/html');
|
| - }
|
| - if (prefix == nextTestPath) {
|
| - textResponse = getNextTest(browserId(request, prefix));
|
| - request.response.headers.set('Content-Type', 'text/plain');
|
| - }
|
| - request.response.write(textResponse);
|
| - request.listen((_) {}, onDone: request.response.close);
|
| - request.response.done.catchError((error) {
|
| - if (!underTermination) {
|
| - print("URI ${request.uri}");
|
| - print("Textresponse $textResponse");
|
| - throw "Error returning content to browser: $error";
|
| - }
|
| + void sendPageHandler(HttpRequest request) {
|
| + // Do NOT make this method async. We need to call catchError below
|
| + // synchronously to avoid unhandled asynchronous errors.
|
| + noCache(request);
|
| + Future<String> textResponse;
|
| + if (request.uri.path.startsWith(driverPath)) {
|
| + textResponse = getDriverPage(browserId(request, driverPath));
|
| + request.response.headers.set('Content-Type', 'text/html');
|
| + } else if (request.uri.path.startsWith(nextTestPath)) {
|
| + textResponse = new Future<String>.value(
|
| + getNextTest(browserId(request, nextTestPath)));
|
| + request.response.headers.set('Content-Type', 'text/plain');
|
| + } else {
|
| + textResponse = new Future<String>.value("");
|
| + }
|
| + request.response.done.catchError((error) {
|
| + if (!underTermination) {
|
| + return textResponse.then((String text) {
|
| + print("URI ${request.uri}");
|
| + print("textResponse $textResponse");
|
| + throw "Error returning content to browser: $error";
|
| });
|
| - };
|
| - server.addHandler(driverPath, makeSendPageHandler(driverPath));
|
| - server.addHandler(nextTestPath, makeSendPageHandler(nextTestPath));
|
| + }
|
| + });
|
| + textResponse.then((String text) async {
|
| + request.response.write(text);
|
| + await request.listen(null).asFuture();
|
| + // Ignoring the returned closure as it returns the 'done' future
|
| + // which alread has catchError installed above.
|
| + request.response.close();
|
| + });
|
| + }
|
| +
|
| + server.addHandler(driverPath, sendPageHandler);
|
| + server.addHandler(nextTestPath, sendPageHandler);
|
| }
|
|
|
| void handleReport(HttpRequest request, String browserId, var testId,
|
| @@ -1447,33 +1480,34 @@ class BrowserTestingServer {
|
| return "http://$localIp:$port/driver/$browserId";
|
| }
|
|
|
| - String getDriverPage(String browserId) {
|
| + Future<String> getDriverPage(String browserId) async {
|
| + await testRunner.browserStatus[browserId].browser.onDriverPageRequested();
|
| var errorReportingUrl =
|
| "http://$localIp:${errorReportingServer.port}/$browserId";
|
| String driverContent = """
|
| <!DOCTYPE html><html>
|
| <head>
|
| + <title>Driving page</title>
|
| <style>
|
| - body {
|
| - margin: 0;
|
| - }
|
| - .box {
|
| - overflow: hidden;
|
| - overflow-y: auto;
|
| - position: absolute;
|
| - left: 0;
|
| - right: 0;
|
| - }
|
| - .controller.box {
|
| - height: 75px;
|
| - top: 0;
|
| - }
|
| - .test.box {
|
| - top: 75px;
|
| - bottom: 0;
|
| - }
|
| +.big-notice {
|
| + background-color: red;
|
| + color: white;
|
| + font-weight: bold;
|
| + font-size: xx-large;
|
| + text-align: center;
|
| +}
|
| +.controller.box {
|
| + white-space: nowrap;
|
| + overflow: scroll;
|
| + height: 6em;
|
| +}
|
| +body {
|
| + font-family: sans-serif;
|
| +}
|
| +body div {
|
| + padding-top: 10px;
|
| +}
|
| </style>
|
| - <title>Driving page</title>
|
| <script type='text/javascript'>
|
| var STATUS_UPDATE_INTERVAL = 10000;
|
|
|
| @@ -1603,7 +1637,8 @@ class BrowserTestingServer {
|
| embedded_iframe_div.removeChild(embedded_iframe);
|
| embedded_iframe = document.createElement('iframe');
|
| embedded_iframe.id = "embedded_iframe";
|
| - embedded_iframe.style="width:100%;height:100%";
|
| + embedded_iframe.width='800px';
|
| + embedded_iframe.height='600px';
|
| embedded_iframe_div.appendChild(embedded_iframe);
|
| embedded_iframe.src = url;
|
| } else {
|
| @@ -1775,13 +1810,26 @@ class BrowserTestingServer {
|
| </script>
|
| </head>
|
| <body onload="startTesting()">
|
| +
|
| + <div class='big-notice'>
|
| + Please keep this window in focus at all times.
|
| + </div>
|
| +
|
| + <div>
|
| + Some browsers, Safari, in particular, may pause JavaScript when not
|
| + visible to conserve power consumption and CPU resources. In addition,
|
| + some tests of focus events will not work correctly if this window doesn't
|
| + have focus. It's also advisable to close any other programs that may open
|
| + modal dialogs, for example, Chrome with Calendar open.
|
| + </div>
|
| +
|
| <div class="controller box">
|
| Dart test driver, number of tests: <span id="number"></span><br>
|
| Currently executing: <span id="currently_executing"></span><br>
|
| Unhandled error: <span id="unhandled_error"></span>
|
| </div>
|
| <div id="embedded_iframe_div" class="test box">
|
| - <iframe style="width:100%;height:100%;" id="embedded_iframe"></iframe>
|
| + <iframe id="embedded_iframe"></iframe>
|
| </div>
|
| </body>
|
| </html>
|
|
|