| // 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. | |
| import 'dart:async'; | |
| import 'dart:convert'; | |
| import 'dart:io'; | |
| import 'package:webdriver/async_io.dart'; | |
| import 'android.dart'; | |
| import 'configuration.dart'; | |
| import 'path.dart'; | |
| import 'service/web_driver_service.dart'; | |
| import 'utils.dart'; | |
| typedef BrowserDoneCallback = void Function(BrowserTestOutput output); | |
| typedef TestChangedCallback = | |
| void Function(String browserId, String output, int testId); | |
| typedef NextTestCallback = BrowserTest? Function(String browserId); | |
| class BrowserOutput { | |
| final StringBuffer stdout = StringBuffer(); | |
| final StringBuffer stderr = StringBuffer(); | |
| final StringBuffer eventLog = StringBuffer(); | |
| } | |
| /// Class describing the interface for communicating with browsers. | |
| abstract class Browser { | |
| static int _browserIdCounter = 1; | |
| static String _nextBrowserId() => "BROWSER${_browserIdCounter++}"; | |
| /// Get the output that was written so far to stdout/stderr/eventLog. | |
| BrowserOutput get testBrowserOutput => _testBrowserOutput; | |
| BrowserOutput _testBrowserOutput = BrowserOutput(); | |
| /// This is called after the process is closed, before the done future | |
| /// is completed. | |
| /// | |
| /// Subclasses can use this to cleanup any browser specific resources | |
| /// (temp directories, profiles, etc). The function is expected to do | |
| /// it's work synchronously. | |
| void Function()? _cleanup; | |
| /// The version of the browser - normally set when starting a browser | |
| Future<String> get 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 treatment to not leak). | |
| Process? process; | |
| void Function(String)? logger; | |
| /// Id of the browser. | |
| final String id = _nextBrowserId(); | |
| /// Reset the browser to a known configuration on start-up. | |
| /// Browser specific implementations are free to ignore this. | |
| static bool resetBrowserConfiguration = false; | |
| /// Print everything (stdout, stderr, usageLog) whenever we add to it | |
| bool debugPrint = false; | |
| /// This future returns when the process exits. It is also the return value | |
| /// of close() | |
| Future<bool>? done; | |
| Browser(); | |
| static Browser fromConfiguration(TestConfiguration configuration) { | |
| Browser browser; | |
| switch (configuration.runtime) { | |
| case Runtime.firefox: | |
| browser = Firefox( | |
| configuration.browserLocation, | |
| headless: configuration.headless, | |
| ); | |
| break; | |
| case Runtime.chrome: | |
| browser = Chrome( | |
| configuration.browserLocation, | |
| noSandbox: configuration.noSandbox, | |
| headless: configuration.headless, | |
| ); | |
| break; | |
| case Runtime.safari: | |
| var service = WebDriverService.fromRuntime(Runtime.safari); | |
| browser = Safari(service.port); | |
| break; | |
| default: | |
| throw "unreachable"; | |
| } | |
| return browser; | |
| } | |
| static const List<String> supportedBrowsers = [ | |
| 'safari', | |
| 'ff', | |
| 'firefox', | |
| 'chrome', | |
| ]; | |
| static bool requiresFocus(String browserName) { | |
| return browserName == "safari"; | |
| } | |
| // TODO(kustermann): add standard support for chrome on android | |
| static bool supportedBrowser(String name) { | |
| return supportedBrowsers.contains(name); | |
| } | |
| void _logEvent(String event) { | |
| var toLog = "$this ($id) - $event \n"; | |
| if (debugPrint) print("usageLog: $toLog"); | |
| logger?.call(toLog); | |
| _testBrowserOutput.eventLog.write(toLog); | |
| } | |
| void _addStdout(String output) { | |
| if (debugPrint) print("stdout: $output"); | |
| _testBrowserOutput.stdout.write(output); | |
| } | |
| void _addStderr(String output) { | |
| if (debugPrint) print("stderr: $output"); | |
| _testBrowserOutput.stderr.write(output); | |
| } | |
| Future<bool> close() { | |
| _logEvent("Close called on browser"); | |
| if (process != null) { | |
| if (process!.kill(ProcessSignal.sigkill)) { | |
| _logEvent("Successfully sent kill signal to process."); | |
| } else { | |
| _logEvent("Sending kill signal failed."); | |
| } | |
| return done ?? Future.value(true); | |
| } else { | |
| _logEvent("The process is already dead."); | |
| return Future.value(true); | |
| } | |
| } | |
| /// Start the browser using the supplied argument. | |
| /// This sets up the error handling and usage logging. | |
| Future<bool> startBrowserProcess( | |
| String command, | |
| List<String> arguments, { | |
| Map<String, String>? environment, | |
| }) { | |
| return startProcess(command, arguments, environment: environment) | |
| .then((startedProcess) { | |
| _logEvent("Started browser using $command ${arguments.join(' ')}"); | |
| process = startedProcess; | |
| // Used to notify when exiting, and as a return value on calls to | |
| // close(). | |
| var doneCompleter = Completer<bool>(); | |
| done = doneCompleter.future; | |
| var stdoutDone = Completer<void>(); | |
| var stderrDone = Completer<void>(); | |
| var stdoutIsDone = false; | |
| var stderrIsDone = false; | |
| StreamSubscription stdoutSubscription; | |
| StreamSubscription stderrSubscription; | |
| // This timer is used to close stdio to the subprocess once we got | |
| // the exitCode. Sometimes descendants of the subprocess keep stdio | |
| // handles alive even though the direct subprocess is dead. | |
| Timer? watchdogTimer; | |
| void closeStdout([_]) { | |
| if (!stdoutIsDone) { | |
| stdoutDone.complete(); | |
| stdoutIsDone = true; | |
| if (stderrIsDone) { | |
| watchdogTimer?.cancel(); | |
| } | |
| } | |
| } | |
| void closeStderr([_]) { | |
| if (!stderrIsDone) { | |
| stderrDone.complete(); | |
| stderrIsDone = true; | |
| if (stdoutIsDone) { | |
| watchdogTimer?.cancel(); | |
| } | |
| } | |
| } | |
| stdoutSubscription = process!.stdout | |
| .transform(utf8.decoder) | |
| .listen( | |
| _addStdout, | |
| 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 occurred in the process stdout handling: $error", | |
| ); | |
| }, | |
| onDone: closeStdout, | |
| ); | |
| stderrSubscription = process!.stderr | |
| .transform(utf8.decoder) | |
| .listen( | |
| _addStderr, | |
| 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 occurred in the process stderr handling: $error", | |
| ); | |
| }, | |
| onDone: closeStderr, | |
| ); | |
| process!.exitCode.then((exitCode) { | |
| _logEvent("Browser closed with exitcode $exitCode"); | |
| if (!stdoutIsDone || !stderrIsDone) { | |
| watchdogTimer = Timer(maxStdioDelay, () { | |
| DebugLogger.warning( | |
| "$maxStdioDelayPassedMessage (browser: $this)", | |
| ); | |
| watchdogTimer = null; | |
| stdoutSubscription.cancel(); | |
| stderrSubscription.cancel(); | |
| closeStdout(); | |
| closeStderr(); | |
| }); | |
| } | |
| Future.wait([stdoutDone.future, stderrDone.future]) | |
| .then((_) { | |
| process = null; | |
| _cleanup?.call(); | |
| }) | |
| .catchError((error) { | |
| _logEvent("Error closing browsers: $error"); | |
| }) | |
| .whenComplete(() => doneCompleter.complete(true)); | |
| }); | |
| return true; | |
| }) | |
| .catchError((error) { | |
| _logEvent("Running $command $arguments failed with $error"); | |
| return false; | |
| }); | |
| } | |
| void resetTestBrowserOutput() { | |
| _testBrowserOutput = BrowserOutput(); | |
| } | |
| /// Add useful info about the browser to the _testBrowserOutput.stdout, | |
| /// where it will be reported for failing tests. Used to report which | |
| /// android device a failing test is running on. | |
| void logBrowserInfoToTestBrowserOutput() {} | |
| /// Starts the browser loading the given url | |
| Future<bool> start(String url); | |
| @override | |
| String toString() => '$runtimeType'; | |
| } | |
| abstract class WebDriverBrowser extends Browser { | |
| WebDriver? _driver; | |
| final int _port; | |
| final Map<String, dynamic> _desiredCapabilities; | |
| bool _terminated = false; | |
| WebDriverBrowser(this._port, this._desiredCapabilities); | |
| @override | |
| Future<bool> start(String url) async { | |
| _logEvent('Starting $this browser on: $url'); | |
| await _createDriver(); | |
| if (_terminated) return false; | |
| await _driver!.get(url); | |
| try { | |
| _logEvent('Got version: ${await version}'); | |
| } catch (error) { | |
| _logEvent('Failed to get version.\nError: $error'); | |
| return false; | |
| } | |
| return true; | |
| } | |
| Future<void> _createDriver() async { | |
| for (var i = 5; i >= 0; i--) { | |
| // Give the driver process some time to be ready to accept connections. | |
| await Future.delayed(const Duration(seconds: 1)); | |
| if (_terminated) return; | |
| try { | |
| _driver = await createDriver( | |
| uri: Uri.parse('http://localhost:$_port/'), | |
| desired: _desiredCapabilities, | |
| ); | |
| } catch (error) { | |
| if (_terminated) return; | |
| if (i > 0) { | |
| _logEvent( | |
| 'Failed to create driver ($i retries left).\nError: $error', | |
| ); | |
| } else { | |
| _logEvent('Failed to create driver.\nError: $error'); | |
| await close(); | |
| rethrow; | |
| } | |
| } | |
| if (_driver != null) break; | |
| } | |
| } | |
| @override | |
| Future<bool> close() async { | |
| _terminated = true; | |
| await _driver?.quit(); | |
| // Give the driver process some time to be quit the browser. | |
| return true; | |
| } | |
| } | |
| class Safari extends WebDriverBrowser { | |
| /// We get the safari version by parsing a version file | |
| static const versionFile = '/Applications/Safari.app/Contents/version.plist'; | |
| Safari(int port) : super(port, {'browserName': 'safari'}); | |
| @override | |
| Future<String> get version async { | |
| // 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> | |
| final versionLine = (await File(versionFile).readAsLines()) | |
| .skipWhile((line) => !line.contains("CFBundleShortVersionString")) | |
| .skip(1) | |
| .take(1); | |
| return versionLine.isEmpty ? 'unknown' : versionLine.first; | |
| } | |
| } | |
| class Chrome extends Browser { | |
| Chrome(this._binary, {this.noSandbox = false, this.headless = true}); | |
| final String _binary; | |
| final bool noSandbox; | |
| final bool headless; | |
| @override | |
| Future<String> get version async { | |
| if (Platform.isWindows) { | |
| // The version flag does not work on windows. | |
| // See issue: | |
| // https://code.google.com/p/chromium/issues/detail?id=158372 | |
| // The registry hack does not seem to work. | |
| return "unknown on windows"; | |
| } | |
| final result = await Process.run(_binary, ["--version"]); | |
| if (result.exitCode != 0) { | |
| _logEvent("Failed to get chrome version"); | |
| _logEvent("Make sure $_binary is a valid program for running chrome"); | |
| throw StateError( | |
| "Failed to get chrome version.\nExit code: ${result.exitCode}", | |
| ); | |
| } | |
| return result.stdout as String; | |
| } | |
| @override | |
| Future<bool> start(String url) async { | |
| _logEvent("Starting chrome browser on: $url"); | |
| if (!await File(_binary).exists()) { | |
| _logEvent("Chrome binary not available."); | |
| _logEvent("Make sure $_binary is a valid program for running chrome"); | |
| return false; | |
| } | |
| try { | |
| _logEvent("Got version: ${await version}"); | |
| final userDir = await Directory.systemTemp.createTemp(); | |
| _cleanup = () { | |
| try { | |
| userDir.deleteSync(recursive: true); | |
| } catch (e) { | |
| _logEvent( | |
| "Error: failed to delete Chrome user-data-dir ${userDir.path}, " | |
| "will try again in 40 seconds: $e", | |
| ); | |
| Timer(const Duration(seconds: 40), () { | |
| try { | |
| userDir.deleteSync(recursive: true); | |
| } catch (e) { | |
| _logEvent( | |
| "Error: failed on second attempt to delete Chrome " | |
| "user-data-dir ${userDir.path}: $e", | |
| ); | |
| } | |
| }); | |
| } | |
| }; | |
| var args = [ | |
| if (noSandbox) "--no-sandbox", | |
| if (headless) "--headless", | |
| "--bwsi", | |
| "--disable-component-update", | |
| "--disable-extensions", | |
| "--disable-popup-blocking", | |
| "--disable-features=IntensiveWakeUpThrottling", | |
| "--disable-blink-features=TimerThrottlingForBackgroundTabs", | |
| "--no-first-run", | |
| "--use-mock-keychain", | |
| "--password-store=basic", | |
| "--user-data-dir=${userDir.path}", | |
| url, | |
| ]; | |
| // TODO(rnystrom): Uncomment this to open the dev tools tab when Chrome | |
| // is spawned. Handy for debugging tests. | |
| // args.add("--auto-open-devtools-for-tabs"); | |
| return await startBrowserProcess(_binary, args); | |
| } catch (e) { | |
| _logEvent("Starting chrome failed with $e"); | |
| return false; | |
| } | |
| } | |
| } | |
| class AndroidChrome extends Browser { | |
| static const String viewAction = 'android.intent.action.VIEW'; | |
| static const String mainAction = 'android.intent.action.MAIN'; | |
| static const String chromePackage = 'com.android.chrome'; | |
| static const String chromeActivity = '.Main'; | |
| static const String browserPackage = 'com.android.browser'; | |
| static const String browserActivity = '.BrowserActivity'; | |
| static const String firefoxPackage = 'org.mozilla.firefox'; | |
| static const String firefoxActivity = '.App'; | |
| static const String turnScreenOnPackage = 'com.google.dart.turnscreenon'; | |
| static const String turnScreenOnActivity = '.Main'; | |
| final AdbDevice _adbDevice; | |
| AndroidChrome(this._adbDevice); | |
| @override | |
| Future<bool> start(String url) { | |
| var chromeIntent = Intent(viewAction, chromePackage, chromeActivity, url); | |
| var turnScreenOnIntent = Intent( | |
| mainAction, | |
| turnScreenOnPackage, | |
| turnScreenOnActivity, | |
| ); | |
| var testingResourcesDir = Path('third_party/android_testing_resources'); | |
| if (!Directory(testingResourcesDir.toNativePath()).existsSync()) { | |
| DebugLogger.error("$testingResourcesDir doesn't exist. Exiting now."); | |
| exit(1); | |
| } | |
| var chromeAPK = testingResourcesDir.append('com.android.chrome-1.apk'); | |
| var turnScreenOnAPK = testingResourcesDir.append('TurnScreenOn.apk'); | |
| var chromeConfDir = testingResourcesDir.append('chrome_configuration'); | |
| var chromeConfDirRemote = 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); | |
| }); | |
| } | |
| @override | |
| Future<bool> close() async { | |
| await _adbDevice.forceStop(chromePackage); | |
| await _adbDevice.killAll(); | |
| return true; | |
| } | |
| @override | |
| void logBrowserInfoToTestBrowserOutput() { | |
| _testBrowserOutput.stdout.write( | |
| 'Android device id: ${_adbDevice.deviceId}\n', | |
| ); | |
| } | |
| @override | |
| final Future<String> version = Future.value('unknown'); | |
| @override | |
| String toString() => "chromeOnAndroid"; | |
| } | |
| class Firefox extends Browser { | |
| Firefox(this._binary, {this.headless = true}); | |
| final String _binary; | |
| final bool headless; | |
| static const String enablePopUp = | |
| 'user_pref("dom.disable_open_during_load", false);'; | |
| static const String disableDefaultCheck = | |
| 'user_pref("browser.shell.checkDefaultBrowser", false);'; | |
| static const String disableScriptTimeLimit = | |
| 'user_pref("dom.max_script_run_time", 0);'; | |
| static const String disableAutoUpdate = | |
| 'user_pref("app.update.auto", false);'; | |
| static const String skipTermsOfUse = | |
| 'user_pref("termsofuse.bypassNotification", true);'; | |
| void _createPreferenceFile(String path) { | |
| var file = File("$path/user.js"); | |
| var randomFile = file.openSync(mode: FileMode.write); | |
| randomFile.writeStringSync(enablePopUp); | |
| randomFile.writeStringSync(disableDefaultCheck); | |
| randomFile.writeStringSync(disableScriptTimeLimit); | |
| randomFile.writeStringSync(disableAutoUpdate); | |
| randomFile.writeStringSync(skipTermsOfUse); | |
| randomFile.close(); | |
| } | |
| @override | |
| Future<String> get version async { | |
| final result = await Process.run(_binary, ["--version"]); | |
| if (result.exitCode != 0) { | |
| _logEvent("Failed to get firefox version"); | |
| _logEvent("Make sure $_binary is a valid program for running firefox"); | |
| throw StateError( | |
| "Failed to get firefox version.\nExit code: ${result.exitCode}", | |
| ); | |
| } | |
| return result.stdout as String; | |
| } | |
| @override | |
| Future<bool> start(String url) async { | |
| _logEvent("Starting firefox browser on: $url"); | |
| try { | |
| _logEvent("Got version: ${await version}"); | |
| final userDir = await Directory.systemTemp.createTemp(); | |
| _createPreferenceFile(userDir.path); | |
| _cleanup = () { | |
| userDir.deleteSync(recursive: true); | |
| }; | |
| var args = [ | |
| if (headless) "-headless", | |
| "-profile", | |
| userDir.path, | |
| "-no-remote", | |
| "-new-instance", | |
| url, | |
| ]; | |
| var environment = Map<String, String>.from(Platform.environment); | |
| environment["MOZ_CRASHREPORTER_DISABLE"] = "1"; | |
| return await startBrowserProcess(_binary, args, environment: environment); | |
| } catch (e) { | |
| _logEvent("Starting firefox failed with $e"); | |
| return false; | |
| } | |
| } | |
| } | |
| /// Describes the current state of a browser used for testing. | |
| class BrowserStatus { | |
| 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 a really stable system. | |
| BrowserTest? lastTest; | |
| bool timeout = false; | |
| Timer? nextTestTimeout; | |
| Stopwatch timeSinceRestart = Stopwatch()..start(); | |
| BrowserStatus(this.browser); | |
| } | |
| /// Describes a single test to be run in the browser. | |
| class BrowserTest { | |
| // TODO(ricow): Add timeout callback instead of the string passing hack. | |
| BrowserDoneCallback doneCallback; | |
| String url; | |
| int timeout; | |
| String lastKnownMessage = ''; | |
| late Stopwatch stopwatch; | |
| Duration? delayUntilTestStarted; | |
| // We store this here for easy access when tests time out (instead of | |
| // capturing this in a closure) | |
| late Timer timeoutTimer; | |
| // Used for debugging, this is simply a unique identifier assigned to each | |
| // test. | |
| final int id = _idCounter++; | |
| static int _idCounter = 0; | |
| BrowserTest(this.url, this.doneCallback, this.timeout); | |
| String toJSON() => jsonEncode({'url': url, 'id': id}); | |
| } | |
| /* Describes the output of running the test in a browser */ | |
| class BrowserTestOutput { | |
| final Duration? delayUntilTestStarted; | |
| final Duration duration; | |
| final String lastKnownMessage; | |
| final BrowserOutput browserOutput; | |
| final bool didTimeout; | |
| BrowserTestOutput( | |
| this.delayUntilTestStarted, | |
| this.duration, | |
| this.lastKnownMessage, | |
| this.browserOutput, { | |
| this.didTimeout = false, | |
| }); | |
| } | |
| /// Encapsulates all the functionality for running tests in browsers. | |
| /// Tests are added to the queue and the supplied callbacks are called | |
| /// when a test completes. | |
| /// BrowserTestRunner starts up to maxNumBrowser instances of the browser, | |
| /// to run the tests, starting them sequentially, as needed, so only | |
| /// one is starting up at a time. | |
| /// BrowserTestRunner starts a BrowserTestingServer, which serves a | |
| /// driver page to the browsers, serves tests, and receives results and | |
| /// requests back from the browsers. | |
| class BrowserTestRunner { | |
| static const int _maxNextTestTimeouts = 10; | |
| static const Duration _nextTestTimeout = Duration(seconds: 120); | |
| /// If the queue was recently empty, don't start another browser. | |
| static const Duration _minNonemptyQueueTime = Duration(seconds: 1); | |
| final TestConfiguration configuration; | |
| final BrowserTestingServer testingServer; | |
| final Browser Function(TestConfiguration configuration) browserFactory; | |
| final String localIp; | |
| final int maxNumBrowsers; | |
| int numBrowsers = 0; | |
| /// Used to send back logs from the browser (start, stop etc.). | |
| void Function(String)? logger; | |
| bool testingServerStarted = false; | |
| bool underTermination = false; | |
| int numBrowserGetTestTimeouts = 0; | |
| DateTime lastEmptyTestQueueTime = DateTime.now(); | |
| String? _currentStartingBrowserId; | |
| List<BrowserTest> testQueue = []; | |
| Map<String, BrowserStatus> browserStatus = {}; | |
| Map<String, AdbDevice> adbDeviceMapping = {}; | |
| late List<AdbDevice> idleAdbDevices; | |
| /// 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 = {}; | |
| Map<int, String> doubleReportingOutputs = {}; | |
| List<String> timedOut = []; | |
| /// We will start a new browser when the test queue hasn't been empty | |
| /// recently, we have fewer than maxNumBrowsers browsers, and there is | |
| /// no other browser instance currently starting up. | |
| bool get queueWasEmptyRecently { | |
| return testQueue.isEmpty || | |
| DateTime.now().difference(lastEmptyTestQueueTime) < | |
| _minNonemptyQueueTime; | |
| } | |
| /// While a browser is starting, but has not requested its first test, its | |
| /// browserId is stored in _currentStartingBrowserId. | |
| /// When no browser is currently starting, _currentStartingBrowserId is null. | |
| bool get aBrowserIsCurrentlyStarting => _currentStartingBrowserId != null; | |
| void markCurrentlyStarting(String id) { | |
| _currentStartingBrowserId = id; | |
| } | |
| void markNotCurrentlyStarting(String id) { | |
| if (_currentStartingBrowserId == id) _currentStartingBrowserId = null; | |
| } | |
| BrowserTestRunner( | |
| this.configuration, | |
| this.localIp, | |
| this.maxNumBrowsers, [ | |
| this.browserFactory = Browser.fromConfiguration, | |
| ]) : testingServer = BrowserTestingServer( | |
| configuration, | |
| localIp, | |
| Browser.requiresFocus(configuration.runtime.name), | |
| ) { | |
| testingServer.testRunner = this; | |
| } | |
| Future<BrowserTestRunner> start() async { | |
| await testingServer.start(); | |
| testingServer | |
| ..testDoneCallBack = handleResults | |
| ..testStatusUpdateCallBack = handleStatusUpdate | |
| ..testStartedCallBack = handleStarted | |
| ..nextTestCallBack = getNextTest; | |
| testingServerStarted = true; | |
| requestBrowser(); | |
| return this; | |
| } | |
| /// requestBrowser() is called whenever we might want to start an additional | |
| /// browser instance. | |
| /// | |
| /// It is called when starting the BrowserTestRunner, and whenever a browser | |
| /// is killed, whenever a new test is enqueued, or whenever a browser | |
| /// finishes a test. | |
| /// So we are guaranteed that this will always eventually be called, as long | |
| /// as the test queue isn't empty. | |
| void requestBrowser() { | |
| if (!testingServerStarted) return; | |
| if (underTermination) return; | |
| if (numBrowsers == maxNumBrowsers) return; | |
| if (aBrowserIsCurrentlyStarting) return; | |
| if (numBrowsers > 0 && queueWasEmptyRecently) return; | |
| createBrowser(); | |
| } | |
| Future<bool> createBrowser() { | |
| Browser browser; | |
| if (configuration.runtime == Runtime.chromeOnAndroid) { | |
| var device = idleAdbDevices.removeLast(); | |
| browser = AndroidChrome(device); | |
| adbDeviceMapping[browser.id] = device; | |
| } else { | |
| browser = browserFactory(configuration); | |
| browser.logger = logger; | |
| } | |
| markCurrentlyStarting(browser.id); | |
| var status = BrowserStatus(browser); | |
| browserStatus[browser.id] = status; | |
| numBrowsers++; | |
| status.nextTestTimeout = createNextTestTimer(status); | |
| return browser.start(testingServer.getDriverUrl(browser.id)); | |
| } | |
| void handleResults(String browserId, String output, int testId) { | |
| var status = browserStatus[browserId]; | |
| if (testCache.containsKey(testId)) { | |
| doubleReportingOutputs[testId] = output; | |
| return; | |
| } | |
| var test = status?.currentTest; | |
| if (status == null || status.timeout) { | |
| // We don't do anything, this browser is currently being killed and | |
| // replaced. The browser here can be null if we decided to kill the | |
| // browser. | |
| } else if (test != null) { | |
| test.timeoutTimer.cancel(); | |
| test.stopwatch.stop(); | |
| if (test.id != testId) { | |
| print("Expected test id ${test.id} for ${test.url}"); | |
| print("Got test id $testId"); | |
| print("Last test id was ${status.lastTest?.id} for ${test.url}"); | |
| throw "This should never happen, wrong test id"; | |
| } | |
| testCache[testId] = test.url; | |
| // Report that the test is finished now | |
| var browserTestOutput = BrowserTestOutput( | |
| test.delayUntilTestStarted, | |
| test.stopwatch.elapsed, | |
| output, | |
| status.browser.testBrowserOutput, | |
| ); | |
| test.doneCallback(browserTestOutput); | |
| status.lastTest = test; | |
| status.currentTest = null; | |
| status.nextTestTimeout = createNextTestTimer(status); | |
| } else { | |
| print("\nThis is bad, should never happen, handleResult no test"); | |
| print("URL: ${status.lastTest?.url}"); | |
| print(output); | |
| terminate().then((_) { | |
| exit(1); | |
| }); | |
| } | |
| } | |
| void handleStatusUpdate(String browserId, String output, int testId) { | |
| var status = browserStatus[browserId]; | |
| if (status == null || status.timeout) { | |
| // We don't do anything, this browser is currently being killed and | |
| // replaced. The browser here can be null if we decided to kill the | |
| // browser. | |
| } else if (status.currentTest?.id == testId) { | |
| status.currentTest!.lastKnownMessage = output; | |
| } | |
| } | |
| void handleStarted(String browserId, String output, int testId) { | |
| var status = browserStatus[browserId]; | |
| if (status == null || status.timeout) return; | |
| var currentTest = status.currentTest; | |
| if (currentTest == null) return; | |
| currentTest.timeoutTimer.cancel(); | |
| currentTest.timeoutTimer = createTimeoutTimer(currentTest, status); | |
| currentTest.delayUntilTestStarted = currentTest.stopwatch.elapsed; | |
| } | |
| Future handleTimeout(BrowserStatus status) async { | |
| // 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. | |
| if (status.timeout) { | |
| DebugLogger.error("Got test timeout for an already restarting browser"); | |
| return; | |
| } | |
| status.timeout = true; | |
| var currentTest = status.currentTest!; | |
| timedOut.add(currentTest.url); | |
| var id = status.browser.id; | |
| currentTest.stopwatch.stop(); | |
| await status.browser.close(); | |
| var lastKnownMessage = | |
| 'Dom could not be fetched, since the test timed out.'; | |
| if (currentTest.lastKnownMessage.isNotEmpty) { | |
| lastKnownMessage = currentTest.lastKnownMessage; | |
| } | |
| if (status.lastTest != null) { | |
| lastKnownMessage += '\nPrevious test was ${status.lastTest!.url}'; | |
| } | |
| // Wait until the browser is closed before reporting the test as timeout. | |
| // This will enable us to capture stdout/stderr from the browser | |
| // (which might provide us with information about what went wrong). | |
| var browserTestOutput = BrowserTestOutput( | |
| currentTest.delayUntilTestStarted, | |
| currentTest.stopwatch.elapsed, | |
| lastKnownMessage, | |
| status.browser.testBrowserOutput, | |
| didTimeout: true, | |
| ); | |
| currentTest.doneCallback(browserTestOutput); | |
| status.lastTest = status.currentTest; | |
| status.currentTest = null; | |
| // We don't want to start a new browser if we are terminating. | |
| if (underTermination) return; | |
| removeBrowser(id); | |
| requestBrowser(); | |
| } | |
| /// Remove a browser that has closed from our data structures that track | |
| /// open browsers. Check if we want to replace it with a new browser. | |
| void removeBrowser(String id) { | |
| if (configuration.runtime == Runtime.chromeOnAndroid) { | |
| idleAdbDevices.add(adbDeviceMapping.remove(id)!); | |
| } | |
| markNotCurrentlyStarting(id); | |
| browserStatus.remove(id); | |
| --numBrowsers; | |
| } | |
| BrowserTest? getNextTest(String browserId) { | |
| markNotCurrentlyStarting(browserId); | |
| var status = browserStatus[browserId]; | |
| if (status == null) return null; | |
| status.nextTestTimeout?.cancel(); | |
| status.nextTestTimeout = null; | |
| if (testQueue.isEmpty) return null; | |
| // We are currently terminating this browser, don't start a new test. | |
| if (status.timeout) return null; | |
| var test = testQueue.removeLast(); | |
| // If our queue isn't empty, try starting more browsers | |
| if (testQueue.isEmpty) { | |
| lastEmptyTestQueueTime = DateTime.now(); | |
| } else { | |
| requestBrowser(); | |
| } | |
| if (status.currentTest != null) { | |
| // TODO(ricow): Handle this better. | |
| print("Browser requested next test before reporting previous result"); | |
| print("This happened for browser $browserId"); | |
| print("Old test was: ${status.currentTest!.url}"); | |
| print("The test before that was: ${status.lastTest?.url}"); | |
| print("Timed out tests:"); | |
| for (var v in timedOut) { | |
| print(" $v"); | |
| } | |
| exit(1); | |
| } | |
| status.currentTest = test | |
| ..lastKnownMessage = '' | |
| ..timeoutTimer = createTimeoutTimer(test, status) | |
| ..stopwatch = (Stopwatch()..start()); | |
| // Reset the test specific output information (stdout, stderr) on the | |
| // browser, since a new test is being started. | |
| status.browser.resetTestBrowserOutput(); | |
| status.browser.logBrowserInfoToTestBrowserOutput(); | |
| return test; | |
| } | |
| /// Creates a timer that is active while a test is running on a browser. | |
| Timer createTimeoutTimer(BrowserTest test, BrowserStatus status) { | |
| return Timer(Duration(seconds: test.timeout), () { | |
| handleTimeout(status); | |
| }); | |
| } | |
| /// Creates a timer that is active while no test is running on the | |
| /// browser. It has finished one test, and it has not requested a new test. | |
| Timer createNextTestTimer(BrowserStatus status) { | |
| return Timer(BrowserTestRunner._nextTestTimeout, () { | |
| handleNextTestTimeout(status); | |
| }); | |
| } | |
| void handleNextTestTimeout(BrowserStatus status) { | |
| DebugLogger.warning( | |
| "Browser timed out before getting next test. Restarting", | |
| ); | |
| if (status.timeout) return; | |
| numBrowserGetTestTimeouts++; | |
| if (numBrowserGetTestTimeouts >= _maxNextTestTimeouts) { | |
| DebugLogger.error( | |
| "Too many browser timeouts before getting next test. Terminating", | |
| ); | |
| terminate().then((_) => exit(1)); | |
| } else { | |
| status.timeout = true; | |
| status.browser.close().then((_) { | |
| removeBrowser(status.browser.id); | |
| requestBrowser(); | |
| }); | |
| } | |
| } | |
| void enqueueTest(BrowserTest test) { | |
| testQueue.add(test); | |
| requestBrowser(); | |
| } | |
| void printDoubleReportingTests() { | |
| if (doubleReportingOutputs.isEmpty) 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 doubleReportingOutputs.keys) { | |
| print(" ${testCache[id]}"); | |
| } | |
| DebugLogger.warning("Double reporting tests:"); | |
| for (var id in doubleReportingOutputs.keys) { | |
| DebugLogger.warning("${testCache[id]}"); | |
| } | |
| } | |
| // TODO(26191): Call a unified fatalError(), that shuts down all subprocesses. | |
| // This just kills the browsers in this BrowserTestRunner instance. | |
| Future terminate() async { | |
| var browsers = <Browser>[]; | |
| underTermination = true; | |
| testingServer.underTermination = true; | |
| for (var status in browserStatus.values) { | |
| browsers.add(status.browser); | |
| status.nextTestTimeout?.cancel(); | |
| status.nextTestTimeout = null; | |
| } | |
| for (var browser in browsers) { | |
| await browser.close(); | |
| } | |
| testingServer.errorReportingServer.close(); | |
| printDoubleReportingTests(); | |
| } | |
| } | |
| /// 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 | |
| class BrowserTestingServer { | |
| final TestConfiguration configuration; | |
| final String localIp; | |
| final bool requiresFocus; | |
| late BrowserTestRunner testRunner; | |
| static const String driverPath = "/driver"; | |
| static const String nextTestPath = "/next_test"; | |
| static const String reportPath = "/report"; | |
| static const String statusUpdatePath = "/status_update"; | |
| static const String startedPath = "/started"; | |
| static const String waitSignal = "WAIT"; | |
| static const String terminateSignal = "TERMINATE"; | |
| var testCount = 0; | |
| late HttpServer errorReportingServer; | |
| bool underTermination = false; | |
| late TestChangedCallback testDoneCallBack; | |
| late TestChangedCallback testStatusUpdateCallBack; | |
| late TestChangedCallback testStartedCallBack; | |
| late NextTestCallback nextTestCallBack; | |
| BrowserTestingServer(this.configuration, this.localIp, this.requiresFocus); | |
| Future start() async { | |
| var server = await HttpServer.bind( | |
| localIp, | |
| configuration.testDriverErrorPort, | |
| ); | |
| setupErrorServer(server); | |
| setupDispatchingServer(server); | |
| } | |
| void setupErrorServer(HttpServer server) { | |
| errorReportingServer = server; | |
| void errorReportingHandler(HttpRequest request) { | |
| var buffer = StringBuffer(); | |
| request | |
| .cast<List<int>>() | |
| .transform(utf8.decoder) | |
| .listen( | |
| (data) { | |
| buffer.write(data); | |
| }, | |
| onDone: () { | |
| var back = buffer.toString(); | |
| request.response.headers.set("Access-Control-Allow-Origin", "*"); | |
| request.response.done.catchError((error) { | |
| DebugLogger.error( | |
| "Error getting error from browser" | |
| "on uri ${request.uri.path}: $error", | |
| ); | |
| }); | |
| request.response.close(); | |
| DebugLogger.error( | |
| "Error from browser on : " | |
| "${request.uri.path}, data: $back", | |
| ); | |
| }, | |
| onError: print, | |
| ); | |
| } | |
| void errorHandler(e) { | |
| if (!underTermination) print("Error occurred in httpserver: $e"); | |
| } | |
| errorReportingServer.listen(errorReportingHandler, onError: errorHandler); | |
| } | |
| void setupDispatchingServer(HttpServer _) { | |
| var server = configuration.servers.server!; | |
| void noCache(HttpRequest request) { | |
| request.response.headers.set( | |
| "Cache-Control", | |
| "no-cache, no-store, must-revalidate", | |
| ); | |
| } | |
| int testId(HttpRequest request) => | |
| int.parse(request.uri.queryParameters["id"]!); | |
| String browserId(HttpRequest request, String prefix) => | |
| request.uri.path.substring(prefix.length + 1); | |
| server.addHandler(reportPath, (HttpRequest request) { | |
| noCache(request); | |
| handleReport( | |
| request, | |
| browserId(request, reportPath), | |
| testId(request), | |
| isStatusUpdate: false, | |
| ); | |
| }); | |
| server.addHandler(statusUpdatePath, (HttpRequest request) { | |
| noCache(request); | |
| handleReport( | |
| request, | |
| browserId(request, statusUpdatePath), | |
| testId(request), | |
| isStatusUpdate: true, | |
| ); | |
| }); | |
| server.addHandler(startedPath, (HttpRequest request) { | |
| noCache(request); | |
| handleStarted(request, browserId(request, startedPath), testId(request)); | |
| }); | |
| 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 = Future.value( | |
| getNextTest(browserId(request, nextTestPath)), | |
| ); | |
| request.response.headers.set('Content-Type', 'text/plain'); | |
| } else { | |
| textResponse = Future.value(""); | |
| } | |
| request.response.done.catchError((error) async { | |
| if (!underTermination) { | |
| var text = await textResponse; | |
| print("URI ${request.uri}"); | |
| print("text $text"); | |
| throw "Error returning content to browser: $error"; | |
| } | |
| }); | |
| 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 already has catchError installed above. | |
| request.response.close(); | |
| }); | |
| } | |
| server.addHandler(driverPath, sendPageHandler); | |
| server.addHandler(nextTestPath, sendPageHandler); | |
| } | |
| void handleReport( | |
| HttpRequest request, | |
| String browserId, | |
| int testId, { | |
| required bool isStatusUpdate, | |
| }) { | |
| var buffer = StringBuffer(); | |
| request | |
| .cast<List<int>>() | |
| .transform(utf8.decoder) | |
| .listen( | |
| (data) { | |
| buffer.write(data); | |
| }, | |
| onDone: () { | |
| var back = buffer.toString(); | |
| request.response.close(); | |
| if (isStatusUpdate) { | |
| testStatusUpdateCallBack(browserId, back, testId); | |
| } else { | |
| testDoneCallBack(browserId, back, testId); | |
| } | |
| // TODO(ricow): We should do something smart if we get an error here. | |
| }, | |
| onError: (error) { | |
| DebugLogger.error("$error"); | |
| }, | |
| ); | |
| } | |
| void handleStarted(HttpRequest request, String browserId, int testId) { | |
| var buffer = StringBuffer(); | |
| // If an error occurs while receiving the data from the request stream, | |
| // we don't handle it specially. We can safely ignore it, since the started | |
| // events are not crucial. | |
| request | |
| .cast<List<int>>() | |
| .transform(utf8.decoder) | |
| .listen( | |
| (data) { | |
| buffer.write(data); | |
| }, | |
| onDone: () { | |
| var back = buffer.toString(); | |
| request.response.close(); | |
| testStartedCallBack(browserId, back, testId); | |
| }, | |
| onError: (error) { | |
| DebugLogger.error("$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; | |
| } | |
| return nextTest == null ? waitSignal : nextTest.toJSON(); | |
| } | |
| String getDriverUrl(String browserId) { | |
| return "http://$localIp:${configuration.servers.port}/driver/$browserId"; | |
| } | |
| Future<String> getDriverPage(String browserId) async { | |
| var errorReportingUrl = | |
| "http://$localIp:${errorReportingServer.port}/$browserId"; | |
| var driverContent = | |
| """ | |
| <!DOCTYPE html><html> | |
| <head> | |
| <title>Driving page</title> | |
| <meta charset="utf-8"> | |
| <style> | |
| .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> | |
| <script type='text/javascript'> | |
| var STATUS_UPDATE_INTERVAL = 10000; | |
| function startTesting() { | |
| var number_of_tests = 0; | |
| var current_id; | |
| var next_id; | |
| // Has the test in the current iframe reported that it is done? | |
| var test_completed = true; | |
| // Has the test in the current iframe reported that it is started? | |
| var test_started = false; | |
| var testing_window; | |
| var embedded_iframe_div = document.getElementById('embedded_iframe_div'); | |
| var embedded_iframe = document.getElementById('embedded_iframe'); | |
| var number_div = document.getElementById('number'); | |
| var executing_div = document.getElementById('currently_executing'); | |
| var error_div = document.getElementById('unhandled_error'); | |
| var start = new Date(); | |
| 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 { | |
| var elapsed = new Date() - start; | |
| var nextTask = JSON.parse(this.responseText); | |
| var url = nextTask.url; | |
| next_id = nextTask.id; | |
| run(url); | |
| } | |
| } else { | |
| reportError('Could not contact the server and get a new task'); | |
| } | |
| } | |
| } | |
| function contactBrowserController(method, | |
| path, | |
| callback, | |
| msg, | |
| isUrlEncoded) { | |
| var client = new XMLHttpRequest(); | |
| client.onreadystatechange = callback; | |
| client.open(method, path); | |
| if (isUrlEncoded) { | |
| client.setRequestHeader('Content-type', | |
| 'application/x-www-form-urlencoded'); | |
| } | |
| client.send(msg); | |
| } | |
| function getNextTask() { | |
| // Until we have the next task we set the current_id to a specific | |
| // negative value. | |
| contactBrowserController( | |
| 'GET', '$nextTestPath/$browserId', newTaskHandler, "", false); | |
| } | |
| function childError(message, filename, lineno, colno, error) { | |
| sendStatusUpdate(); | |
| if (error) { | |
| reportMessage('FAIL:' + filename + ':' + lineno + | |
| ':' + colno + ':' + message + '\\n' + error.stack, false, false); | |
| } else if (filename) { | |
| reportMessage('FAIL:' + filename + ':' + lineno + | |
| ':' + colno + ':' + message, false, false); | |
| } else { | |
| reportMessage('FAIL: ' + message, false, false); | |
| } | |
| return true; | |
| } | |
| function run(url) { | |
| number_of_tests++; | |
| number_div.textContent = number_of_tests; | |
| executing_div.textContent = url; | |
| if (typeof testing_window != 'undefined') { | |
| testing_window.close(); | |
| } | |
| testing_window = window.open(url); | |
| test_started = false; | |
| test_completed = false; | |
| } | |
| window.onerror = function (message, url, lineNumber, columnNumber, err) { | |
| // Ensure the stack is included in the reported error, if available. | |
| if (err && err.stack) { | |
| message = message + '\\n' + err.stack; | |
| } | |
| if (url) { | |
| message = url + ':' + lineNumber + ':' + columnNumber + ':' + message; | |
| } | |
| reportError(message); | |
| } | |
| function reportError(msg) { | |
| function handleReady() { | |
| if (this.readyState == this.DONE && this.status != 200) { | |
| var error = 'Sending back error did not succeed: ' + this.status; | |
| error = error + '. Failed to send msg: ' + msg; | |
| error_div.innerHTML = error; | |
| } | |
| } | |
| contactBrowserController( | |
| 'POST', '$errorReportingUrl?test=1', handleReady, msg, true); | |
| } | |
| function reportMessage(msg, isFirstMessage, isStatusUpdate) { | |
| if (isFirstMessage) { | |
| if (test_started) { | |
| reportMessage( | |
| "FAIL: test started more than once (test reloads itself) " + | |
| msg, false, false); | |
| return; | |
| } | |
| current_id = next_id; | |
| test_started = true; | |
| contactBrowserController( | |
| 'POST', '$startedPath/$browserId?id=' + current_id, | |
| function () {}, msg, true); | |
| } else if (isStatusUpdate) { | |
| contactBrowserController( | |
| 'POST', '$statusUpdatePath/$browserId?id=' + current_id, | |
| function() {}, msg, true); | |
| } else { | |
| var is_double_report = test_completed; | |
| var retry = 0; | |
| test_completed = true; | |
| function reportDoneMessage() { | |
| contactBrowserController( | |
| 'POST', '$reportPath/$browserId?id=' + current_id, | |
| handleReady, msg, true); | |
| } | |
| function handleReady() { | |
| if (this.readyState == this.DONE) { | |
| if (this.status == 200) { | |
| if (!is_double_report) { | |
| getNextTask(); | |
| } | |
| } else { | |
| reportError('Error sending result to server. Status: ' + | |
| this.status + ' Retry: ' + retry); | |
| retry++; | |
| if (retry < 3) { | |
| setTimeout(reportDoneMessage, 1000); | |
| } | |
| } | |
| } | |
| } | |
| reportDoneMessage(); | |
| } | |
| } | |
| function parseResult(result) { | |
| var parsedData = null; | |
| try { | |
| parsedData = JSON.parse(result); | |
| } catch(error) { } | |
| return parsedData; | |
| } | |
| // Browser tests send JSON messages to the driver window, handled here. | |
| function messageHandler(e) { | |
| var msg = e.data; | |
| if (typeof msg != 'string') return; | |
| if (e.source != testing_window) { | |
| reportError("Message received from old test window: " + msg); | |
| return; | |
| } | |
| var parsedData = parseResult(msg); | |
| if (parsedData) { | |
| // Only if the JSON message contains all required parameters, | |
| // will we handle it and post it back to the test controller. | |
| if ('message' in parsedData && | |
| 'is_first_message' in parsedData && | |
| 'is_status_update' in parsedData && | |
| 'is_done' in parsedData) { | |
| var message = parsedData['message']; | |
| var isFirstMessage = parsedData['is_first_message']; | |
| var isStatusUpdate = parsedData['is_status_update']; | |
| var isDone = parsedData['is_done']; | |
| if (!isFirstMessage && !isStatusUpdate) { | |
| if (!isDone) { | |
| alert("Bug in test_controller.js: " + | |
| "isFirstMessage/isStatusUpdate/isDone were all false"); | |
| } | |
| } | |
| reportMessage(message, isFirstMessage, isStatusUpdate); | |
| } | |
| } | |
| } | |
| function sendStatusUpdate () { | |
| var dom = | |
| embedded_iframe.contentWindow.document.documentElement.innerHTML; | |
| var message = 'Status:\\n'; | |
| message += ' DOM:\\n' + | |
| ' ' + dom; | |
| reportMessage(message, false, true); | |
| } | |
| function sendRepeatingStatusUpdate() { | |
| sendStatusUpdate(); | |
| setTimeout(sendRepeatingStatusUpdate, STATUS_UPDATE_INTERVAL); | |
| } | |
| window.addEventListener('message', messageHandler, false); | |
| waitForDone = false; | |
| getNextTask(); | |
| } | |
| </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 id="embedded_iframe"></iframe> | |
| </div> | |
| </body> | |
| </html> | |
| """; | |
| return driverContent; | |
| } | |
| } |