blob: 52679e698e25474d9f4e9a83c0f473d9310ded79 [file] [log] [blame]
// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
library browser;
import "dart:async";
import "dart:core";
import "dart:io";
import 'android.dart';
import 'utils.dart';
/** Class describing the interface for communicating with browsers. */
abstract class Browser {
// Browsers actually takes a while to cleanup after itself when closing
// Give it sufficient time to do that.
static final Duration killRepeatInternal = const Duration(seconds: 10);
static final int killRetries = 5;
StringBuffer _stdout = new StringBuffer();
StringBuffer _stderr = new StringBuffer();
StringBuffer _usageLog = new StringBuffer();
// This function is called when the process is closed.
// This is extracted to an external function so that we can do additional
// functionality when the process closes (cleanup and call onExit)
Function _processClosed;
// This is called after the process is closed, after _processClosed has
// been called, but before onExit. Subclasses can use this to cleanup
// any browser specific resources (temp directories, profiles, etc)
Function _cleanup;
/** The version of the browser - normally set when starting a browser */
String version = "";
/**
* The underlying process - don't mess directly with this if you don't
* know what you are doing (this is an interactive process that needs
* special threatment to not leak).
*/
Process process;
Function logger;
/**
* Id of the browser
*/
String id;
/** Callback that will be executed when the browser has closed */
Function onClose;
/** Print everything (stdout, stderr, usageLog) whenever we add to it */
bool debugPrint = false;
// This future will be lazily set when calling close() and will complete once
// the process did exit.
Future browserTerminationFuture;
Browser();
factory Browser.byName(String name) {
if (name == 'ff' || name == 'firefox') {
return new Firefox();
} else if (name == 'chrome') {
return new Chrome();
} else if (name == 'safari') {
return new Safari();
} else {
throw "Non supported browser";
}
}
static const List<String> SUPPORTED_BROWSERS =
const ['safari', 'ff', 'firefox', 'chrome'];
static const List<String> BROWSERS_WITH_WINDOW_SUPPORT =
const ['safari', 'ff', 'firefox', 'chrome'];
// TODO(kustermann): add standard support for chrome on android
static bool supportedBrowser(String name) {
return SUPPORTED_BROWSERS.contains(name);
}
void _logEvent(String event) {
String toLog = "$this ($id) - ${new DateTime.now()}: $event \n";
if (debugPrint) print("usageLog: $toLog");
if (logger != null) logger(toLog);
_usageLog.write(toLog);
}
void _addStdout(String output) {
if (debugPrint) print("stdout: $output");
_stdout.write(output);
}
void _addStderr(String output) {
if (debugPrint) print("stderr: $output");
_stderr.write(output);
}
Future close() {
_logEvent("Close called on browser");
if (browserTerminationFuture == null) {
var completer = new Completer();
browserTerminationFuture = completer.future;
if (process != null) {
// Make sure we intercept onExit calls and complete.
_processClosed = () {
_processClosed = null;
process = null;
completer.complete(true);
};
if (process.kill(ProcessSignal.SIGKILL)) {
_logEvent("Successfully sent kill signal to process.");
} else {
_logEvent("Sending kill signal failed.");
}
} else {
_logEvent("The process is already dead.");
completer.complete(true);
}
}
return browserTerminationFuture;
}
/**
* Start the browser using the supplied argument.
* This sets up the error handling and usage logging.
*/
Future<bool> startBrowser(String command, List<String> arguments) {
return Process.start(command, arguments).then((startedProcess) {
process = startedProcess;
process.stdout.transform(new StringDecoder()).listen((data) {
_addStdout(data);
}, onError: (error) {
// This should _never_ happen, but we really want this in the log
// if it actually does due to dart:io or vm bug.
_logEvent("An error occured in the process stdout handling: $error");
});
process.stderr.transform(new StringDecoder()).listen((data) {
_addStderr(data);
}, onError: (error) {
// This should _never_ happen, but we really want this in the log
// if it actually does due to dart:io or vm bug.
_logEvent("An error occured in the process stderr handling: $error");
});
process.exitCode.then((exitCode) {
_logEvent("Browser closed with exitcode $exitCode");
if (_processClosed != null) _processClosed();
if (_cleanup != null) _cleanup();
if (onClose != null) onClose(exitCode);
});
return true;
}).catchError((error) {
_logEvent("Running $command $arguments failed with $error");
return false;
});
}
/**
* Get any stdout that the browser wrote during execution.
*/
String get stdout => _stdout.toString();
String get stderr => _stderr.toString();
String get usageLog => _usageLog.toString();
String toString();
/** Starts the browser loading the given url */
Future<bool> start(String url);
}
class Safari extends Browser {
/**
* The binary used to run safari - changing this can be nececcary for
* testing or using non standard safari installation.
*/
const String binary = "/Applications/Safari.app/Contents/MacOS/Safari";
/**
* We get the safari version by parsing a version file
*/
const String versionFile = "/Applications/Safari.app/Contents/version.plist";
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;
});
}
Future<String> getVersion() {
/**
* Example of the file:
* <?xml version="1.0" encoding="UTF-8"?>
* <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
* <plist version="1.0">
* <dict>
* <key>BuildVersion</key>
* <string>2</string>
* <key>CFBundleShortVersionString</key>
* <string>6.0.4</string>
* <key>CFBundleVersion</key>
* <string>8536.29.13</string>
* <key>ProjectName</key>
* <string>WebBrowser</string>
* <key>SourceVersion</key>
* <string>7536029013000000</string>
* </dict>
* </plist>
*/
File f = new File(versionFile);
return f.readAsLines().then((content) {
bool versionOnNextLine = false;
for (var line in content) {
if (versionOnNextLine) return line;
if (line.contains("CFBundleShortVersionString")) {
versionOnNextLine = true;
}
}
return null;
});
}
void _createLaunchHTML(var path, var url) {
var file = new File("${path}/launch.html");
var randomFile = file.openSync(FileMode.WRITE);
var content = '<script language="JavaScript">location = "$url"</script>';
randomFile.writeStringSync(content);
randomFile.close();
}
Future<bool> start(String url) {
_logEvent("Starting Safari browser on: $url");
// Get the version and log that.
return allowPopUps().then((success) {
if (!success) {
return new Future.immediate(false);
}
return getVersion().then((version) {
_logEvent("Got version: $version");
var args = ["'$url'"];
return new Directory('').createTemp().then((userDir) {
_cleanup = () { userDir.delete(recursive: true); };
_createLaunchHTML(userDir.path, url);
var args = ["${userDir.path}/launch.html"];
return startBrowser(binary, args);
});
}).catchError((e) {
_logEvent("Running $binary --version failed with $e");
return false;
});
});
}
String toString() => "Safari";
}
class Chrome extends Browser {
/**
* The binary used to run chrome - changing this can be nececcary for
* testing or using non standard chrome installation.
*/
const String binary = "google-chrome";
Future<bool> start(String url) {
_logEvent("Starting chrome browser on: $url");
// Get the version and log that.
return Process.run(binary, ["--version"]).then((var versionResult) {
if (versionResult.exitCode != 0) {
_logEvent("Failed to chrome get version");
_logEvent("Make sure $binary is a valid program for running chrome");
return new Future.immediate(false);
}
version = versionResult.stdout;
_logEvent("Got version: $version");
return new Directory('').createTemp().then((userDir) {
_cleanup = () { userDir.delete(recursive: true); };
var args = ["--user-data-dir=${userDir.path}", url,
"--disable-extensions", "--disable-popup-blocking",
"--bwsi", "--no-first-run"];
return startBrowser(binary, args);
});
}).catchError((e) {
_logEvent("Running $binary --version failed with $e");
return false;
});
}
String toString() => "Chrome";
}
class AndroidChrome extends Browser {
const String viewAction = 'android.intent.action.VIEW';
const String mainAction = 'android.intent.action.MAIN';
const String chromePackage = 'com.android.chrome';
const String browserPackage = 'com.android.browser';
const String firefoxPackage = 'org.mozilla.firefox';
const String turnScreenOnPackage = 'com.google.dart.turnscreenon';
AndroidEmulator _emulator;
AdbDevice _adbDevice;
AndroidChrome(this._adbDevice);
Future<bool> start(String url) {
var browserIntent = new Intent(
viewAction, browserPackage, '.BrowserActivity', url);
var chromeIntent = new Intent(viewAction, chromePackage, '.Main', url);
var firefoxIntent = new Intent(viewAction, firefoxPackage, '.App', url);
var turnScreenOnIntent =
new Intent(mainAction, turnScreenOnPackage, '.Main');
var testing_resources_dir =
new Path('third_party/android_testing_resources');
if (!new Directory.fromPath(testing_resources_dir).existsSync()) {
DebugLogger.error("$testing_resources_dir doesn't exist. Exiting now.");
exit(1);
}
var chromeAPK = testing_resources_dir.append('com.android.chrome-1.apk');
var turnScreenOnAPK = testing_resources_dir.append('TurnScreenOn.apk');
var chromeConfDir = testing_resources_dir.append('chrome_configuration');
var chromeConfDirRemote = new Path('/data/user/0/com.android.chrome/');
return _adbDevice.waitForBootCompleted().then((_) {
return _adbDevice.forceStop(chromeIntent.package);
}).then((_) {
return _adbDevice.killAll();
}).then((_) {
return _adbDevice.adbRoot();
}).then((_) {
return _adbDevice.installApk(turnScreenOnAPK);
}).then((_) {
return _adbDevice.installApk(chromeAPK);
}).then((_) {
return _adbDevice.pushData(chromeConfDir, chromeConfDirRemote);
}).then((_) {
return _adbDevice.chmod('777', chromeConfDirRemote);
}).then((_) {
return _adbDevice.startActivity(turnScreenOnIntent).then((_) => true);
}).then((_) {
return _adbDevice.startActivity(chromeIntent).then((_) => true);
});
}
Future<bool> close() {
if (_adbDevice != null) {
return _adbDevice.forceStop(chromePackage).then((_) {
return _adbDevice.killAll().then((_) => true);
});
}
return new Future.immediate(true);
}
String toString() => "chromeOnAndroid";
}
class Firefox extends Browser {
/**
* The binary used to run firefox - changing this can be nececcary for
* testing or using non standard firefox installation.
*/
const String binary = "firefox";
const String enablePopUp =
'user_pref("dom.disable_open_during_load", false);';
const String disableDefaultCheck =
'user_pref("browser.shell.checkDefaultBrowser", false);';
Future _createPreferenceFile(var path) {
var file = new File("${path.toString()}/user.js");
var randomFile = file.openSync(FileMode.WRITE);
randomFile.writeStringSync(enablePopUp);
randomFile.writeStringSync(disableDefaultCheck);
randomFile.close();
}
Future<bool> start(String url) {
_logEvent("Starting firefox browser on: $url");
// Get the version and log that.
return Process.run(binary, ["--version"]).then((var versionResult) {
if (versionResult.exitCode != 0) {
_logEvent("Failed to firefox get version");
_logEvent("Make sure $binary is a valid program for running firefox");
return new Future.immediate(false);
}
version = versionResult.stdout;
_logEvent("Got version: $version");
return new Directory('').createTemp().then((userDir) {
_createPreferenceFile(userDir.path);
_cleanup = () { userDir.delete(recursive: true); };
var args = ["-profile", "${userDir.path}",
"-no-remote", "-new-instance", url];
return startBrowser(binary, args);
});
}).catchError((e) {
_logEvent("Running $binary --version failed with $e");
return false;
});
}
String toString() => "Firefox";
}
/**
* Describes the current state of a browser used for testing.
*/
class BrowserTestingStatus {
// TODO(ricow): Add prefetching to the browsers. We spend a lot of time waiting
// for the next test. Handling timeouts is the hard part of this!
Browser browser;
BrowserTest currentTest;
// This is currently not used for anything except for error reporting.
// Given the usefulness of this in debugging issues this should not be
// removed even when we have really stable system.
BrowserTest lastTest;
bool timeout = false;
BrowserTestingStatus(Browser this.browser);
}
/**
* Describes a single test to be run int the browser.
*/
class BrowserTest {
// TODO(ricow): Add timeout callback instead of the string passing hack.
Function doneCallback;
String url;
int timeout;
// We store this here for easy access when tests time out (instead of
// capturing this in a closure)
Timer timeoutTimer;
// Used for debugging, this is simply a unique identifier assigned to each
// test.
int id;
static int _idCounter = 0;
BrowserTest(this.url, this.doneCallback, this.timeout) {
id = _idCounter++;
}
}
/**
* Encapsulates all the functionality for running tests in browsers.
* The interface is rather simple. After starting the runner tests
* are simply added to the queue and a the supplied callbacks are called
* whenever a test completes.
*/
class BrowserTestRunner {
String local_ip;
String browserName;
int maxNumBrowsers;
// Used to send back logs from the browser (start, stop etc)
Function logger;
bool underTermination = false;
List<BrowserTest> testQueue = new List<BrowserTest>();
Map<String, BrowserTestingStatus> browserStatus =
new Map<String, BrowserTestingStatus>();
var adbDeviceMapping = new Map<String, AdbDevice>();
// This cache is used to guarantee that we never see double reporting.
// If we do we need to provide developers with this information.
// We don't add urls to the cache until we have run it.
Map<int, String> testCache = new Map<int, String>();
List<int> doubleReportingTests = new List<int>();
BrowserTestingServer testingServer;
BrowserTestRunner(this.local_ip, this.browserName, this.maxNumBrowsers);
Future<bool> start() {
// If [browserName] doesn't support opening new windows, we use new iframes
// instead.
bool useIframe =
!Browser.BROWSERS_WITH_WINDOW_SUPPORT.contains(browserName);
testingServer = new BrowserTestingServer(local_ip, useIframe);
return testingServer.start().then((_) {
testingServer.testDoneCallBack = handleResults;
testingServer.nextTestCallBack = getNextTest;
return getBrowsers().then((browsers) {
var futures = [];
for (var browser in browsers) {
var url = testingServer.getDriverUrl(browser.id);
var future = browser.start(url).then((success) {
if (success) {
browserStatus[browser.id] = new BrowserTestingStatus(browser);
}
return success;
});
futures.add(future);
}
return Future.wait(futures).then((values) {
return !values.contains(false);
});
});
});
}
Future<List<Browser>> getBrowsers() {
// TODO(kustermann): This is a hackisch way to accomplish it and should
// be encapsulated
var browsersCompleter = new Completer();
if (browserName == 'chromeOnAndroid') {
AdbHelper.listDevices().then((deviceIds) {
if (deviceIds.length > 0) {
var browsers = [];
for (int i = 0; i < deviceIds.length; i++) {
var id = "BROWSER$i";
var device = new AdbDevice(deviceIds[i]);
adbDeviceMapping[id] = device;
var browser = new AndroidChrome(device);
browsers.add(browser);
// We store this in case we need to kill the browser.
browser.id = id;
}
browsersCompleter.complete(browsers);
} else {
throw new StateError("No android devices found.");
}
});
} else {
var browsers = [];
for (int i = 0; i < maxNumBrowsers; i++) {
var id = "BROWSER$i";
var browser = getInstance();
browsers.add(browser);
// We store this in case we need to kill the browser.
browser.id = id;
}
browsersCompleter.complete(browsers);
}
return browsersCompleter.future;
}
var timedOut = [];
void handleResults(String browserId, String output, int testId) {
var status = browserStatus[browserId];
if (testCache.containsKey(testId)) {
doubleReportingTests.add(testId);
return;
}
if (status.timeout) {
// We don't do anything, this browser is currently being killed and
// replaced.
} else if (status.currentTest != null) {
status.currentTest.timeoutTimer.cancel();
if (status.currentTest.id != testId) {
print("Expected test id ${status.currentTest.id} for"
"${status.currentTest.url}");
print("Got test id ${testId}");
print("Last test id was ${status.lastTest.id} for "
"${status.currentTest.url}");
throw("This should never happen, wrong test id");
}
testCache[testId] = status.currentTest.url;
status.currentTest.doneCallback(output);
status.lastTest = status.currentTest;
status.currentTest = null;
} else {
print("\nThis is bad, should never happen, handleResult no test");
print("URL: ${status.lastTest.url}");
print(output);
terminate().then((_) {
exit(1);
});
}
}
void handleTimeout(BrowserTestingStatus status) {
// We simply kill the browser and starts up a new one!
// We could be smarter here, but it does not seems like it is worth it.
status.timeout = true;
timedOut.add(status.currentTest.url);
var id = status.browser.id;
status.browser.close().then((closed) {
if (!closed) {
// Very bad, we could not kill the browser.
print("could not kill browser $id");
return;
}
// We don't want to start a new browser if we are terminating.
if (underTermination) return;
var browser;
if (browserName == 'chromeOnAndroid') {
browser = new AndroidChrome(adbDeviceMapping[id]);
} else {
browser = getInstance();
}
browser.start(testingServer.getDriverUrl(id)).then((success) {
// We may have started terminating in the mean time.
if (underTermination) {
browser.close().then((success) {
// We should never hit this, print it out.
if (!success) {
print("Could not kill browser ($id) started due to timeout");
}
});
return;
}
if (success) {
browser.id = id;
status.browser = browser;
status.timeout = false;
} else {
// TODO(ricow): Handle this better.
print("This is bad, should never happen, could not start browser");
exit(1);
}
});
});
status.currentTest.doneCallback("TIMEOUT");
status.currentTest = null;
}
BrowserTest getNextTest(String browserId) {
if (testQueue.isEmpty) return null;
var status = browserStatus[browserId];
if (status == null) return null;
// We are currently terminating this browser, don't start a new test.
if (status.timeout) return null;
BrowserTest test = testQueue.removeLast();
if (status.currentTest == null) {
status.currentTest = test;
} else {
// TODO(ricow): Handle this better.
print("This is bad, should never happen, getNextTest all full");
print("Old test was: ${status.currentTest.url}");
print("Timed out tests:");
for (var v in timedOut) {
print(" $v");
}
exit(1);
}
Timer timer = new Timer(new Duration(seconds: test.timeout),
() { handleTimeout(status); });
status.currentTest.timeoutTimer = timer;
return test;
}
void queueTest(BrowserTest test) {
testQueue.add(test);
}
void printDoubleReportingTests() {
if (doubleReportingTests.length == 0) return;
// TODO(ricow): die on double reporting.
// Currently we just report this here, we could have a callback to the
// encapsulating environment.
print("");
print("Double reporting tests");
for (var id in doubleReportingTests) {
print(" ${testCache[id]}");
}
}
Future<bool> terminate() {
var futures = [];
underTermination = true;
testingServer.underTermination = true;
for (BrowserTestingStatus status in browserStatus.values) {
futures.add(status.browser.close());
}
return Future.wait(futures).then((values) {
testingServer.httpServer.close();
printDoubleReportingTests();
return !values.contains(false);
});
}
Browser getInstance() {
var browser = new Browser.byName(browserName);
browser.logger = logger;
return browser;
}
}
class BrowserTestingServer {
/// Interface of the testing server:
///
/// GET /driver/BROWSER_ID -- This will get the driver page to fetch
/// and run tests ...
/// GET /next_test/BROWSER_ID -- returns "WAIT" "TERMINATE" or "url#id"
/// where url is the test to run, and id is the id of the test.
/// If there are currently no available tests the waitSignal is send
/// back. If we are in the process of terminating the terminateSignal
/// is send back and the browser will stop requesting new tasks.
/// POST /report/BROWSER_ID?id=NUM -- sends back the dom of the executed
/// test
final String local_ip;
const String driverPath = "/driver";
const String nextTestPath = "/next_test";
const String reportPath = "/report";
const String waitSignal = "WAIT";
const String terminateSignal = "TERMINATE";
var testCount = 0;
var httpServer;
bool underTermination = false;
bool useIframe = false;
Function testDoneCallBack;
Function nextTestCallBack;
BrowserTestingServer(this.local_ip, this.useIframe);
Future start() {
return HttpServer.bind(local_ip, 0).then((createdServer) {
httpServer = createdServer;
void handler(HttpRequest request) {
if (request.uri.path.startsWith(reportPath)) {
var browserId = request.uri.path.substring(reportPath.length + 1);
var testId = int.parse(request.queryParameters["id"].split("=")[1]);
handleReport(request, browserId, testId);
// handleReport will asynchroniously fetch the data and will handle
// the closing of the streams.
return;
}
var textResponse = "";
if (request.uri.path.startsWith(driverPath)) {
var browserId = request.uri.path.substring(driverPath.length + 1);
textResponse = getDriverPage(browserId);
} else if (request.uri.path.startsWith(nextTestPath)) {
var browserId = request.uri.path.substring(nextTestPath.length + 1);
textResponse = getNextTest(browserId);
} else {
// We silently ignore other requests.
}
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 errorHandler(e) {
if (!underTermination) print("Error occured in httpserver: $e");
};
httpServer.listen(handler, onError: errorHandler);
return true;
});
}
void handleReport(HttpRequest request, String browserId, var testId) {
StringBuffer buffer = new StringBuffer();
request.transform(new StringDecoder()).listen((data) {
buffer.write(data);
}, onDone: () {
String back = buffer.toString();
request.response.close();
testDoneCallBack(browserId, back, testId);
}, onError: (error) { print(error); });
}
String getNextTest(String browserId) {
var nextTest = nextTestCallBack(browserId);
if (underTermination) {
// Browsers will be killed shortly, send them a terminate signal so
// that they stop pulling.
return terminateSignal;
} else if (nextTest == null) {
// We don't currently have any tests ready for consumption, wait.
return waitSignal;
} else {
return "${nextTest.url}#id=${nextTest.id}";
}
}
String getDriverUrl(String browserId) {
if (httpServer == null) {
print("Bad browser testing server, you are not started yet. Can't "
"produce driver url");
exit(1);
// This should never happen - exit immediately;
}
return "http://$local_ip:${httpServer.port}/driver/$browserId";
}
String getDriverPage(String browserId) {
String driverContent = """
<!DOCTYPE html><html>
<head>
<title>Driving page</title>
<script type='text/javascript'>
function startTesting() {
var number_of_tests = 0;
var current_id;
var last_reported_id;
var testing_window;
// We use this to determine if we did actually get back a start event
// from the test we just loaded.
var did_start = false;
var embedded_iframe = document.getElementById('embedded_iframe');
var use_iframe = ${useIframe};
function newTaskHandler() {
if (this.readyState == this.DONE) {
if (this.status == 200) {
if (this.responseText == '$waitSignal') {
setTimeout(getNextTask, 500);
} else if (this.responseText == '$terminateSignal') {
// Don't do anything, we will be killed shortly.
} else {
// TODO(ricow): Do something more clever here.
if (nextTask != undefined) alert('This is really bad');
// The task is send to us as:
// URL#ID
var split = this.responseText.split('#');
var nextTask = split[0];
current_id = split[1];
did_start = false;
run(nextTask);
}
} else {
// We are basically in trouble - do something clever.
}
}
}
function getNextTask() {
var client = new XMLHttpRequest();
client.onreadystatechange = newTaskHandler;
client.open('GET', '$nextTestPath/$browserId');
client.send();
}
function run(url) {
number_of_tests++;
document.getElementById('number').innerHTML = number_of_tests;
if (use_iframe) {
embedded_iframe.src = url;
} else {
if (testing_window == undefined) {
testing_window = window.open(url);
} else {
testing_window.location = url;
}
}
}
function reportMessage(msg) {
if (msg == 'STARTING') {
did_start = true;
return;
}
var client = new XMLHttpRequest();
function handleReady() {
if (this.readyState == this.DONE) {
if (last_reported_id != current_id && did_start) {
getNextTask();
last_reported_id = current_id;
}
}
}
client.onreadystatechange = handleReady;
// If did_start is false it means that we did actually set the url on
// the testing_window, but this is a report left in the event loop or
// a callback because the page did not load yet.
// In both cases this is a double report from the last test.
var posting_id = did_start ? current_id : last_reported_id;
client.open('POST', '$reportPath/${browserId}?id=' + posting_id);
client.setRequestHeader('Content-type',
'application/x-www-form-urlencoded');
client.send(msg);
// TODO(ricow) add error handling to somehow report the fact that
// we could not send back a result.
}
function messageHandler(e) {
var msg = e.data;
if (typeof msg != 'string') return;
reportMessage(msg);
}
window.addEventListener('message', messageHandler, false);
waitForDone = false;
getNextTask();
}
</script>
</head>
<body onload="startTesting()">
Dart test driver, number of tests: <div id="number"></div>
<iframe id="embedded_iframe"></iframe>
</body>
</html>
""";
return driverContent;
}
}