diff --git a/bin/test.dart b/bin/test.dart new file mode 100644 index 00000000..125460fc --- /dev/null +++ b/bin/test.dart @@ -0,0 +1,360 @@ +// Copyright (c) 2015, 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. + +// TODO(DrMarcII) This is forked from executable.dart in the 'test' package. +// When 'test' supports more extension points, remove/replace this. +library test.executable; + +import 'dart:async'; +import 'dart:io'; + +import 'package:args/args.dart'; +import 'package:stack_trace/stack_trace.dart'; +import 'package:yaml/yaml.dart'; + +import 'package:test/src/backend/test_platform.dart'; +import 'package:test/src/runner/reporter/compact.dart'; +import 'package:test/src/runner/reporter/expanded.dart'; +import 'package:test/src/runner/application_exception.dart'; +import 'package:test/src/runner/load_exception.dart'; +import 'package:test/src/runner/load_exception_suite.dart'; +import 'package:test/src/util/exit_codes.dart' as exit_codes; +import 'package:test/src/util/io.dart'; +import 'package:test/src/utils.dart'; +import 'package:webdriver/test/loader.dart'; + +/// The argument parser used to parse the executable arguments. +final _parser = new ArgParser(allowTrailingOptions: true); + +/// A merged stream of all signals that tell the test runner to shut down +/// gracefully. +/// +/// Signals will only be captured as long as this has an active subscription. +/// Otherwise, they'll be handled by Dart's default signal handler, which +/// terminates the program immediately. +final _signals = Platform.isWindows + ? ProcessSignal.SIGINT.watch() + : mergeStreams( + [ProcessSignal.SIGTERM.watch(), ProcessSignal.SIGINT.watch()]); + +/// Returns whether the current package has a pubspec which uses the +/// `test/pub_serve` transformer. +bool get _usesTransformer { + if (!new File('pubspec.yaml').existsSync()) return false; + var contents = new File('pubspec.yaml').readAsStringSync(); + + var yaml; + try { + yaml = loadYaml(contents); + } on FormatException { + return false; + } + + if (yaml is! Map) return false; + + var transformers = yaml['transformers']; + if (transformers == null) return false; + if (transformers is! List) return false; + + return transformers.any((transformer) { + if (transformer is String) return transformer == 'test/pub_serve'; + if (transformer is! Map) return false; + if (transformer.keys.length != 1) return false; + return transformer.keys.single == 'test/pub_serve'; + }); +} + +void main(List args) { + var allPlatforms = TestPlatform.all.toList(); + allPlatforms.removeWhere((p) => !p.isBrowser); + + _parser.addFlag("help", + abbr: "h", negatable: false, help: "Shows this usage information."); + _parser.addFlag("version", + negatable: false, help: "Shows the package's version."); + _parser.addOption("package-root", hide: true); + _parser.addOption("name", + abbr: 'n', help: 'A substring of the name of the test to run.\n' + 'Regular expression syntax is supported.'); + _parser.addOption("plain-name", + abbr: 'N', + help: 'A plain-text substring of the name of the test to run.'); + _parser.addOption("platform", + abbr: 'p', + help: 'The platform on which to run the tests.', + allowed: allPlatforms.map((platform) => platform.identifier).toList(), + defaultsTo: 'chrome'); + _parser.addOption("pub-serve", + help: 'The port of a pub serve instance serving "test/".', + hide: !supportsPubServe, + valueHelp: 'port'); + _parser.addOption("reporter", + abbr: 'r', + help: 'The runner used to print test results.', + allowed: ['compact', 'expanded'], + defaultsTo: Platform.isWindows ? 'expanded' : 'compact', + allowedHelp: { + 'compact': 'A single line, updated continuously.', + 'expanded': 'A separate line for each update.' + }); + _parser.addFlag("color", + defaultsTo: null, + help: 'Whether to use terminal colors.\n(auto-detected by default)'); + _parser.addOption("config-dir", + defaultsTo: 'packages/webdriver/test/configs', + help: 'Directory with browser config JSON files.'); + + var options; + try { + options = _parser.parse(args); + } on FormatException catch (error) { + _printUsage(error.message); + exitCode = exit_codes.usage; + return; + } + + if (options["help"]) { + _printUsage(); + return; + } + + if (options["version"]) { + if (!_printVersion()) { + stderr.writeln("Couldn't find version number."); + exitCode = exit_codes.data; + } + return; + } + + var color = options["color"]; + if (color == null) color = canUseSpecialChars; + + var pubServeUrl; + if (options["pub-serve"] != null) { + pubServeUrl = Uri.parse("http://localhost:${options['pub-serve']}"); + if (!_usesTransformer) { + stderr.write(''' +When using --pub-serve, you must include the "test/pub_serve" transformer in +your pubspec: + +transformers: +- test/pub_serve: + \$include: test/**_test.dart +'''); + exitCode = exit_codes.data; + return; + } + } + + var platforms; + if (options["platform"] is List) { + platforms = options["platform"].map(TestPlatform.find); + } else { + platforms = [TestPlatform.find(options["platform"])]; + } + var loader = new Loader(platforms, + pubServeUrl: pubServeUrl, + packageRoot: options["package-root"], + color: color, + configDir: options["config-dir"]); + + var concurrency = 1; + + var paths = options.rest; + if (paths.isEmpty) { + if (!new Directory("test").existsSync()) { + _printUsage('No test files were passed and the default "test/" ' + "directory doesn't exist."); + exitCode = exit_codes.data; + return; + } + paths = ["test"]; + } + + var signalSubscription; + var closed = false; + signalSubscription = _signals.listen((_) { + signalSubscription.cancel(); + closed = true; + loader.close(); + }); + + mergeStreams(paths.map((path) { + if (new Directory(path).existsSync()) return loader.loadDir(path); + if (new File(path).existsSync()) return loader.loadFile(path); + return new Stream.fromFuture(new Future.error( + new LoadException(path, 'Does not exist.'), new Trace.current())); + })) + .transform(new StreamTransformer.fromHandlers( + handleError: (error, stackTrace, sink) { + if (error is! LoadException) { + sink.addError(error, stackTrace); + } else { + sink.add(new LoadExceptionSuite(error, stackTrace)); + } + })).toList().then((suites) { + if (closed) return null; + suites = flatten(suites); + + var pattern; + if (options["name"] != null) { + if (options["plain-name"] != null) { + _printUsage("--name and --plain-name may not both be passed."); + exitCode = exit_codes.data; + return null; + } + pattern = new RegExp(options["name"]); + } else if (options["plain-name"] != null) { + pattern = options["plain-name"]; + } + + if (pattern != null) { + suites = suites.map((suite) { + // Don't ever filter out load errors. + if (suite is LoadExceptionSuite) return suite; + return suite.change( + tests: suite.tests.where((test) => test.name.contains(pattern))); + }).toList(); + + if (suites.every((suite) => suite.tests.isEmpty)) { + stderr.write('No tests match '); + + if (pattern is RegExp) { + stderr.writeln('regular expression "${pattern.pattern}".'); + } else { + stderr.writeln('"$pattern".'); + } + exitCode = exit_codes.data; + return null; + } + } + + var reporter = options["reporter"] == "compact" + ? new CompactReporter(flatten(suites), + concurrency: concurrency, color: color) + : new ExpandedReporter(flatten(suites), + concurrency: concurrency, color: color); + + // Override the signal handler to close [reporter]. [loader] will still be + // closed in the [whenComplete] below. + signalSubscription.onData((_) { + signalSubscription.cancel(); + closed = true; + + // Wait a bit to print this message, since printing it eagerly looks weird + // if the tests then finish immediately. + var timer = new Timer(new Duration(seconds: 1), () { + // Print a blank line first to ensure that this doesn't interfere with + // the compact reporter's unfinished line. + print(""); + print("Waiting for current test(s) to finish."); + print("Press Control-C again to terminate immediately."); + }); + + reporter.close().then((_) => timer.cancel()); + }); + + return reporter.run().then((success) { + exitCode = success ? 0 : 1; + }).whenComplete(() { + signalSubscription.cancel(); + return reporter.close(); + }); + }).whenComplete(signalSubscription.cancel).catchError((error, stackTrace) { + if (error is ApplicationException) { + stderr.writeln(error.message); + exitCode = exit_codes.data; + return; + } + + stderr.writeln(getErrorMessage(error)); + stderr.writeln(new Trace.from(stackTrace).terse); + stderr.writeln("This is an unexpected error. Please file an issue at " + "http://github.com/dart-lang/test\n" + "with the stack trace and instructions for reproducing the error."); + exitCode = exit_codes.software; + }).whenComplete(() { + return loader.close(); + }); +} + +/// Print usage information for this command. +/// +/// If [error] is passed, it's used in place of the usage message and the whole +/// thing is printed to stderr instead of stdout. +void _printUsage([String error]) { + var output = stdout; + + var message = "Runs tests in this package."; + if (error != null) { + message = error; + output = stderr; + } + + output.write("""$message + +Usage: pub run webdriver:test [files or directories...] + +${_parser.usage} +"""); +} + +/// Prints the version number of the test package. +/// +/// This loads the version number from the current package's lockfile. It +/// returns true if it successfully printed the version number and false if it +/// couldn't be loaded. +bool _printVersion() { + var lockfile; + try { + lockfile = loadYaml(new File("pubspec.lock").readAsStringSync()); + } on FormatException catch (_) { + return false; + } on IOException catch (_) { + return false; + } + + if (lockfile is! Map) return false; + var packages = lockfile["packages"]; + if (packages is! Map) return false; + var package = packages["test"]; + if (package is! Map) return false; + + var source = package["source"]; + if (source is! String) return false; + + switch (source) { + case "hosted": + var version = package["version"]; + if (version is! String) return false; + + print(version); + return true; + + case "git": + var version = package["version"]; + if (version is! String) return false; + var description = package["description"]; + if (description is! Map) return false; + var ref = description["resolved-ref"]; + if (ref is! String) return false; + + print("$version (${ref.substring(0, 7)})"); + return true; + + case "path": + var version = package["version"]; + if (version is! String) return false; + var description = package["description"]; + if (description is! Map) return false; + var path = description["path"]; + if (path is! String) return false; + + print("$version (from $path)"); + return true; + + default: + return false; + } +} diff --git a/lib/support/forwarder.dart b/lib/support/forwarder.dart index 651ec8b9..bef75be4 100644 --- a/lib/support/forwarder.dart +++ b/lib/support/forwarder.dart @@ -14,17 +14,15 @@ library webdriver.support.forwarder; -import 'dart:async' show Future, StreamConsumer; +import 'dart:async' show Future, Stream, StreamConsumer; import 'dart:convert' show JSON, UTF8; -import 'dart:io' show ContentType, Directory, File, HttpRequest, HttpStatus; +import 'dart:io' show Directory, File; import 'package:path/path.dart' as path; +import 'package:shelf/shelf.dart' as shelf; import 'package:webdriver/core.dart' show By, WebDriver, WebDriverException, WebElement; -final _contentTypeJson = - new ContentType('application', 'json', charset: 'utf-8'); - /// Attribute on elements used to locate them on passed WebDriver commands. const wdElementIdAttribute = 'wd-element-id'; @@ -53,12 +51,19 @@ const wdElementIdAttribute = 'wd-element-id'; /// POST '/source': takes a 'file' arg and will capture the current page's /// source and save it to the specified file name in [outputDir]. /// +/// Finally the forwarder support a command for switching to a specific +/// frame: +/// POST '/findframe/': switches to a frame that includes an element +/// with wd-element-id="frame-id" and innerText equal to id. +/// Only looks at the top-level context and any first-level iframes. +/// All iframes in the top-level context will be made visible. +/// /// See https://code.google.com/p/selenium/wiki/JsonWireProtocol for /// documentation of other commands. class WebDriverForwarder { /// [WebDriver] instance to forward commands to. final WebDriver driver; - /// Path prefix that all forwarded commands will have. + /// Path prefix that all forwarded commands will have. Should be relative. final Pattern prefix; /// Directory to save screenshots and page source to. final Directory outputDir; @@ -66,44 +71,51 @@ class WebDriverForwarder { bool useDeep; WebDriverForwarder(this.driver, - {this.prefix: '/webdriver', Directory outputDir, this.useDeep: false}) + {this.prefix: 'webdriver', Directory outputDir, this.useDeep: false}) : this.outputDir = outputDir == null ? Directory.systemTemp.createTempSync() : outputDir; /// Forward [request] to [driver] and respond to the request with the returned /// value or any thrown exceptions. - Future forward(HttpRequest request) async { + Future handler(shelf.Request request) async { try { - if (!request.uri.path.startsWith(prefix)) { - request.response.statusCode = HttpStatus.NOT_FOUND; - return; + if (!request.url.path.startsWith(prefix)) { + return new shelf.Response.notFound(null); } - request.response.statusCode = HttpStatus.OK; - request.response.headers.contentType = _contentTypeJson; - var endpoint = request.uri.path.replaceFirst(prefix, ''); + var endpoint = request.url.path.replaceFirst(prefix, ''); if (endpoint.startsWith('/')) { endpoint = endpoint.substring(1); } var params; if (request.method == 'POST') { - String requestBody = await UTF8.decodeStream(request); + String requestBody = await UTF8.decodeStream(request.read()); if (requestBody != null && requestBody.isNotEmpty) { params = JSON.decode(requestBody); } } var value = await _forward(request.method, endpoint, params); - request.response - .add(UTF8.encode(JSON.encode({'status': 0, 'value': value}))); + return new shelf.Response.ok(JSON.encode({'status': 0, 'value': value}), + headers: { + 'Content-Type': 'application/json', + 'Cache-Control': 'no-cache' + }); } on WebDriverException catch (e) { - request.response.add(UTF8.encode(JSON - .encode({'status': e.statusCode, 'value': {'message': e.message}}))); + return new shelf.Response.internalServerError( + body: JSON.encode( + {'status': e.statusCode, 'value': {'message': e.message}}), + headers: { + 'Content-Type': 'application/json', + 'Cache-Control': 'no-cache' + }); } catch (e) { - request.response.add(UTF8.encode( - JSON.encode({'status': 13, 'value': {'message': e.toString()}}))); - } finally { - await request.response.close(); + return new shelf.Response.internalServerError( + body: JSON.encode({'status': 13, 'value': {'message': e.toString()}}), + headers: { + 'Content-Type': 'application/json', + 'Cache-Control': 'no-cache' + }); } } @@ -139,6 +151,9 @@ class WebDriverForwarder { return null; } break; + case 'findframe': + await _findFrame(endpointTokens[1]); + return null; case 'element': // process endpoints of the form /element/[id]/... if (endpointTokens.length >= 2) { @@ -178,14 +193,44 @@ class WebDriverForwarder { } } - Future _findElement(String id) async { + Future _findFrame(String id) async { + await driver.switchTo.frame(); + if (await _isFrame(id)) { + return; + } + await for (WebElement element + in driver.findElements(const By.tagName('iframe'))) { + if (!await element.displayed) { + await driver.execute( + 'arguments[0].style.display = "block";', [element]); + } + await driver.switchTo.frame(element); + if (await _isFrame(id)) { + return; + } + await driver.switchTo.frame(); + } + throw 'Frame $id not found'; + } + + Future _isFrame(String id) async { + await for (WebElement element in _findElements('frame-id')) { + if ((await element.attributes['innerText']).trim() == id) { + return true; + } + } + return false; + } + + Future _findElement(String id) async => + (await _findElements(id).toList()).single.id; + + Stream _findElements(String id) async* { var selector = "[$wdElementIdAttribute='$id']"; if (useDeep) { selector = '* /deep/ $selector'; } - var elements = - await driver.findElements(new By.cssSelector(selector)).toList(); - return elements.single.id; + yield* driver.findElements(new By.cssSelector(selector)); } dynamic _deepCopy(dynamic source) async { diff --git a/lib/test/configs/chrome_cfg.json b/lib/test/configs/chrome_cfg.json new file mode 100644 index 00000000..caf39432 --- /dev/null +++ b/lib/test/configs/chrome_cfg.json @@ -0,0 +1,9 @@ +{ + "desired": { + "browserName": "chrome", + "chromeOptions": { + "binary": "$CHROMEDRIVER_BINARY", + "args": [ "--no-sandbox" ] + } + } +} diff --git a/lib/test/configs/dartium_cfg.json b/lib/test/configs/dartium_cfg.json new file mode 100644 index 00000000..94366c4d --- /dev/null +++ b/lib/test/configs/dartium_cfg.json @@ -0,0 +1,9 @@ +{ + "desired": { + "browserName": "chrome", + "chromeOptions": { + "binary": "/usr/lib/google-dartlang/bin/dartium", + "args": [ "--no-sandbox" ] + } + } +} diff --git a/lib/test/hybrid.dart b/lib/test/hybrid.dart new file mode 100644 index 00000000..654d8ff1 --- /dev/null +++ b/lib/test/hybrid.dart @@ -0,0 +1,136 @@ +// Copyright 2015 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +library webdriver.test.hybrid; + +import 'dart:async'; +import 'dart:html'; +import 'dart:math'; + +import 'package:path/path.dart' as p; +import 'package:webdriver/html.dart'; + +const _wdIdAttribute = 'wd-element-id'; +const _frameId = 'frame-id'; + +Future createDriver() async { + //throw "${window.location}"; + var path = p.join('/', p.split(window.location.pathname)[1], 'webdriver/'); + WebDriver driver = await fromExistingSession('1', + uri: new Uri.http( + '${window.location.hostname}:${window.location.port}', path)); + var hybrid = new HybridDriver(driver); + await hybrid.switchToCurrentFrame(); + return hybrid; +} + +class HybridDriver { + final WebDriver _driver; + + HybridDriver(this._driver); + + static int _nextId = 0; + + /// Allow methods to work on elements in Shadow DOMs. + /// Requires browser support the /deep/ selector combinator. + Future enableShadowDomSupport() async { + await _driver.postRequest('enabledeep'); + } + + /// Disallow methods to work on elements in Shadow DOMs. + Future disableShadowDomSupport() async { + await _driver.postRequest('disabledeep'); + } + + /// Makes the current frame the active frame for WebDriver, making sure that + /// frame is visible. + /// Will only work if the current frame is the top-level browser context of + /// the current window or an iframe in the top-level browser context. + Future switchToCurrentFrame() async { + var element = document.querySelector('[$_wdIdAttribute="$_frameId"]'); + if (element == null) { + element = new Element.div() + ..attributes[_wdIdAttribute] = _frameId + ..text = new Random().nextInt(1024).toString(); + document.body.append(element); + } + var id = element.text.trim(); + await _driver.postRequest('findframe/$id'); + } + + /// Clicks on [element]. + Future click(Element element) async { + await _driver.postRequest('element/${_elementId(element)}/click'); + } + + /// Type into the [element] (or the currently focused element if [element] + /// not provided). + Future sendKeys(String keys, {Element element}) async { + if (element != null) { + await _driver.postRequest( + 'element/${_elementId(element)}/value', {'value': [keys]}); + } else { + await _driver.keyboard.sendKeys(keys); + } + } + + /// Clear the value of [element]. + Future clear(Element element) async { + await _driver.postRequest('element/${_elementId(element)}/clear'); + } + + /// See [Mouse.moveTo]. + Future moveMouseTo({Element element, int xOffset, int yOffset}) async { + var params = {}; + if (element != null) { + params['element'] = _elementId(element); + } + if (xOffset != null) { + params['xoffset'] = xOffset.ceil(); + } + if (yOffset != null) { + params['yoffset'] = yOffset.ceil(); + } + await _driver.postRequest('moveto', params); + } + + /// See [Mouse.down]. + Future mouseDown(int button) async { + await _driver.mouse.down(button); + } + + /// See [Mouse.up]. + Future mouseUp(int button) async { + await _driver.mouse.up(button); + } + + /// Take a screenshot and save it to [filename]. + Future screenshot(String filename) async { + await _driver.postRequest('screenshot', {'file': filename}); + } + + /// Capture the current page source and save it to [filename]. + Future sourceDump(String filename) async { + await _driver.postRequest('source', {'file': filename}); + } + + String _elementId(Element element) { + var elementId = element.attributes[_wdIdAttribute]; + if (elementId == null) { + elementId = 'element-${_nextId++}'; + element.attributes[_wdIdAttribute] = elementId; + } + return elementId; + } +} diff --git a/lib/test/loader.dart b/lib/test/loader.dart new file mode 100644 index 00000000..8550c5b4 --- /dev/null +++ b/lib/test/loader.dart @@ -0,0 +1,180 @@ +// Copyright (c) 2015, 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. + +// TODO(DrMarcII) This is forked from loader.dart in the 'test' package. +// When 'test' supports more extension points, remove/replace this. +library test.runner.loader; + +import 'dart:async'; +import 'dart:io'; + +import 'package:analyzer/analyzer.dart'; +import 'package:path/path.dart' as p; + +import 'package:test/src/backend/invoker.dart'; +import 'package:test/src/backend/metadata.dart'; +import 'package:test/src/backend/suite.dart'; +import 'package:test/src/backend/test_platform.dart'; +import 'package:test/src/util/io.dart'; +import 'package:test/src/util/isolate_wrapper.dart'; +import 'package:test/src/utils.dart'; +import 'package:test/src/runner/load_exception.dart'; +import 'package:test/src/runner/parse_metadata.dart'; + +import 'server.dart'; + +/// A class for finding test files and loading them into a runnable form. +class Loader { + /// All platforms for which tests should be loaded. + final List _platforms; + + /// Whether to enable colors for Dart compilation. + final bool _color; + + /// The root directory that will be served for browser tests. + final String _root; + + /// The package root to use for loading tests. + final String _packageRoot; + + /// The URL for the `pub serve` instance to use to load tests. + /// + /// This is `null` if tests should be loaded from the filesystem. + final Uri _pubServeUrl; + + /// All isolates that have been spun up by the loader. + final _isolates = new Set(); + + final String _configDir; + + /// The server that serves browser test pages. + /// + /// This is lazily initialized the first time it's accessed. + Future get _browserServer { + if (_browserServerCompleter == null) { + _browserServerCompleter = new Completer(); + BrowserServer + .start( + root: _root, + packageRoot: _packageRoot, + pubServeUrl: _pubServeUrl, + color: _color, + configDir: _configDir) + .then(_browserServerCompleter.complete) + .catchError(_browserServerCompleter.completeError); + } + return _browserServerCompleter.future; + } + Completer _browserServerCompleter; + + /// Creates a new loader. + /// + /// [root] is the root directory that will be served for browser tests. It + /// defaults to the working directory. + /// + /// If [packageRoot] is passed, it's used as the package root for all loaded + /// tests. Otherwise, it's inferred from [root]. + /// + /// If [pubServeUrl] is passed, tests will be loaded from the `pub serve` + /// instance at that URL rather than from the filesystem. + /// + /// If [color] is true, console colors will be used when compiling Dart. + /// + /// If the package root doesn't exist, throws an [ApplicationException]. + Loader(Iterable platforms, {String root, String packageRoot, + Uri pubServeUrl, bool color: false, String configDir}) + : _platforms = platforms.toList(), + _pubServeUrl = pubServeUrl, + _root = root == null ? p.current : root, + _packageRoot = packageRootFor(root, packageRoot), + _color = color, + _configDir = configDir; + + /// Loads all test suites in [dir]. + /// + /// This will load tests from files that end in "_test.dart". Any tests that + /// fail to load will be emitted as [LoadException]s. + Stream loadDir(String dir) { + return mergeStreams(new Directory(dir) + .listSync(recursive: true) + .map((entry) { + if (entry is! File) return new Stream.fromIterable([]); + + if (!entry.path.endsWith("_test.dart")) { + return new Stream.fromIterable([]); + } + + if (p.split(entry.path).contains('packages')) { + return new Stream.fromIterable([]); + } + + return loadFile(entry.path); + })); + } + + /// Loads a test suite from the file at [path]. + /// + /// This will emit a [LoadException] if the file fails to load. + Stream loadFile(String path) { + var suiteMetadata; + try { + suiteMetadata = parseMetadata(path); + } on AnalyzerErrorGroup catch (_) { + // Ignore the analyzer's error, since its formatting is much worse than + // the VM's or dart2js's. + suiteMetadata = new Metadata(); + } on FormatException catch (error, stackTrace) { + return new Stream.fromFuture( + new Future.error(new LoadException(path, error), stackTrace)); + } + + var controller = new StreamController(); + Future.forEach(_platforms, (platform) { + if (!suiteMetadata.testOn.evaluate(platform, os: currentOS)) { + return null; + } + + var metadata = suiteMetadata.forPlatform(platform, os: currentOS); + + // Don't load a skipped suite. + if (metadata.skip) { + controller.add(new Suite([new LocalTest(path, metadata, () {})], + path: path, platform: platform.name, metadata: metadata)); + return null; + } + + return new Future.sync(() { + if (_pubServeUrl != null && !p.isWithin('test', path)) { + throw new LoadException( + path, 'When using "pub serve", all test files must be in test/.'); + } + assert(platform.isBrowser); + return _loadBrowserFile(path, platform, metadata); + }).then((suite) { + if (suite != null) controller.add(suite); + }).catchError(controller.addError); + }).then((_) => controller.close()); + + return controller.stream; + } + + /// Load the test suite at [path] in [platform]. + /// + /// [metadata] is the suite-level metadata for the test. + Future _loadBrowserFile( + String path, TestPlatform platform, Metadata metadata) => _browserServer + .then( + (browserServer) => browserServer.loadSuite(path, platform, metadata)); + + /// Closes the loader and releases all resources allocated by it. + Future close() { + for (var isolate in _isolates) { + isolate.kill(); + } + _isolates.clear(); + + if (_browserServerCompleter == null) return new Future.value(); + return _browserServer.then((browserServer) => browserServer.close()); + } +} diff --git a/lib/test/server.dart b/lib/test/server.dart new file mode 100644 index 00000000..ac8b4e54 --- /dev/null +++ b/lib/test/server.dart @@ -0,0 +1,453 @@ +// Copyright (c) 2015, 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. + +// TODO(DrMarcII) This is forked from server.dart in the 'test' package. +// When 'test' supports more extension points, remove/replace this. +library test.runner.browser.server; + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:path/path.dart' as p; +import 'package:pool/pool.dart'; +import 'package:shelf/shelf.dart' as shelf; +import 'package:shelf/shelf_io.dart' as shelf_io; +import 'package:shelf_static/shelf_static.dart'; +import 'package:shelf_web_socket/shelf_web_socket.dart'; + +import 'package:test/src/backend/metadata.dart'; +import 'package:test/src/backend/suite.dart'; +import 'package:test/src/backend/test_platform.dart'; +import 'package:test/src/util/io.dart'; +import 'package:test/src/util/path_handler.dart'; +import 'package:test/src/util/one_off_handler.dart'; +import 'package:test/src/utils.dart'; +import 'package:test/src/runner/application_exception.dart'; +import 'package:test/src/runner/load_exception.dart'; +import 'package:test/src/runner/browser/browser.dart'; +import 'package:test/src/runner/browser/browser_manager.dart'; +import 'package:test/src/runner/browser/compiler_pool.dart'; + +import 'webdriver.dart'; + +/// A server that serves JS-compiled tests to browsers. +/// +/// A test suite may be loaded for a given file using [loadSuite]. +class BrowserServer { + /// Starts the server. + /// + /// [root] is the root directory that the server should serve. It defaults to + /// the working directory. + /// + /// If [packageRoot] is passed, it's used for all package imports when + /// compiling tests to JS. Otherwise, the package root is inferred from + /// [root]. + /// + /// If [pubServeUrl] is passed, tests will be loaded from the `pub serve` + /// instance at that URL rather than from the filesystem. + /// + /// If [color] is true, console colors will be used when compiling Dart. + /// + /// If the package root doesn't exist, throws an [ApplicationException]. + static Future start({String root, String packageRoot, + Uri pubServeUrl, bool color: false, String configDir}) { + var server = + new BrowserServer._(root, packageRoot, pubServeUrl, color, configDir); + return server._load().then((_) => server); + } + + /// The underlying HTTP server. + HttpServer _server; + + /// A randomly-generated secret. + /// + /// This is used to ensure that other users on the same system can't snoop + /// on data being served through this server. + final _secret = randomBase64(24, urlSafe: true); + + /// The URL for this server. + Uri get url => + baseUrlForAddress(_server.address, _server.port).resolve(_secret + "/"); + + /// A [OneOffHandler] for servicing WebSocket connections for + /// [BrowserManager]s. + /// + /// This is one-off because each [BrowserManager] can only connect to a single + /// WebSocket, + final _webSocketHandler = new OneOffHandler(); + + /// A [PathHandler] used to serve compiled JS. + final _jsHandler = new PathHandler(); + + /// The [CompilerPool] managing active instances of `dart2js`. + /// + /// This is `null` if tests are loaded from `pub serve`. + final CompilerPool _compilers; + + /// The temporary directory in which compiled JS is emitted. + final String _compiledDir; + + /// The root directory served statically by this server. + final String _root; + + /// The package root. + final String _packageRoot; + + final String _configDir; + + /// The URL for the `pub serve` instance to use to load tests. + /// + /// This is `null` if tests should be compiled manually. + final Uri _pubServeUrl; + + /// The pool of active `pub serve` compilations. + /// + /// Pub itself ensures that only one compilation runs at a time; we just use + /// this pool to make sure that the output is nice and linear. + final _pubServePool = new Pool(1); + + /// The HTTP client to use when caching JS files in `pub serve`. + final HttpClient _http; + + /// Whether [close] has been called. + bool get _closed => _closeCompleter != null; + + /// The completer for the [Future] returned by [close]. + Completer _closeCompleter; + + /// All currently-running browsers. + /// + /// These are controlled by [_browserManager]s. + final _browsers = new Map(); + + /// A map from browser identifiers to futures that will complete to the + /// [BrowserManager]s for those browsers. + /// + /// This should only be accessed through [_browserManagerFor]. + final _browserManagers = new Map>(); + + /// A map from test suite paths to Futures that will complete once those + /// suites are finished compiling. + /// + /// This is used to make sure that a given test suite is only compiled once + /// per run, rather than one per browser per run. + final _compileFutures = new Map(); + + BrowserServer._(String root, String packageRoot, Uri pubServeUrl, bool color, + this._configDir) + : _root = root == null ? p.current : root, + _packageRoot = packageRootFor(root, packageRoot), + _pubServeUrl = pubServeUrl, + _compiledDir = pubServeUrl == null ? createTempDir() : null, + _http = pubServeUrl == null ? null : new HttpClient(), + _compilers = new CompilerPool(color: color); + + /// Starts the underlying server. + Future _load() { + var cascade = new shelf.Cascade().add(_webSocketHandler.handler); + + if (_pubServeUrl == null) { + cascade = cascade + .add(_webDriverHandler) + .add(_createPackagesHandler()) + .add(_jsHandler.handler) + .add(createStaticHandler(_root)) + .add(_wrapperHandler); + } + + var pipeline = new shelf.Pipeline() + .addMiddleware(nestingMiddleware(_secret)) + .addHandler(cascade.handler); + + return shelf_io.serve(pipeline, 'localhost', 0).then((server) { + _server = server; + }); + } + + _webDriverHandler(shelf.Request request) { + var cascade = new shelf.Cascade(); + var atLeastOne = false; + for (var browser in _browsers.values) { + if (browser is WebDriver) { + atLeastOne = true; + cascade = cascade.add(browser.handler); + } + } + if (atLeastOne) { + return cascade.handler(request); + } + return new shelf.Response.notFound(null); + } + + /// Returns a handler that serves the contents of the "packages/" directory + /// for any URL that contains "packages/". + /// + /// This is a factory so it can wrap a static handler. + shelf.Handler _createPackagesHandler() { + var staticHandler = + createStaticHandler(_packageRoot, serveFilesOutsidePath: true); + + return (request) { + var segments = p.url.split(shelfUrl(request).path); + + for (var i = 0; i < segments.length; i++) { + if (segments[i] != "packages") continue; + return staticHandler( + shelfChange(request, path: p.url.joinAll(segments.take(i + 1)))); + } + + return new shelf.Response.notFound("Not found."); + }; + } + + /// A handler that serves wrapper files used to bootstrap tests. + shelf.Response _wrapperHandler(shelf.Request request) { + var path = p.fromUri(shelfUrl(request)); + + if (path.endsWith(".browser_test.dart")) { + return new shelf.Response.ok(''' +import "package:test/src/runner/browser/iframe_listener.dart"; + +import "${p.basename(p.withoutExtension(p.withoutExtension(path)))}" as test; + +void main() { + IframeListener.start(() => test.main); +} +''', headers: {'Content-Type': 'application/dart'}); + } + + if (path.endsWith(".html")) { + var test = p.withoutExtension(path) + ".dart"; + + // Link to the Dart wrapper on Dartium and the compiled JS version + // elsewhere. + var scriptBase = + "${HTML_ESCAPE.convert(p.basename(test))}.browser_test.dart"; + var script = request.headers['user-agent'].contains('(Dart)') + ? 'type="application/dart" src="$scriptBase"' + : 'src="$scriptBase.js"'; + + return new shelf.Response.ok(''' + + + + ${HTML_ESCAPE.convert(test)} Test + + + +''', headers: {'Content-Type': 'text/html'}); + } + + return new shelf.Response.notFound('Not found.'); + } + + /// Loads the test suite at [path] on the browser [browser]. + /// + /// This will start a browser to load the suite if one isn't already running. + /// Throws an [ArgumentError] if [browser] isn't a browser platform. + Future loadSuite( + String path, TestPlatform browser, Metadata metadata) { + if (!browser.isBrowser) { + throw new ArgumentError("$browser is not a browser."); + } + + var htmlPath = p.withoutExtension(path) + '.html'; + if (new File(htmlPath).existsSync() && + !new File(htmlPath) + .readAsStringSync() + .contains('packages/test/dart.js')) { + throw new LoadException(path, + '"${htmlPath}" must contain .'); + } + + return new Future.sync(() { + if (_pubServeUrl != null) { + var suitePrefix = + p.withoutExtension(p.relative(path, from: p.join(_root, 'test'))); + var jsUrl = + _pubServeUrl.resolve('$suitePrefix.dart.browser_test.dart.js'); + return _pubServeSuite(path, jsUrl) + .then((_) => _pubServeUrl.resolveUri(p.toUri('$suitePrefix.html'))); + } + + return new Future.sync(() => browser.isJS ? _compileSuite(path) : null) + .then((_) { + if (_closed) return null; + return url.resolveUri(p.toUri( + p.withoutExtension(p.relative(path, from: _root)) + ".html")); + }); + }).then((suiteUrl) { + if (_closed) return null; + + // TODO(nweiz): Don't start the browser until all the suites are compiled. + return _browserManagerFor(browser).then((browserManager) { + if (_closed || browserManager == null) return null; + return browserManager.loadSuite(path, suiteUrl, metadata); + }).then((suite) { + if (_closed) return null; + if (suite != null) return suite.change(platform: browser.name); + + // If the browser manager fails to load a suite and the server isn't + // closed, it's probably because the browser failed. We emit the failure + // here to ensure that it gets surfaced. + return _browsers[browser].onExit; + }); + }); + } + + /// Loads a test suite at [path] from the `pub serve` URL [jsUrl]. + /// + /// This ensures that only one suite is loaded at a time, and that any errors + /// are exposed as [LoadException]s. + Future _pubServeSuite(String path, Uri jsUrl) { + return _pubServePool.withResource(() { + var timer = new Timer(new Duration(seconds: 1), () { + print('"pub serve" is compiling $path...'); + }); + + return _http + .headUrl(jsUrl) + .then((request) => request.close()) + .whenComplete(timer.cancel) + .catchError((error, stackTrace) { + if (error is! IOException) throw error; + + var message = getErrorMessage(error); + if (error is SocketException) { + message = "${error.osError.message} " + "(errno ${error.osError.errorCode})"; + } + + throw new LoadException(path, "Error getting $jsUrl: $message\n" + 'Make sure "pub serve" is running.'); + }).then((response) { + if (response.statusCode == 200) return; + + throw new LoadException(path, + "Error getting $jsUrl: ${response.statusCode} " + "${response.reasonPhrase}\n" + 'Make sure "pub serve" is serving the test/ directory.'); + }); + }); + } + + /// Compile the test suite at [dartPath] to JavaScript. + /// + /// Once the suite has been compiled, it's added to [_jsHandler] so it can be + /// served. + Future _compileSuite(String dartPath) { + return _compileFutures.putIfAbsent(dartPath, () { + var dir = new Directory(_compiledDir).createTempSync('test_').path; + var jsPath = p.join(dir, p.basename(dartPath) + ".js"); + + return _compilers + .compile(dartPath, jsPath, packageRoot: _packageRoot) + .then((_) { + if (_closed) return; + + _jsHandler.add(p.toUri(p.relative(dartPath, from: _root)).path + + '.browser_test.dart.js', (request) { + return new shelf.Response.ok(new File(jsPath).readAsStringSync(), + headers: {'Content-Type': 'application/javascript'}); + }); + }); + }); + } + + /// Returns the [BrowserManager] for [platform], which should be a browser. + /// + /// If no browser manager is running yet, starts one. + Future _browserManagerFor(TestPlatform platform) { + var manager = _browserManagers[platform]; + if (manager != null) return manager; + + var completer = new Completer(); + + // Swallow errors, since they're already being surfaced through the return + // value and [browser.onError]. + _browserManagers[platform] = completer.future.catchError((_) {}); + var path = _webSocketHandler.create(webSocketHandler((webSocket) { + completer.complete(new BrowserManager(platform, webSocket)); + })); + + var webSocketUrl = url.replace(scheme: 'ws').resolve(path); + + var hostUrl = (_pubServeUrl == null ? url : _pubServeUrl) + .resolve('packages/test/src/runner/browser/static/index.html'); + + var browser = _newBrowser(hostUrl.replace( + queryParameters: {'managerUrl': webSocketUrl.toString()}), platform); + _browsers[platform] = browser; + + // TODO(nweiz): Gracefully handle the browser being killed before the + // tests complete. + browser.onExit.catchError((error, stackTrace) { + if (completer.isCompleted) return; + completer.completeError(error, stackTrace); + }); + + return completer.future.timeout(new Duration(seconds: 7), onTimeout: () { + throw new ApplicationException( + "Timed out waiting for ${platform.name} to connect."); + }); + } + + /// Starts the browser identified by [browser] and has it load [url]. + Browser _newBrowser(Uri url, TestPlatform browser) { + switch (browser) { + case TestPlatform.dartium: + return new WebDriver(url, + browser: browser, + configFile: p.join(_configDir, 'dartium_cfg.json')); + case TestPlatform.chrome: + return new WebDriver(url, + browser: browser, + configFile: p.join(_configDir, 'chrome_cfg.json')); + case TestPlatform.phantomJS: + return new WebDriver(url, + browser: browser, + configFile: p.join(_configDir, 'phantomjs_cfg.json')); + case TestPlatform.firefox: + return new WebDriver(url, + browser: browser, + configFile: p.join(_configDir, 'firefox_cfg.json')); + case TestPlatform.safari: + return new WebDriver(url, + browser: browser, + configFile: p.join(_configDir, 'safari_cfg.json')); + case TestPlatform.internetExplorer: + return new WebDriver(url, + browser: browser, configFile: p.join(_configDir, 'ie_cfg.json')); + default: + throw new ArgumentError("$browser is not a browser."); + } + } + + /// Closes the server and releases all its resources. + /// + /// Returns a [Future] that completes once the server is closed and its + /// resources have been fully released. + Future close() { + if (_closeCompleter != null) return _closeCompleter.future; + _closeCompleter = new Completer(); + + return Future.wait([_server.close(), _compilers.close()]).then((_) { + if (_browserManagers.isEmpty) return null; + return Future.wait(_browserManagers.keys.map((platform) { + return _browserManagers[platform] + .then((_) => _browsers[platform].close()); + })); + }).then((_) { + if (_pubServeUrl == null) { + new Directory(_compiledDir).deleteSync(recursive: true); + } else { + _http.close(); + } + + _closeCompleter.complete(); + }).catchError(_closeCompleter.completeError); + } +} diff --git a/lib/test/webdriver.dart b/lib/test/webdriver.dart new file mode 100644 index 00000000..ea91a89d --- /dev/null +++ b/lib/test/webdriver.dart @@ -0,0 +1,128 @@ +// Copyright 2015 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +library test.runner.browser.webdriver; + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:webdriver/support/async.dart'; +import 'package:webdriver/support/forwarder.dart'; +import 'package:webdriver/io.dart' as wd; + +import 'package:test/src/backend/test_platform.dart'; +import 'package:test/src/runner/browser/browser.dart'; +import 'package:shelf/shelf.dart' as shelf; + +/// A class for running a browser via WebDriver. +/// +/// Most of the communication with the browser is expected to happen via HTTP, +/// so this exposes a bare-bones API. The browser starts as soon as the class is +/// constructed, and is killed when [close] is called. +/// +/// Any errors starting or running the process are reported through [onExit]. +class WebDriver extends Browser { + final TestPlatform browser; + Future _forwarder; + + /// Starts a new WebDriver-provisioned open to the given [url], which may + /// be a [Uri] or a [String]. + /// + /// If [configFile] (a [File] or [String]) is passed, then WebDriver + /// configuration will be read from the file it exists, otherwise if + /// 'test/webdriver_cfg.json' exists configuration will be read from that + /// file, otherwise sensible defaults will be used. + /// + /// The config file should be a JSON object with the following format: + /// { + /// "address": , + /// "desired": + /// } + WebDriver(url, {configFile: 'test/webdriver_cfg.json', this.browser}) { + if (configFile is String) { + configFile = new File(configFile); + } + + Uri address; + Map desired = {}; + if (configFile is File && configFile.existsSync()) { + Map config = _readConfig(configFile); + if (config.containsKey('address')) { + address = Uri.parse(config['address']); + } + if (config.containsKey('desired')) { + desired = config['desired']; + } + } + + _driver = wd.createDriver(uri: address, desired: desired); + _forwarder = _driver.then((driver) => + new WebDriverForwarder(driver, prefix: 'webdriver/session/1')); + _onExit = () async { + var driver = await _driver; + await driver.get(url.toString()); + while (true) { + // poll WebDriver server once a second to ensure the session is still + // alive. + await driver.currentUrl; + await clock.sleep(new Duration(seconds: 1)); + } + }(); + } + + Map _readConfig(File configFile) { + Map config = JSON.decode(configFile.readAsStringSync()); + return _updateConfig(config); + } + + _updateConfig(config) { + if (config is String && config.startsWith(r'$')) { + return Platform.environment[config.substring(1)]; + } + if (config is Map) { + var result = {}; + for (var key in config.keys) { + var value = config[key]; + result[_updateConfig(key)] = _updateConfig(value); + } + return result; + } + if (config is Iterable) { + var result = []; + for (var value in config) { + result.add(_updateConfig(value)); + } + return result; + } + return config; + } + + Future _driver; + Future _onExit; + + @override + Future get onExit => _onExit; + + Future handler(shelf.Request request) async => + (await _forwarder).handler(request); + + @override + Future close() async { + await (await _driver).quit(); + try { + await onExit; + } catch (e) {} + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 4c661d83..264e865a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,15 +1,22 @@ name: webdriver -version: 0.10.0-pre.8 +version: 0.10.0-pre.9 author: Marc Fisher II description: > Provides WebDriver bindings for Dart. These use the WebDriver JSON interface, and as such, require the use of the WebDriver remote server. homepage: https://github.com/google/webdriver.dart environment: - sdk: '>=1.9.0 <2.0.0' + sdk: '>=1.10.0 <2.0.0' dependencies: + analyzer: '^0.25.0+1' + args: '^0.13.1' crypto: '^0.9.0' - matcher: '^0.12.0' -dev_dependencies: + matcher: '^0.12.0+1' path: '^1.3.5' - test: '^0.12.0' + pool: '^1.0.2' + shelf: '^0.6.1+2' + shelf_static: '^0.2.2' + shelf_web_socket: '^0.0.1+2' + stack_trace: '^1.3.2' + test: '^0.12.2' + yaml: '^2.1.2' diff --git a/test/support/forwarder_test.dart b/test/support/forwarder_test.dart index b6baa0e9..83df6d3f 100644 --- a/test/support/forwarder_test.dart +++ b/test/support/forwarder_test.dart @@ -15,9 +15,11 @@ @TestOn("vm") library webdriver.support.forwarder_test; -import 'dart:io'; +import 'dart:io' show File, HttpServer, InternetAddress, sleep; import 'package:path/path.dart' as path; +import 'package:shelf/shelf.dart' as shelf; +import 'package:shelf/shelf_io.dart' as shelf_io; import 'package:test/test.dart'; import 'package:webdriver/io.dart'; import 'package:webdriver/support/forwarder.dart'; @@ -31,36 +33,34 @@ const buttonNotClicked = 'Button not clicked'; void main() { config.config(); + String testPage = + new File(path.join('test', 'support', 'forwarder_test_page.html')) + .readAsStringSync(); + group('WebDriverForwarder', () { WebDriver driver; - WebDriverForwarder forwarder; HttpServer server; WebDriver forwardedDriver; Uri address; setUp(() async { driver = await test_util.createTestDriver(); - forwarder = - new WebDriverForwarder(driver, prefix: '/webdriver/session/1'); - - server = await HttpServer.bind(InternetAddress.ANY_IP_V4, 0); - server.listen((request) { - if (request.uri.path.startsWith('/webdriver')) { - forwarder.forward(request); - } else if (request.method == 'GET' && - request.uri.path.endsWith('test_page.html')) { - File file = new File( - path.join('test', 'support', 'forwarder_test_page.html')); - request.response - ..statusCode = HttpStatus.OK - ..headers.set('Content-type', 'text/html'); - file.openRead().pipe(request.response); - } else { - request.response - ..statusCode = HttpStatus.NOT_FOUND - ..close(); + + var cascade = new shelf.Cascade() + .add(new WebDriverForwarder(driver, + prefix: 'webdriver/session/1').handler) + .add((shelf.Request request) { + if (request.method == 'GET' && + request.url.path.endsWith('test_page.html')) { + return new shelf.Response.ok(testPage, + headers: {"Content-Type": "text/html"}); } + return new shelf.Response.notFound(null); }); + + server = + await shelf_io.serve(cascade.handler, InternetAddress.ANY_IP_V4, 0); + address = new Uri.http('localhost:${server.port}', '/webdriver/'); forwardedDriver = await fromExistingSession('1', uri: address); diff --git a/test/support/forwarder_test_page.html b/test/support/forwarder_test_page.html index 09915036..4102c94b 100644 --- a/test/support/forwarder_test_page.html +++ b/test/support/forwarder_test_page.html @@ -33,4 +33,4 @@ } - \ No newline at end of file + diff --git a/test/test/hybrid_test.dart b/test/test/hybrid_test.dart new file mode 100644 index 00000000..4acaa0dc --- /dev/null +++ b/test/test/hybrid_test.dart @@ -0,0 +1,77 @@ +// Copyright 2015 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +@TestOn("browser") +library webdriver.test.hybrid_test; + +import 'dart:html'; + +import 'package:webdriver/test/hybrid.dart'; +import 'package:test/test.dart'; + +main() { + group('Hybrid testing framework', () { + HybridDriver driver; + var textChange = []; + Element div; + var text; + var button; + + setUp(() async { + driver = await createDriver(); + div = new Element.div(); + button = new ButtonElement()..value = 'Button'; + text = new TextInputElement(); + + button.onClick.listen((_) => text.value = ''); + text.onInput.listen(textChange.add); + text.onChange.listen(textChange.add); + div + ..append(button) + ..append(text); + document.body.append(div); + }); + + tearDown(() async { + div.remove(); + div = null; + text = null; + button = null; + textChange = []; + driver = null; + }); + + test('typing sends key events', () async { + await driver.sendKeys('some keys', element: text); + expect(textChange, hasLength(greaterThan(0))); + expect(text.value, 'some keys'); + var length = textChange.length; + await driver.clear(text); + expect(textChange, hasLength(greaterThan(length))); + expect(text.value, ''); + }); + + test('clicking works', () async { + text.value = 'some keys'; + await driver.click(button); + expect(text.value, ''); + }); + + test('click/sendKeys', () async { + await driver.click(text); + await driver.sendKeys('some other keys'); + expect(text.value, 'some other keys'); + }); + }); +} diff --git a/tool/travis.sh b/tool/travis.sh index c427b2e2..a85192ac 100755 --- a/tool/travis.sh +++ b/tool/travis.sh @@ -27,4 +27,5 @@ chromedriver --port=4444 --url-base=wd/hub & # Run tests # TODO(DrMarcII) enable running tests in browser when chrome setuid problem # is fixed on travis. -pub run test -r expanded -p vm +pub run test:test -r expanded -p vm +pub run webdriver:test -r expanded -p chrome test/support/async_test.dart test/test/hybrid_test.dart