diff --git a/.babelrc b/.babelrc
new file mode 100644
index 0000000..50f2ab3
--- /dev/null
+++ b/.babelrc
@@ -0,0 +1,3 @@
+{
+ "only": "test"
+}
diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 0000000..68d14a4
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,9 @@
+{
+ "extends": "airbnb/legacy",
+ "rules": {
+ "id-length": 0,
+ "vars-on-top": 0,
+ "strict": [2, "global"],
+ "no-param-reassign": 0
+ }
+}
diff --git a/.gitignore b/.gitignore
index dabed5d..3af2c89 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,5 @@ node_modules
npm-debug.log
test/tmp
examples/**/log
+test/log
+test/**/*.coffee
diff --git a/.testiumrc b/.testiumrc
new file mode 100644
index 0000000..66e7106
--- /dev/null
+++ b/.testiumrc
@@ -0,0 +1,6 @@
+{
+ "launch": true,
+ "app": {
+ "command": "testium-example-app"
+ }
+}
diff --git a/README.md b/README.md
index 4732bf1..ac044a5 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,63 @@
# Testium: Sync
+
+Provides a sync API for [testium](https://www.npmjs.com/package/testium).
+Uses [webdriver-http-sync](https://www.npmjs.com/package/webdriver-http-sync) for interacting with selenium.
+
+Check out [testium's documentation](https://www.npmjs.com/package/testium) for more information on the `browser` API.
+
+## Usage
+
+```js
+import initTestium from 'testium-core';
+import createDriver from 'testium-driver-sync';
+
+async function runTests() {
+ const { browser } = initTestium().then(createDriver);
+ browser.navigateTo('/');
+}
+runTests();
+```
+
+```coffee
+initTestium = require 'testium-core';
+createDriver = require 'testium-driver-sync';
+
+initTestium()
+ .then(createDriver)
+ .then ({browser}) ->
+ browser.navigateTo '/'
+```
+
+## License *(BSD-3-Clause)*
+
+```
+Copyright (c) 2015, Groupon, Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+Redistributions of source code must retain the above copyright notice,
+this list of conditions and the following disclaimer.
+
+Redistributions in binary form must reproduce the above copyright
+notice, this list of conditions and the following disclaimer in the
+documentation and/or other materials provided with the distribution.
+
+Neither the name of GROUPON nor the names of its contributors may be
+used to endorse or promote products derived from this software without
+specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+```
diff --git a/lib/assert/element.js b/lib/assert/element.js
new file mode 100644
index 0000000..3b763f6
--- /dev/null
+++ b/lib/assert/element.js
@@ -0,0 +1,176 @@
+'use strict';
+
+var util = require('util');
+
+var assert = require('assertive');
+var _ = require('lodash');
+
+function isTextOrRegexp(textOrRegExp) {
+ return _.isString(textOrRegExp) || _.isRegExp(textOrRegExp);
+}
+
+exports._getElementWithProperty = function _getElementWithProperty(selector, property) {
+ var element = this._getElement(selector);
+ return [ element, element.get(property) ];
+};
+
+exports._getElement = function _getElement(selector) {
+ var elements = this.driver.getElements(selector);
+ var count = elements.length;
+
+ if (count === 0) {
+ throw new Error('Element not found for selector: ' + selector);
+ }
+ if (count !== 1) {
+ throw new Error(
+ 'assertion needs a unique selector!\n' +
+ selector + ' has ' + count + ' hits in the page');
+ }
+
+ return elements[0];
+};
+
+exports.elementHasAttributes = function elementHasAttributes(doc, selector, attributesObject) {
+ if (arguments.length === 2) {
+ attributesObject = selector;
+ selector = doc;
+ doc = [
+ 'elementHasAttributes - selector:' + selector,
+ 'attributesObject:' + JSON.stringify(attributesObject),
+ ].join('\n');
+ } else {
+ assert.hasType('elementHasAttributes(docstring, selector, attributesObject) - requires String docstring', String, doc);
+ }
+
+ assert.hasType('elementHasAttributes(selector, attributesObject) - requires String selector', String, selector);
+ assert.hasType('elementHasAttributes(selector, attributesObject) - requires Object attributesObject', Object, attributesObject);
+
+ var element = this._getElement(selector);
+
+ _.each(attributesObject, function verifyAttribute(val, attribute) {
+ var actualVal = element.get(attribute);
+ var attrDoc = util.format(
+ '%s\nattribute %j was expected to be %j but was %j.',
+ doc, attribute, val, actualVal);
+
+ if (_.isString(val)) {
+ assert.equal(attrDoc, val, actualVal);
+ } else {
+ assert.hasType('elementHasAttributes(selector, attributesObject) - attributesObject requires String or RegExp value', RegExp, val);
+ assert.match(attrDoc, val, actualVal);
+ }
+ });
+
+ return element;
+};
+
+exports.elementHasText = function elementHasText(doc, selector, textOrRegExp) {
+ if (arguments.length === 2) {
+ textOrRegExp = selector;
+ selector = doc;
+ doc = 'elementHasText: ' + selector;
+ } else {
+ assert.hasType('elementHasText(docstring, selector, textOrRegExp) - requires docstring', String, doc);
+ }
+
+ assert.hasType('elementHasText(selector, textOrRegExp) - requires selector', String, selector);
+ assert.truthy('elementHasText(selector, textOrRegExp) - requires textOrRegExp', isTextOrRegexp(textOrRegExp));
+
+ var result = this._getElementWithProperty(selector, 'text');
+
+ if (textOrRegExp === '') {
+ assert.equal(textOrRegExp, result[1]);
+ } else {
+ assert.include(doc, textOrRegExp, result[1]);
+ }
+
+ return result[0];
+};
+
+exports.elementLacksText = function elementLacksText(doc, selector, textOrRegExp) {
+ if (arguments.length === 2) {
+ textOrRegExp = selector;
+ selector = doc;
+ doc = 'elementLacksText: ' + selector;
+ } else {
+ assert.hasType('elementLacksText(docstring, selector, textOrRegExp) - requires docstring', String, doc);
+ }
+
+ assert.hasType('elementLacksText(selector, textOrRegExp) - requires selector', String, selector);
+ assert.truthy('elementLacksText(selector, textOrRegExp) - requires textOrRegExp', isTextOrRegexp(textOrRegExp));
+
+ var result = this._getElementWithProperty(selector, 'text');
+
+ assert.notInclude(doc, textOrRegExp, result[1]);
+ return result[0];
+};
+
+exports.elementHasValue = function elementHasValue(doc, selector, textOrRegExp) {
+ if (arguments.length === 2) {
+ textOrRegExp = selector;
+ selector = doc;
+ doc = 'elementHasValue: ' + selector;
+ } else {
+ assert.hasType('elementHasValue(docstring, selector, textOrRegExp) - requires docstring', String, doc);
+ }
+
+ assert.hasType('elementHasValue(selector, textOrRegExp) - requires selector', String, selector);
+ assert.truthy('elementHasValue(selector, textOrRegExp) - requires textOrRegExp', isTextOrRegexp(textOrRegExp));
+
+ var result = this._getElementWithProperty(selector, 'value');
+
+ if (textOrRegExp === '') {
+ assert.equal(textOrRegExp, result[1]);
+ } else {
+ assert.include(doc, textOrRegExp, result[1]);
+ }
+
+ return result[0];
+};
+
+exports.elementLacksValue = function elementLacksValue(doc, selector, textOrRegExp) {
+ if (arguments.length === 2) {
+ textOrRegExp = selector;
+ selector = doc;
+ doc = 'elementLacksValue: ' + selector;
+ } else {
+ assert.hasType('elementLacksValue(docstring, selector, textOrRegExp) - requires docstring', String, doc);
+ }
+
+ assert.hasType('elementLacksValue(selector, textOrRegExp) - requires selector', String, selector);
+ assert.truthy('elementLacksValue(selector, textOrRegExp) - requires textOrRegExp', isTextOrRegexp(textOrRegExp));
+
+ var result = this._getElementWithProperty(selector, 'value');
+
+ assert.notInclude(doc, textOrRegExp, result[1]);
+ return result[0];
+};
+
+exports.elementIsVisible = function elementIsVisible(selector) {
+ assert.hasType('elementIsVisible(selector) - requires (String) selector', String, selector);
+ var element = this.browser.getElementWithoutError(selector);
+ assert.truthy('Element not found for selector: ' + selector, element);
+ assert.truthy('Element should be visible for selector: ' + selector, element.isVisible());
+ return element;
+};
+
+exports.elementNotVisible = function elementNotVisible(selector) {
+ assert.hasType('elementNotVisible(selector) - requires (String) selector', String, selector);
+ var element = this.browser.getElementWithoutError(selector);
+ assert.truthy('Element not found for selector: ' + selector, element);
+ assert.falsey('Element should not be visible for selector: ' + selector, element.isVisible());
+ return element;
+};
+
+exports.elementExists = function elementExists(selector) {
+ assert.hasType('elementExists(selector) - requires (String) selector', String, selector);
+ var element = this.browser.getElementWithoutError(selector);
+ assert.truthy('Element not found for selector: ' + selector, element);
+ return element;
+};
+
+exports.elementDoesntExist = function elementDoesntExist(selector) {
+ assert.hasType('elementDoesntExist(selector) - requires (String) selector', String, selector);
+ var element = this.browser.getElementWithoutError(selector);
+ assert.falsey('Element found for selector: ' + selector, element);
+};
diff --git a/lib/assert/imgLoaded.js b/lib/assert/imgLoaded.js
new file mode 100644
index 0000000..95c219c
--- /dev/null
+++ b/lib/assert/imgLoaded.js
@@ -0,0 +1,34 @@
+'use strict';
+
+var assert = require('assertive');
+var imgLoadedFn = require('./imgLoaded_client');
+var _ = require('lodash');
+
+exports.imgLoaded = function imgLoaded(doc, selector) {
+ if (arguments.length === 1) {
+ selector = doc;
+ doc = undefined;
+ }
+ assert.hasType('imgLoaded(selector) - requires (String) selector', String, selector);
+
+ var result = this.browser.evaluate(selector, imgLoadedFn);
+ if (result === true) {
+ return;
+ }
+
+ function fail(help) {
+ var message = 'imgLoaded ' + JSON.stringify(selector) + ': ' + help;
+ if (doc) {
+ message = doc + '\n' + message;
+ }
+ throw new Error(message);
+ }
+
+ if (result === 0) {
+ fail('element not found');
+ } else if (_.isNumber(result)) {
+ fail('non-unique selector; count: ' + result);
+ } else {
+ fail('failed to load ' + result);
+ }
+};
diff --git a/lib/assert/imgLoaded_client.js b/lib/assert/imgLoaded_client.js
new file mode 100644
index 0000000..fab7a04
--- /dev/null
+++ b/lib/assert/imgLoaded_client.js
@@ -0,0 +1,35 @@
+/* jshint browser: true */
+'use strict';
+
+// returns true if the image is loaded and decoded,
+// or a number, if it didn't find one single image,
+// or a helpful error if it wasn't an image,
+// or the path, if it found some non-loaded image
+module.exports = function imgLoaded(selector) {
+ function describe(elem) {
+ var tag = (elem.tagName || '').toLowerCase();
+ if (elem.id) {
+ tag += '#' + elem.id;
+ }
+ var classes = elem.className.replace(/^\s+|\s+$/g, '');
+ if (classes) {
+ tag += '.' + classes.replace(/\s+/g, '.');
+ }
+ return tag;
+ }
+
+ var imgs = document.querySelectorAll(selector);
+ if (imgs.length !== 1) {
+ return imgs.length;
+ }
+ var img = imgs[0];
+
+ if (!img.src) {
+ if ((img.tagName || '').match(/^img$/i)) {
+ return 'src-less ' + describe(img);
+ }
+ return 'non-image ' + describe(img);
+ }
+
+ return img.complete && img.naturalWidth ? true : img.src;
+};
diff --git a/lib/assert/index.js b/lib/assert/index.js
new file mode 100644
index 0000000..0ec646e
--- /dev/null
+++ b/lib/assert/index.js
@@ -0,0 +1,18 @@
+'use strict';
+
+var _ = require('lodash');
+
+function Assertions(driver, browser) {
+ this.driver = driver;
+ this.browser = browser;
+}
+
+_.each([
+ require('./element'),
+ require('./imgLoaded'),
+ require('./navigation'),
+], function applyMixin(mixin) {
+ _.extend(Assertions.prototype, mixin);
+});
+
+module.exports = Assertions;
diff --git a/lib/assert/navigation.js b/lib/assert/navigation.js
new file mode 100644
index 0000000..6b09225
--- /dev/null
+++ b/lib/assert/navigation.js
@@ -0,0 +1,9 @@
+'use strict';
+
+var assert = require('assertive');
+
+exports.httpStatus = function httpStatus(expectedStatus) {
+ assert.hasType('assert.httpStatus(status) - requires (Number) status', Number, expectedStatus);
+ var actualStatus = this.browser.getStatusCode();
+ assert.equal('statuscode', expectedStatus, actualStatus);
+};
diff --git a/lib/browser/alert.js b/lib/browser/alert.js
new file mode 100644
index 0000000..9949f7a
--- /dev/null
+++ b/lib/browser/alert.js
@@ -0,0 +1,8 @@
+'use strict';
+
+exports._forwarded = [
+ 'acceptAlert',
+ 'dismissAlert',
+ 'getAlertText',
+ 'typeAlert',
+];
diff --git a/lib/browser/cookie.js b/lib/browser/cookie.js
new file mode 100644
index 0000000..f04c87f
--- /dev/null
+++ b/lib/browser/cookie.js
@@ -0,0 +1,59 @@
+'use strict';
+
+var _ = require('lodash');
+var assert = require('assertive');
+
+var getTestiumCookie = require('testium-cookie').getTestiumCookie;
+
+exports._forwarded = [
+ // TODO: Port validateCookie to webdriver-http-sync
+ 'setCookie',
+ 'clearCookies',
+];
+
+exports.setCookies = function setCookies(cookies) {
+ _.each(cookies, this.setCookie, this);
+ return this;
+};
+
+exports.getCookie = function getCookie(name) {
+ assert.hasType('getCookie(name) - requires (String) name', String, name);
+
+ var cookies = this.driver.getCookies();
+ return _.find(cookies, { name: name });
+};
+
+exports.getCookies = function getCookies() {
+ return _.reject(this.driver.getCookies(), { name: '_testium_' });
+};
+
+exports.clearCookie = function clearCookie(name) {
+ return this.setCookie({
+ name: name,
+ value: 'dummy', // setCookie doesn't allow null values
+ expiry: 0,
+ });
+};
+
+// BEGIN _testium_ cookie magic
+
+exports._getTestiumCookieField = function _getTestiumCookieField(name) {
+ var cookies = this.driver.getCookies();
+ var testiumCookie = getTestiumCookie(cookies);
+ return testiumCookie[name];
+};
+
+exports.getStatusCode = function getStatusCode() {
+ return this._getTestiumCookieField('statusCode');
+};
+
+exports.getHeaders = function getHeaders() {
+ return this._getTestiumCookieField('headers');
+};
+
+exports.getHeader = function getHeader(name) {
+ assert.hasType('getHeader(name) - require (String) name', String, name);
+ return this.getHeaders()[name];
+};
+
+// END _testium_ cookie magic
diff --git a/lib/browser/debug/console.js b/lib/browser/debug/console.js
new file mode 100644
index 0000000..1bc001d
--- /dev/null
+++ b/lib/browser/debug/console.js
@@ -0,0 +1,31 @@
+'use strict';
+
+var _ = require('lodash');
+
+var logMap = {
+ 'SEVERE': 'error',
+ 'WARNING': 'warn',
+ 'INFO': 'log',
+ 'DEBUG': 'debug',
+};
+
+function convertLogType(log) {
+ if (log.level) {
+ log.type = logMap[log.level];
+ delete log.level;
+ }
+ return log;
+}
+
+exports.parseLogs = function parseLogs(logs) {
+ return _.map(logs, convertLogType);
+};
+
+exports.filterLogs = function filterLogs(logs, type) {
+ if (!type) {
+ return { matched: logs };
+ }
+ return _.groupBy(logs, function byMatched(log) {
+ return log.type === type ? 'matched' : 'rest';
+ });
+};
diff --git a/lib/browser/debug/index.js b/lib/browser/debug/index.js
new file mode 100644
index 0000000..7282eed
--- /dev/null
+++ b/lib/browser/debug/index.js
@@ -0,0 +1,29 @@
+'use strict';
+
+var assert = require('assertive');
+var _private = require('./console');
+
+var parseLogs = _private.parseLogs;
+var filterLogs = _private.filterLogs;
+
+var TYPES = [
+ 'error',
+ 'warn',
+ 'log',
+ 'debug',
+];
+
+var cachedLogs = [];
+
+exports.getConsoleLogs = function getConsoleLogs(type) {
+ if (type) {
+ assert.include(type, TYPES);
+ }
+
+ var newLogs = parseLogs(this.driver.getConsoleLogs());
+ var logs = cachedLogs.concat(newLogs);
+
+ var filtered = filterLogs(logs, type);
+ cachedLogs = filtered.rest || [];
+ return filtered.matched || [];
+};
diff --git a/lib/browser/element.js b/lib/browser/element.js
new file mode 100644
index 0000000..2ea735f
--- /dev/null
+++ b/lib/browser/element.js
@@ -0,0 +1,123 @@
+'use strict';
+
+var util = require('util');
+
+var assert = require('assertive');
+var _ = require('lodash');
+
+var STALE_MESSAGE = /stale element reference/;
+
+var NOT_FOUND_MESSAGE = new RegExp([
+ 'Unable to locate element', // firefox message
+ 'Unable to find element', // phantomjs message
+ 'no such element', // chrome message
+].join('|'));
+
+function visiblePredicate(shouldBeVisible, element) {
+ return element && element.isVisible() === shouldBeVisible;
+}
+
+function visibleFailure(shouldBeVisible, selector, timeout) {
+ throw new Error(util.format('Timeout (%dms) waiting for element (%s) to %sbe visible.',
+ timeout, selector, shouldBeVisible ? '' : 'not '));
+}
+
+function elementExistsPredicate(element) {
+ return !!element;
+}
+
+function elementExistsFailure(selector, timeout) {
+ throw new Error(util.format('Timeout (%dms) waiting for element (%s) to exist in page.',
+ timeout, selector));
+}
+
+// Curry some functions for later use
+var isVisiblePredicate = _.partial(visiblePredicate, true);
+var isntVisiblePredicate = _.partial(visiblePredicate, false);
+
+var isVisibleFailure = _.partial(visibleFailure, true);
+var isntVisibleFailure = _.partial(visibleFailure, false);
+
+exports._forwarded = [
+ // TODO: port type assertion for selector to webdriver-http-sync
+ 'getElements',
+];
+
+exports.getElementWithoutError = function getElementWithoutError(selector) {
+ // TODO: part typeof selector === string check to webdriver-http-sync
+ assert.hasType('`selector` as to be a String', String, selector);
+ try {
+ return this.driver.getElement(selector);
+ } catch (exception) {
+ var message = exception.toString();
+
+ if (NOT_FOUND_MESSAGE.test(message)) {
+ return null;
+ }
+
+ throw exception;
+ }
+};
+
+exports.getElement = exports.getElementWithoutError;
+
+exports.getExistingElement = function getExistingElement(selector) {
+ var element = this.getElement(selector);
+ assert.truthy('Element not found at selector: ' + selector, element);
+ return element;
+};
+
+exports.waitForElementVisible = function waitForElementVisible(selector, timeout) {
+ return this._waitForElement(selector, isVisiblePredicate, isVisibleFailure, timeout);
+};
+
+exports.waitForElementNotVisible = function waitForElementNotVisible(selector, timeout) {
+ return this._waitForElement(selector, isntVisiblePredicate, isntVisibleFailure, timeout);
+};
+
+exports.waitForElementExist = function waitForElementExist(selector, timeout) {
+ return this._waitForElement(selector, elementExistsPredicate, elementExistsFailure, timeout);
+};
+
+exports.click = function click(selector) {
+ return this.getExistingElement(selector).click();
+};
+
+function tryFindElement(self, selector, predicate, untilTime) {
+ var element;
+
+ while (Date.now() < untilTime) {
+ element = self.getElementWithoutError(selector);
+
+ try {
+ if (predicate(element)) {
+ return element;
+ }
+ } catch (exception) {
+ // Occasionally webdriver throws an error about the element reference being
+ // stale. Let's handle that case as the element doesn't yet exist. All
+ // other errors are re thrown.
+ if (!STALE_MESSAGE.test(exception.toString())) {
+ throw exception;
+ }
+ }
+ }
+ return null;
+}
+
+// Where predicate takes a single parameter which is an element (or null) and
+// returns true when the wait is over
+exports._waitForElement = function _waitForElement(selector, predicate, failure, timeout) {
+ assert.hasType('`selector` as to be a String', String, selector);
+ timeout = timeout || 3000;
+
+ this.driver.setElementTimeout(timeout);
+ var foundElement = tryFindElement(this, selector, predicate, Date.now() + timeout);
+ this.driver.setElementTimeout(0);
+
+ if (foundElement === null) {
+ return failure(selector, timeout);
+ }
+
+ return foundElement;
+};
diff --git a/lib/browser/index.js b/lib/browser/index.js
new file mode 100644
index 0000000..bb4ddcc
--- /dev/null
+++ b/lib/browser/index.js
@@ -0,0 +1,67 @@
+'use strict';
+
+var _ = require('lodash');
+var assert = require('assertive');
+var Bluebird = require('bluebird');
+
+var builtIns = [
+ require('./alert'),
+ require('./cookie'),
+ require('./debug'),
+ require('./element'),
+ require('./input'),
+ require('./navigation'),
+ require('./page'),
+ require('./window'),
+];
+
+function Browser(driver, options) {
+ this.driver = driver;
+ this.capabilities = driver.capabilities;
+
+ options = options || {};
+ this.appUrl = options.appUrl;
+ this._getNewPageUrl = options.getNewPageUrl;
+}
+module.exports = Browser;
+
+Browser.prototype.evaluate = function evaluate() {
+ var args = _.toArray(arguments);
+ var clientFunction = args.pop();
+
+ var invocation = 'evaluate(clientFunction) - requires (Function|String) clientFunction';
+ assert.truthy(invocation, clientFunction);
+
+ switch (typeof clientFunction) {
+ case 'function':
+ clientFunction =
+ 'return (' + clientFunction + ').apply(this, ' + JSON.stringify(args) + ');';
+ /* falls through */
+ case 'string':
+ return this.driver.evaluate(clientFunction);
+
+ default:
+ throw new Error(invocation);
+ }
+};
+
+Browser.prototype.close = function close(callback) {
+ return Bluebird
+ .try(this.driver.close, [], this.driver)
+ .nodeify(callback); // compatible with testium
+};
+
+function forwardToDriver(method) {
+ Browser.prototype[method] = function _forwarded() {
+ return this.driver[method].apply(this.driver, arguments);
+ };
+}
+
+function addBuiltIn(builtIn) {
+ var methods = _.omit(builtIn, '_forwarded');
+ if (builtIn._forwarded) {
+ _.each(builtIn._forwarded, forwardToDriver);
+ }
+ _.extend(Browser.prototype, methods);
+}
+builtIns.forEach(addBuiltIn);
diff --git a/lib/browser/input.js b/lib/browser/input.js
new file mode 100644
index 0000000..b361b9e
--- /dev/null
+++ b/lib/browser/input.js
@@ -0,0 +1,28 @@
+'use strict';
+
+var assert = require('assertive');
+var _ = require('lodash');
+
+exports.clear = function clear(selector) {
+ assert.hasType('clear(selector) - requires (String) selector', String, selector);
+ return this.getExistingElement(selector).clear();
+};
+
+exports.type = function type(selector) {
+ var keys = _.toArray(arguments).slice(1);
+ assert.hasType('type(selector, ...keys) - requires (String) selector', String, selector);
+ assert.truthy('type(selector, ...keys) - requires keys', keys.length > 0);
+ var element = this.getExistingElement(selector);
+ return element.type.apply(element, keys);
+};
+
+exports.setValue = function setValue(selector) {
+ var keys = _.toArray(arguments).slice(1);
+ assert.hasType('setValue(selector, ...keys) - requires (String) selector', String, selector);
+ assert.truthy('setValue(selector, ...keys) - requires keys', keys.length > 0);
+
+ var element = this.getExistingElement(selector);
+ element.clear();
+ return element.type.apply(element, keys);
+};
+exports.clearAndType = exports.setValue;
diff --git a/lib/browser/makeUrlRegExp.js b/lib/browser/makeUrlRegExp.js
new file mode 100644
index 0000000..c34f4f9
--- /dev/null
+++ b/lib/browser/makeUrlRegExp.js
@@ -0,0 +1,48 @@
+'use strict';
+
+var _ = require('lodash');
+
+// TODO: Figure out if there's a non-regex way to achieve the same,
+// e.g. by comparing parsed urls.
+
+function quoteRegExp(string) {
+ return string.replace(/[-\\\/\[\]{}()*+?.^$|]/g, '\\$&');
+}
+
+function bothCases(alpha) {
+ var up = alpha.toUpperCase();
+ var dn = alpha.toLowerCase();
+ return '[' + up + dn + ']';
+}
+
+var isHexaAlphaRE = /[a-f]/gi;
+function matchCharacter(uriEncoded, hex) {
+ var codepoint = parseInt(hex, 16);
+ var character = String.fromCharCode(codepoint);
+ character = quoteRegExp(character);
+ if (character === ' ') {
+ character += '|\\+';
+ }
+ return '(?:' + uriEncoded.replace(isHexaAlphaRE, bothCases) + '|' + character + ')';
+}
+
+var encodedCharRE = /%([0-9a-f]{2})/gi;
+function matchURI(stringOrRegExp) {
+ if (_.isRegExp(stringOrRegExp)) {
+ return stringOrRegExp.toString().replace(/^\/|\/\w*$/g, '');
+ }
+
+ var fullyEncoded = encodeURIComponent(stringOrRegExp);
+ return quoteRegExp(fullyEncoded).replace(encodedCharRE, matchCharacter);
+}
+
+function makeUrlRegExp(url, query) {
+ var expr = matchURI(url);
+ _.each(query || {}, function queryParamMatcher(val, key) {
+ key = matchURI(key);
+ val = matchURI(val);
+ expr += '(?=(?:\\?|.*&)' + key + '=' + val + ')';
+ });
+ return new RegExp(expr);
+}
+module.exports = makeUrlRegExp;
diff --git a/lib/browser/navigation.js b/lib/browser/navigation.js
new file mode 100644
index 0000000..ae55e7b
--- /dev/null
+++ b/lib/browser/navigation.js
@@ -0,0 +1,41 @@
+'use strict';
+
+var Url = require('url');
+
+var _ = require('lodash');
+var debug = require('debug')('testium-driver-sync:navigation');
+
+var makeUrlRegExp = require('./makeUrlRegExp');
+var waitFor = require('./wait');
+
+exports._forwarded = [
+ 'refresh',
+ 'getUrl',
+];
+
+exports.navigateTo = function navigateTo(url, options) {
+ var targetUrl = this._getNewPageUrl(url, options);
+ debug('navigateTo', targetUrl);
+ this.driver.navigateTo(targetUrl);
+
+ // Save the window handle for referencing later
+ // in `switchToDefaultWindow`
+ this.driver.rootWindow = this.driver.getCurrentWindowHandle();
+};
+
+exports.getPath = function getPath() {
+ return Url.parse(this.getUrl()).path;
+};
+
+exports.waitForUrl = function waitForUrl(url, query, timeout) {
+ if (typeof query === 'number') {
+ timeout = query;
+ } else if (_.isObject(query)) {
+ url = makeUrlRegExp(url, query);
+ }
+ return waitFor(url, 'Url', _.bindKey(this, 'getUrl'), timeout || 5000);
+};
+
+exports.waitForPath = function waitForPath(path, timeout) {
+ return waitFor(path, 'Path', _.bindKey(this, 'getPath'), timeout || 5000);
+};
diff --git a/lib/browser/page.js b/lib/browser/page.js
new file mode 100644
index 0000000..ecfa936
--- /dev/null
+++ b/lib/browser/page.js
@@ -0,0 +1,9 @@
+'use strict';
+
+exports._forwarded = [
+ 'getPageTitle',
+ 'getPageSource',
+ 'getScreenshot',
+ 'setPageSize',
+ 'getPageSize',
+];
diff --git a/lib/browser/wait.js b/lib/browser/wait.js
new file mode 100644
index 0000000..ff17e13
--- /dev/null
+++ b/lib/browser/wait.js
@@ -0,0 +1,38 @@
+'use strict';
+
+var util = require('util');
+
+var _ = require('lodash');
+
+function matches(stringOrRegex, testUrl) {
+ return stringOrRegex.test(testUrl);
+}
+
+function createTest(stringOrRegex, waitingFor) {
+ if (_.isString(stringOrRegex)) {
+ return _.partial(_.isEqual, stringOrRegex);
+ } else if (_.isRegExp(stringOrRegex)) {
+ return _.partial(matches, stringOrRegex);
+ }
+ throw new Error(
+ util.format('waitFor%s(urlStringOrRegex) - requires a string or regex param',
+ waitingFor));
+}
+
+function waitFor(stringOrRegex, waitingFor, getValue, timeout) {
+ var test = createTest(stringOrRegex, waitingFor);
+
+ var start = Date.now();
+ var currentValue;
+ while ((Date.now() - start) < timeout) {
+ currentValue = getValue();
+ if (test(currentValue)) {
+ return;
+ }
+ }
+
+ throw new Error(
+ util.format('Timed out (%dms) waiting for %s (%j). Last value was: %j',
+ timeout, waitingFor.toLowerCase(), stringOrRegex, currentValue));
+}
+module.exports = waitFor;
diff --git a/lib/browser/window.js b/lib/browser/window.js
new file mode 100644
index 0000000..e6c8b11
--- /dev/null
+++ b/lib/browser/window.js
@@ -0,0 +1,24 @@
+'use strict';
+
+var assert = require('assertive');
+
+exports._forwarded = [
+ 'switchToWindow',
+ 'closeWindow',
+];
+
+exports.switchToDefaultFrame = function switchToDefaultFrame() {
+ return this.driver.switchToFrame(null);
+};
+
+exports.switchToFrame = function switchToFrame(indexOrNameOrId) {
+ assert.truthy('switchToFrame(indexOrNameOrId) - requires (Number|String) indexOrNameOrId',
+ indexOrNameOrId);
+ return this.driver.switchToFrame(indexOrNameOrId);
+};
+
+exports.switchToDefaultWindow = function switchToDefaultWindow() {
+ assert.truthy('Attempted to locate the root window, but failed. Did you navigate to a URL first?',
+ this.driver.rootWindow);
+ return this.driver.switchToWindow(this.driver.rootWindow);
+};
diff --git a/lib/testium-driver-sync.js b/lib/testium-driver-sync.js
index ad9a93a..e61897f 100644
--- a/lib/testium-driver-sync.js
+++ b/lib/testium-driver-sync.js
@@ -1 +1,48 @@
'use strict';
+
+var WebDriver = require('webdriver-http-sync');
+var debug = require('debug')('testium-driver-sync:browser');
+
+var Browser = require('./browser');
+var Assertions = require('./assert');
+
+function createDriver(testium) {
+ var config = testium.config;
+
+ var seleniumUrl = testium.config.get('selenium.serverUrl');
+ var requestOptions = config.get('webdriver.requestOptions', {});
+ var desiredCapabilities = config.get('desiredCapabilities');
+
+ var driver = new WebDriver(seleniumUrl, desiredCapabilities, requestOptions);
+
+ var browser = testium.browser = new Browser(driver, {
+ appUrl: 'http://127.0.0.1:' + config.get('app.port'),
+ getNewPageUrl: testium.getNewPageUrl,
+ });
+ browser.assert = new Assertions(driver, browser);
+
+ // Default to reasonable size.
+ // This fixes some phantomjs element size/position reporting.
+ browser.setPageSize({ height: 768, width: 1024 });
+
+ var skipPriming = false;
+ var keepCookies = false;
+
+ if (skipPriming) {
+ debug('Skipping priming load');
+ } else {
+ driver.navigateTo(testium.getInitialUrl());
+ debug('Browser was primed');
+ }
+
+ if (keepCookies) {
+ debug('Keeping cookies around');
+ } else {
+ debug('Clearing cookies for clean state');
+ browser.clearCookies();
+ }
+
+ return testium;
+}
+
+module.exports = createDriver;
diff --git a/package.json b/package.json
index 16a58b4..75f2966 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,7 @@
"description": "Sync interface for testium",
"main": "lib/testium-driver-sync.js",
"scripts": {
- "test": "tap 'test/**/*.js'"
+ "test": "eslint lib test && mocha"
},
"repository": {
"type": "git",
@@ -21,5 +21,23 @@
"bugs": {
"url": "https://github.com/testiumjs/testium-driver-sync/issues"
},
- "homepage": "https://github.com/testiumjs/testium-driver-sync#readme"
+ "homepage": "https://github.com/testiumjs/testium-driver-sync#readme",
+ "devDependencies": {
+ "babel-core": "^5.8.25",
+ "babel-eslint": "^4.1.3",
+ "eslint": "^1.6.0",
+ "eslint-config-airbnb": "~0.1.0",
+ "mocha": "^2.3.3",
+ "node-static": "~0.7.7",
+ "testium-core": "^1.1.2",
+ "testium-example-app": "^1.0.4"
+ },
+ "dependencies": {
+ "assertive": "^2.0.2",
+ "bluebird": "~2.10.2",
+ "debug": "^2.2.0",
+ "lodash": "^3.10.1",
+ "testium-cookie": "^1.0.0",
+ "webdriver-http-sync": "^1.2.1"
+ }
}
diff --git a/test/.eslintrc b/test/.eslintrc
new file mode 100644
index 0000000..cdb7351
--- /dev/null
+++ b/test/.eslintrc
@@ -0,0 +1,9 @@
+{
+ "extends": "airbnb/base",
+ "env": {
+ "mocha": true
+ },
+ "rules": {
+ "id-length": 0
+ }
+}
diff --git a/test/browser/debug.js b/test/browser/debug.js
new file mode 100644
index 0000000..b388a30
--- /dev/null
+++ b/test/browser/debug.js
@@ -0,0 +1,37 @@
+import assert from 'assertive';
+import {parseLogs, filterLogs} from '../../lib/browser/debug/console';
+
+describe('parseLogs', () => {
+ it('maps log levels to browser log types', () => {
+ const logs = [ { level: 'SEVERE' } ];
+ const parsed = parseLogs(logs);
+ const log = parsed[0];
+
+ assert.equal(log.level, undefined);
+ assert.equal(log.type, 'error');
+ });
+});
+
+describe('filterLogs', () => {
+ it('returns all logs if no type is given', () => {
+ const logs = [
+ { type: 'error', message: 'something broke' },
+ { type: 'log', message: 'things are working' },
+ ];
+
+ const { matched } = filterLogs(logs);
+ assert.deepEqual(logs, matched);
+ });
+
+ it('filters logs based on type', () => {
+ const errorItem = { type: 'error', message: 'something broke' };
+ const logItem = { type: 'log', message: 'things are working' };
+ const logs = [ logItem, errorItem ];
+
+ const { matched, rest } = filterLogs(logs, 'error');
+ assert.equal(errorItem, matched[0]);
+ assert.equal(1, matched.length);
+ assert.equal(logItem, rest[0]);
+ assert.equal(1, rest.length);
+ });
+});
diff --git a/test/integration/assert/imgLoaded.test.js b/test/integration/assert/imgLoaded.test.js
new file mode 100644
index 0000000..07df077
--- /dev/null
+++ b/test/integration/assert/imgLoaded.test.js
@@ -0,0 +1,44 @@
+import {getBrowser} from '../../mini-testium-mocha';
+import assert from 'assertive';
+
+describe('imgLoaded', () => {
+ let browser;
+ before(async () => (browser = await getBrowser()));
+
+ before(() => browser.navigateTo('/'));
+
+ it('throws an error when the image was not found', () => {
+ const msg = 'imgLoaded "img.not-in-the-page": element not found';
+ const err = assert.throws(() => browser.assert.imgLoaded('img.not-in-the-page'));
+ assert.include(msg, err.message);
+ });
+
+ it('throws an error for non-unique selectors when finding multiple images', () => {
+ const msg = 'imgLoaded "img[alt][class]": non-unique selector; count: 3';
+ const err = assert.throws(() => browser.assert.imgLoaded('img[alt][class]'));
+ assert.include(msg, err.message);
+ });
+
+ it('throws an error for an image not successfully loaded / decoded', () => {
+ const msg = 'imgLoaded "img.fail": failed to load ';
+ const err = assert.throws(() => browser.assert.imgLoaded('img.fail'));
+ assert.include(msg, err.message);
+ assert.include('/non-existent-image.jpg', err.message);
+ });
+
+ it('throws a helpful error for an missing the src attribute', () => {
+ const msg = 'imgLoaded "#no": failed to load src-less img#no.src.dude';
+ const err = assert.throws(() => browser.assert.imgLoaded('#no'));
+ assert.include(msg, err.message);
+ });
+
+ it('throws a helpful error when the selector did not match an tag', () => {
+ const msg = 'imgLoaded "body": failed to load non-image body';
+ const err = assert.throws(() => browser.assert.imgLoaded('body'));
+ assert.include(msg, err.message);
+ });
+
+ it('does nothing when the image was successfully loaded and decoded', () => {
+ browser.assert.imgLoaded('img.okay');
+ });
+});
diff --git a/test/integration/console.test.js b/test/integration/console.test.js
new file mode 100644
index 0000000..9b327da
--- /dev/null
+++ b/test/integration/console.test.js
@@ -0,0 +1,44 @@
+import {getBrowser} from '../mini-testium-mocha';
+import assert from 'assertive';
+
+describe('console', () => {
+ let browser;
+ before(async () => (browser = await getBrowser()));
+
+ before(() => {
+ browser.navigateTo('/');
+ browser.assert.httpStatus(200);
+ });
+
+ // Each browser fails to implement the WebDriver spec
+ // for console.logs differently.
+ // Use at your own risk.
+ it('can all be retrieved', () => {
+ const { browserName } = browser.capabilities;
+ let logs;
+
+ switch (browserName) {
+ case 'firefox':
+ // firefox ignores this entirely
+ break;
+
+ case 'chrome':
+ logs = browser.getConsoleLogs();
+ assert.truthy('console.logs length', logs.length > 0);
+
+ logs = browser.getConsoleLogs();
+ assert.equal(0, logs.length);
+
+ browser.click('#log-button');
+
+ logs = browser.getConsoleLogs();
+ assert.truthy('console.logs length', logs.length > 0);
+ break;
+
+ default:
+ logs = browser.getConsoleLogs();
+ assert.truthy('console.logs length', logs.length > 0);
+ break;
+ }
+ });
+});
diff --git a/test/integration/cookie.test.js b/test/integration/cookie.test.js
new file mode 100644
index 0000000..5299727
--- /dev/null
+++ b/test/integration/cookie.test.js
@@ -0,0 +1,54 @@
+import {getBrowser} from '../mini-testium-mocha';
+import assert from 'assertive';
+
+describe('cookie', () => {
+ let browser;
+ before(async () => (browser = await getBrowser()));
+
+ it('can be set individually', () => {
+ browser.setCookie({
+ name: 'test_cookie',
+ value: '3',
+ });
+
+ const cookie = browser.getCookie('test_cookie');
+ assert.equal('3', cookie.value);
+ });
+
+ it('can be set in groups', () => {
+ browser.setCookies([
+ { name: 'test_cookie1', value: '5' },
+ { name: 'test_cookie2', value: '7' },
+ ]);
+
+ const cookie1 = browser.getCookie('test_cookie1');
+ const cookie2 = browser.getCookie('test_cookie2');
+
+ assert.equal('5', cookie1.value);
+ assert.equal('7', cookie2.value);
+ });
+
+ it('can be cleared as a group', () => {
+ browser.setCookie({
+ name: 'test_cookie',
+ value: '9',
+ });
+ browser.clearCookies();
+
+ const cookies = browser.getCookies();
+
+ assert.equal(0, cookies.length);
+ });
+
+ it('can be cleared individually', () => {
+ browser.setCookie({
+ name: 'test_cookie',
+ value: '4',
+ });
+
+ browser.clearCookie('test_cookie');
+
+ const cookie = browser.getCookie('test_cookie');
+ assert.falsey(cookie);
+ });
+});
diff --git a/test/integration/dialog.test.js b/test/integration/dialog.test.js
new file mode 100644
index 0000000..ff27fa5
--- /dev/null
+++ b/test/integration/dialog.test.js
@@ -0,0 +1,86 @@
+import {getBrowser} from '../mini-testium-mocha';
+import assert from 'assertive';
+
+import Config from 'testium-core/lib/config';
+
+const browserName = Config.load().get('browser');
+
+describe('dialogs', () => {
+ if (browserName === 'phantomjs') {
+ xit('skipping tests because browser phantomjs doesn\'t support alerts');
+ return;
+ }
+
+ let browser;
+ before(async () => (browser = await getBrowser()));
+
+ let target;
+ before(() => {
+ browser.navigateTo('/');
+ browser.assert.httpStatus(200);
+
+ target = browser.getElement('#alert_target');
+ browser.click('.link_to_clear_alert_target');
+ });
+
+ xdescribe('alert', () => {
+ beforeEach(() => browser.click('.link_to_open_an_alert'));
+
+ it('can get an alert text', () => {
+ const text = browser.getAlertText();
+ browser.acceptAlert();
+ assert.equal('Alert text was not found', 'An alert!', text);
+ });
+
+ it('can accept an alert', () => {
+ browser.acceptAlert();
+ assert.equal('alerted', target.get('text'));
+ });
+
+ it('can dismiss an alert', () => {
+ browser.dismissAlert();
+ assert.equal('alerted', target.get('text'));
+ });
+ });
+
+ describe('confirm', () => {
+ beforeEach(() => browser.click('.link_to_open_a_confirm'));
+
+ it('can get confirm text', () => {
+ const text = browser.getAlertText();
+ browser.acceptAlert();
+ assert.equal('Confirm text was not found', 'A confirmation!', text);
+ });
+
+ it('can accept a confirm', () => {
+ browser.acceptAlert();
+ assert.equal('confirmed', target.get('text'));
+ });
+
+ it('can dismiss a confirm', () => {
+ browser.dismissAlert();
+ assert.equal('dismissed', target.get('text'));
+ });
+ });
+
+ describe('prompt', () => {
+ beforeEach(() => browser.click('.link_to_open_a_prompt'));
+
+ it('can get prompt text', () => {
+ const text = browser.getAlertText();
+ browser.acceptAlert();
+ assert.equal('Confirm text was not found', 'A prompt!', text);
+ });
+
+ it('can send text to and accept a prompt', () => {
+ browser.typeAlert('Some words');
+ browser.acceptAlert();
+ assert.equal('Some words', target.get('text'));
+ });
+
+ it('can dismiss a prompt', () => {
+ browser.dismissAlert();
+ assert.equal('dismissed', target.get('text'));
+ });
+ });
+});
diff --git a/test/integration/element.test.js b/test/integration/element.test.js
new file mode 100644
index 0000000..1406f94
--- /dev/null
+++ b/test/integration/element.test.js
@@ -0,0 +1,260 @@
+import {getBrowser} from '../mini-testium-mocha';
+import assert from 'assertive';
+
+describe('element', () => {
+ let browser;
+ before(async () => (browser = await getBrowser()));
+
+ before(() => browser.navigateTo('/'));
+
+ it('can get an element\'s text', () => {
+ const element = browser.getElement('h1');
+ const text = element.get('text');
+ assert.equal('Element text was not found', 'Test Page!', text);
+ });
+
+ it('can get special properties from an element', () => {
+ // the "checked" property (when it doesn't exist)
+ // returns a non-standard response from selenium;
+ // let's make sure we can handle it properly
+ const element = browser.getElement('#checkbox');
+ const checked = element.get('checked');
+ assert.equal('checked is null', null, checked);
+ });
+
+ it('returns null when the element can not be found', () => {
+ const element = browser.getElement('.non-existing');
+ assert.equal('Element magically appeared on the page', null, element);
+ });
+
+ it('can get several elements', () => {
+ const elements = browser.getElements('.message');
+ assert.equal('Messages were not all found', 3, elements.length);
+ });
+
+ describe('elementIsVisible', () => {
+ it('fails if element does not exist', () => {
+ const error = assert.throws(() => browser.assert.elementIsVisible('.non-existing'));
+ const expectedError = 'Assertion failed: Element not found for selector: .non-existing\n\u001b[39;49;00mExpected \u001b[31mnull\u001b[39m to be truthy';
+ assert.equal(expectedError, error.message);
+ });
+
+ it('fails if element exists, but is not visible', () => {
+ const error = assert.throws(() => browser.assert.elementIsVisible('#hidden_thing'));
+ const expectedError = 'Assertion failed: Element should be visible for selector: #hidden_thing\n\u001b[39;49;00mExpected \u001b[31mfalse\u001b[39m to be truthy';
+ assert.equal(expectedError, error.message);
+ });
+
+ it('succeeds if element exists and is visible', () => {
+ browser.assert.elementIsVisible('h1');
+ });
+ });
+
+ describe('elementNotVisible', () => {
+ it('fails if element does not exist', () => {
+ const error = assert.throws(() => browser.assert.elementNotVisible('.non-existing'));
+ const expectedError = 'Assertion failed: Element not found for selector: .non-existing\n\u001b[39;49;00mExpected \u001b[31mnull\u001b[39m to be truthy';
+ assert.equal(expectedError, error.message);
+ });
+
+ it('fails if element exists, but is visible', () => {
+ const error = assert.throws(() => browser.assert.elementNotVisible('h1'));
+ const expectedError = 'Assertion failed: Element should not be visible for selector: h1\n\u001b[39;49;00mExpected \u001b[31mtrue\u001b[39m to be falsey';
+ assert.equal(expectedError, error.message);
+ });
+
+ it('succeeds if element exists and is not visible', () => {
+ browser.assert.elementNotVisible('#hidden_thing');
+ });
+ });
+
+ describe('elementExists', () => {
+ it('fails if element does not exist', () => {
+ const error = assert.throws(() => browser.assert.elementExists('.non-existing'));
+ const expectedError = 'Assertion failed: Element not found for selector: .non-existing\n\u001b[39;49;00mExpected \u001b[31mnull\u001b[39m to be truthy';
+ assert.equal(expectedError, error.message);
+ });
+
+ it('succeeds if element exists', () => {
+ browser.assert.elementExists('h1');
+ });
+ });
+
+ describe('elementDoesntExist', () => {
+ it('succeeds if element does not exist', () => {
+ browser.assert.elementDoesntExist('.non-existing');
+ });
+
+ it('fails if element exists', () => {
+ const error = assert.throws(() => browser.assert.elementDoesntExist('h1'));
+ const expectedError = 'Assertion failed: Element found for selector: h1\n\u001b[39;49;00mExpected \u001b[31mElement\u001b[39m to be falsey';
+ assert.equal(expectedError, error.message);
+ });
+ });
+
+ describe('elementHasText', () => {
+ it('finds and returns a single element', () => {
+ const element = browser.assert.elementHasText('.only', 'only one here');
+ assert.equal('resolve the element\'s class', 'only', element.get('class'));
+ });
+
+ it('finds an element with the wrong text', () => {
+ const error = assert.throws(() =>
+ browser.assert.elementHasText('.only', 'the wrong text'));
+
+ const expected = 'Assertion failed: elementHasText: .only\n\u001b[39;49;00minclude expected needle to be found in haystack\n- needle: \"the wrong text\"\nhaystack: \"only one here\"';
+ assert.equal(expected, error.message);
+ });
+
+ it('finds no elements', () => {
+ const error = assert.throws(() =>
+ browser.assert.elementHasText('.does-not-exist', 'some text'));
+
+ assert.equal('Element not found for selector: .does-not-exist', error.message);
+ });
+
+ it('finds many elements', () => {
+ const error = assert.throws(() =>
+ browser.assert.elementHasText('.message', 'some text'));
+
+ assert.equal('assertion needs a unique selector!\n.message has 3 hits in the page', error.message);
+ });
+
+ it('succeeds with empty string', () => {
+ browser.assert.elementHasText('#blank-input', '');
+ });
+ });
+
+ describe('elementLacksText', () => {
+ it('asserts an element lacks some text, and returns the element', () => {
+ const element = browser.assert.elementLacksText('.only', 'this text not present');
+ assert.equal('resolve the element\'s class', 'only', element.get('class'));
+ });
+
+ it('finds an element incorrectly having some text', () => {
+ const error = assert.throws(() =>
+ browser.assert.elementLacksText('.only', 'only'));
+
+ const expected = 'Assertion failed: elementLacksText: .only\n\u001b[39;49;00mnotInclude expected needle not to be found in haystack\n- needle: \"only\"\nhaystack: \"only one here\"';
+ assert.equal(expected, error.message);
+ });
+ });
+
+ describe('elementHasValue', () => {
+ it('finds and returns a single element', () => {
+ const element = browser.assert.elementHasValue('#text-input', 'initialvalue');
+ assert.equal('resolve the element\'s id', 'text-input', element.get('id'));
+ });
+
+ it('succeeds with empty string', () => {
+ browser.assert.elementHasValue('#blank-input', '');
+ });
+ });
+
+ describe('elementLacksValue', () => {
+ it('asserts an element lacks some value, and returns the element', () => {
+ const element = browser.assert.elementLacksValue('#text-input', 'this text not present');
+ assert.equal('resolve the element\'s id', 'text-input', element.get('id'));
+ });
+
+ it('finds an element incorrectly having some text', () => {
+ const error = assert.throws(() =>
+ browser.assert.elementLacksValue('#text-input', 'initialvalue'));
+
+ const expected = 'Assertion failed: elementLacksValue: #text-input\n\u001b[39;49;00mnotInclude expected needle not to be found in haystack\n- needle: \"initialvalue\"\nhaystack: \"initialvalue\"';
+ assert.equal(expected, error.message);
+ });
+ });
+
+ describe('waitForElementExist', () => {
+ before(() => browser.navigateTo('/dynamic.html'));
+
+ it('finds an element after waiting', () => {
+ browser.assert.elementNotVisible('.load_later');
+ browser.waitForElementExist('.load_later');
+ });
+
+ it('finds a hidden element', () => {
+ browser.assert.elementNotVisible('.load_never');
+ browser.waitForElementExist('.load_never');
+ });
+
+ it('fails to find an element that never exists', () => {
+ const error = assert.throws(() =>
+ browser.waitForElementExist('.does-not-exist', 10));
+ assert.equal('Timeout (10ms) waiting for element (.does-not-exist) to exist in page.', error.message);
+ });
+ });
+
+ describe('waitForElementVisible', () => {
+ before(() => browser.navigateTo('/dynamic.html'));
+
+ it('finds an element after waiting', () => {
+ browser.assert.elementNotVisible('.load_later');
+ browser.waitForElementVisible('.load_later');
+ });
+
+ it('fails to find a visible element within the timeout', () => {
+ const error = assert.throws(() =>
+ browser.waitForElementVisible('.load_never', 10));
+ assert.equal('Timeout (10ms) waiting for element (.load_never) to be visible.', error.message);
+ });
+
+ it('fails to find an element that never exists', () => {
+ const error = assert.throws(() =>
+ browser.waitForElementVisible('.does-not-exist', 10));
+ assert.equal('Timeout (10ms) waiting for element (.does-not-exist) to be visible.', error.message);
+ });
+ });
+
+ describe('waitForElementNotVisible', () => {
+ before(() => browser.navigateTo('/dynamic.html'));
+
+ it('does not find an existing element after waiting for it to disappear', () => {
+ browser.assert.elementIsVisible('.hide_later');
+ browser.waitForElementNotVisible('.hide_later');
+ });
+
+ it('fails to find a not-visible element within the timeout', () => {
+ const error = assert.throws(() =>
+ browser.waitForElementNotVisible('.hide_never', 10));
+ assert.equal('Timeout (10ms) waiting for element (.hide_never) to not be visible.', error.message);
+ });
+
+ it('fails to find an element that never exists', () => {
+ const error = assert.throws(() =>
+ browser.waitForElementNotVisible('.does-not-exist', 10));
+ assert.equal('Timeout (10ms) waiting for element (.does-not-exist) to not be visible.', error.message);
+ });
+ });
+
+ describe('#getElement', () => {
+ before(() => browser.navigateTo('/'));
+
+ it('succeeds if selector is a String', () => {
+ const element = browser.getElement('body');
+ element.getElement('.message');
+ });
+
+ it('return null if not found an element on the message element', () => {
+ const messageElement = browser.getElement('.message');
+ const element = messageElement.getElement('.message');
+ assert.falsey(element);
+ });
+ });
+
+ describe('#getElements', () => {
+ before(() => browser.navigateTo('/'));
+
+ it('succeeds if selector is a String', () => {
+ const element = browser.getElement('body');
+ element.getElements('.message');
+ });
+
+ it('return empty array if not found an element on the message element', () => {
+ const messageElement = browser.getElement('.message');
+ const elements = messageElement.getElements('.message');
+ assert.equal(0, elements.length);
+ });
+ });
+});
diff --git a/test/integration/evaluate.test.js b/test/integration/evaluate.test.js
new file mode 100644
index 0000000..4d5b695
--- /dev/null
+++ b/test/integration/evaluate.test.js
@@ -0,0 +1,22 @@
+import {getBrowser} from '../mini-testium-mocha';
+import assert from 'assertive';
+
+describe('evaluate', () => {
+ let browser;
+ before(async () => (browser = await getBrowser()));
+
+ before(() => browser.navigateTo('/'));
+
+ it('runs JavaScript passed as a String', () => {
+ const value = browser.evaluate('return 3;');
+ assert.equal(3, value);
+ });
+
+ it('runs JavaScript passed as a Function', () => {
+ assert.equal(6, browser.evaluate(() => 6));
+ });
+
+ it('runs JavaScript passed as a Function with optional prepended args', () => {
+ assert.equal(18, browser.evaluate(3, 6, (a, b) => a * b));
+ });
+});
diff --git a/test/integration/form.test.js b/test/integration/form.test.js
new file mode 100644
index 0000000..a92dd45
--- /dev/null
+++ b/test/integration/form.test.js
@@ -0,0 +1,61 @@
+import {getBrowser} from '../mini-testium-mocha';
+import assert from 'assertive';
+
+describe('evaluate', () => {
+ let browser;
+ before(async () => (browser = await getBrowser()));
+
+ before(() => {
+ browser.navigateTo('/');
+ browser.assert.httpStatus(200);
+ });
+
+ it('can get an input\'s value', () => {
+ const element = browser.getElement('#text-input');
+ const value = element.get('value');
+ assert.equal('Input value was not found', 'initialvalue', value);
+ });
+
+ it('can clear an input\'s value', () => {
+ const element = browser.getElement('#text-input');
+ element.clear();
+ const value = element.get('value');
+ assert.equal('Input value was not cleared', '', value);
+ });
+
+ it('can type into an input', () => {
+ const element = browser.getElement('#text-input');
+ element.type('new stuff');
+ const value = element.get('value');
+ assert.equal('Input value was not typed', 'new stuff', value);
+ });
+
+ it('can replace the input\'s value', () => {
+ const element = browser.getElement('#text-input');
+ const valueBefore = element.get('value');
+ assert.notEqual('Input value is already empty', '', valueBefore);
+ browser.clearAndType('#text-input', 'new stuff2');
+ const valueAfter = element.get('value');
+ assert.equal('Input value was not typed', 'new stuff2', valueAfter);
+ });
+
+ it('can get a textarea\'s value', () => {
+ const element = browser.getElement('#text-area');
+ const value = element.get('value');
+ assert.equal('Input value was not found', 'initialvalue', value);
+ });
+
+ it('can clear an textarea\'s value', () => {
+ const element = browser.getElement('#text-area');
+ element.clear();
+ const value = element.get('value');
+ assert.equal('Input value was not cleared', '', value);
+ });
+
+ it('can type into a textarea', () => {
+ const element = browser.getElement('#text-area');
+ element.type('new stuff');
+ const value = element.get('value');
+ assert.equal('Input value was not typed', 'new stuff', value);
+ });
+});
diff --git a/test/integration/header.test.js b/test/integration/header.test.js
new file mode 100644
index 0000000..8fdd653
--- /dev/null
+++ b/test/integration/header.test.js
@@ -0,0 +1,36 @@
+import {getBrowser} from '../mini-testium-mocha';
+import assert from 'assertive';
+
+describe('header', () => {
+ let browser;
+ before(async () => (browser = await getBrowser()));
+
+ describe('can be retireved', () => {
+ before(() => {
+ browser.navigateTo('/');
+ browser.assert.httpStatus(200);
+ });
+
+ it('as a group', () => {
+ const headers = browser.getHeaders();
+ const contentType = headers['content-type'];
+ assert.equal('text/html', contentType);
+ });
+
+ it('individually', () => {
+ const contentType = browser.getHeader('content-type');
+ assert.equal('text/html', contentType);
+ });
+ });
+
+ describe('can be set', () => {
+ before(() =>
+ browser.navigateTo('/echo', { headers: { 'x-something': 'that place' } }));
+
+ it('to new values', () => {
+ const source = browser.getElement('body').get('text');
+ const body = JSON.parse(source);
+ assert.equal(body.headers['x-something'], 'that place');
+ });
+ });
+});
diff --git a/test/integration/navigation.test.js b/test/integration/navigation.test.js
new file mode 100644
index 0000000..808e1d5
--- /dev/null
+++ b/test/integration/navigation.test.js
@@ -0,0 +1,119 @@
+import {getBrowser} from '../mini-testium-mocha';
+import assert from 'assertive';
+
+describe('navigation', () => {
+ let browser;
+ before(async () => (browser = await getBrowser()));
+
+ it('supports just a path', () => {
+ browser.navigateTo('/');
+ assert.equal(200, browser.getStatusCode());
+ });
+
+ it('supports query args', () => {
+ browser.navigateTo('/', { query: { 'a b': 'München', x: 0 } });
+ assert.equal(200, browser.getStatusCode());
+
+ browser.waitForPath('/?a%20b=M%C3%BCnchen&x=0', 100);
+ });
+
+ it('with a query string and query arg', () => {
+ browser.navigateTo('/?x=0', { query: { 'a b': 'München' } });
+ assert.equal(200, browser.getStatusCode());
+
+ browser.waitForPath('/?x=0&a%20b=M%C3%BCnchen', 100);
+ });
+
+ it('by clicking a link', () => {
+ browser.navigateTo('/');
+ assert.equal(200, browser.getStatusCode());
+
+ browser.click('.link-to-other-page');
+ assert.equal('/other-page.html', browser.getPath());
+ });
+
+ it('by refreshing', () => {
+ browser.navigateTo('/');
+ assert.equal(200, browser.getStatusCode());
+
+ browser.refresh();
+ assert.equal(200, browser.getStatusCode());
+
+ // No real way to assert this worked
+ });
+
+ describe('waiting for a url', () => {
+ it('can work with a string', () => {
+ browser.navigateTo('/redirect-after.html');
+ assert.equal(200, browser.getStatusCode());
+
+ browser.waitForPath('/index.html');
+ });
+
+ it('can work with a regex', () => {
+ browser.navigateTo('/redirect-after.html');
+ assert.equal(200, browser.getStatusCode());
+
+ browser.waitForUrl(/\/index.html/);
+ });
+
+ it('can fail', () => {
+ browser.navigateTo('/index.html');
+ assert.equal(200, browser.getStatusCode());
+
+ const error = assert.throws(() => browser.waitForUrl('/some-random-place.html', 5));
+ const expectedError = /^Timed out \(5ms\) waiting for url \("\/some-random-place\.html"\)\. Last value was: "http:\/\/127\.0\.0\.1:[\d]+\/index\.html"$/;
+ assert.match(expectedError, error.message);
+ });
+
+ describe('groks url and query object', () => {
+ it('can make its own query regexp', () => {
+ browser.navigateTo('/redirect-to-query.html');
+ browser.waitForUrl('/index.html', {
+ 'a b': 'A B',
+ c: '1,7',
+ });
+ assert.equal(200, browser.getStatusCode());
+ });
+
+ it('can find query arguments in any order', () => {
+ browser.navigateTo('/redirect-to-query.html');
+ browser.waitForUrl('/index.html', {
+ c: '1,7',
+ 'a b': 'A B',
+ });
+ });
+
+ it('can handle regexp query arguments', () => {
+ browser.navigateTo('/redirect-to-query.html');
+ browser.waitForUrl('/index.html', {
+ c: /[\d,]+/,
+ 'a b': 'A B',
+ });
+ });
+
+ it('detects non-matches too', () => {
+ browser.navigateTo('/redirect-to-query.html');
+
+ const error = assert.throws(() => browser.waitForUrl('/index.html', { no: 'q' }, 200));
+ assert.match(/Timed out .* waiting for url/, error.message);
+ });
+ });
+ });
+
+ describe('waiting for a path', () => {
+ it('can work with a string', () => {
+ browser.navigateTo('/redirect-after.html');
+ assert.equal(200, browser.getStatusCode());
+
+ browser.waitForPath('/index.html');
+ });
+
+ it('can work with a regex', () => {
+ browser.navigateTo('/redirect-after.html');
+ assert.equal(200, browser.getStatusCode());
+
+ browser.waitForPath(/index.html/);
+ });
+ });
+});
diff --git a/test/integration/non_browser.test.js b/test/integration/non_browser.test.js
new file mode 100644
index 0000000..0afec06
--- /dev/null
+++ b/test/integration/non_browser.test.js
@@ -0,0 +1,17 @@
+import {get} from 'http';
+
+import {getBrowser} from '../mini-testium-mocha';
+import assert from 'assertive';
+
+describe('Non-browser test', () => {
+ let browser;
+ before(async () => (browser = await getBrowser()));
+
+ it('can make a request without using the browser', done => {
+ const url = `${browser.appUrl}/echo`;
+ get(url, response => {
+ assert.equal(200, response.statusCode);
+ done();
+ }).on('error', done);
+ });
+});
diff --git a/test/integration/page_data.test.js b/test/integration/page_data.test.js
new file mode 100644
index 0000000..31bcca8
--- /dev/null
+++ b/test/integration/page_data.test.js
@@ -0,0 +1,15 @@
+import {getBrowser} from '../mini-testium-mocha';
+import assert from 'assertive';
+
+describe('header', () => {
+ let browser;
+ before(async () => (browser = await getBrowser()));
+
+ before(() => browser.navigateTo('/'));
+
+ it('title', () =>
+ assert.equal('Test Title', browser.getPageTitle()));
+
+ it('source', () =>
+ assert.include('DOCTYPE', browser.getPageSource()));
+});
diff --git a/test/integration/proxy.test.js b/test/integration/proxy.test.js
new file mode 100644
index 0000000..db49a97
--- /dev/null
+++ b/test/integration/proxy.test.js
@@ -0,0 +1,46 @@
+import {getBrowser} from '../mini-testium-mocha';
+
+describe('proxy', () => {
+ let browser;
+ before(async () => (browser = await getBrowser()));
+
+ describe('handles errors', () => {
+ it('with no content type and preserves status code', () => {
+ browser.navigateTo('/');
+ browser.assert.httpStatus(200);
+
+ browser.navigateTo('/error');
+ browser.assert.httpStatus(500);
+ });
+
+ it('that crash and preserves status code', () => {
+ browser.navigateTo('/crash');
+ browser.assert.httpStatus(500);
+ });
+ });
+
+ it('handles request abortion', done => {
+ // loads a page that has a resource that will
+ // be black holed
+ browser.navigateTo('/blackholed-resource.html');
+ browser.assert.httpStatus(200);
+
+ // this can't simply be sync
+ // because firefox blocks dom-ready
+ // if we don't wait on the client-side
+ setTimeout(() => {
+ // when navigating away, the proxy should
+ // abort the resource request;
+ // this should not interfere with the new page load
+ // or status code retrieval
+ browser.navigateTo('/');
+ browser.assert.httpStatus(200);
+ done();
+ }, 50);
+ });
+
+ it('handles hashes in urls', () => {
+ browser.navigateTo('/#deals');
+ browser.assert.httpStatus(200);
+ });
+});
diff --git a/test/integration/screenshot.test.js b/test/integration/screenshot.test.js
new file mode 100644
index 0000000..574963c
--- /dev/null
+++ b/test/integration/screenshot.test.js
@@ -0,0 +1,19 @@
+import {getBrowser} from '../mini-testium-mocha';
+import assert from 'assertive';
+
+describe('screenshots', () => {
+ let browser;
+ before(async () => (browser = await getBrowser()));
+
+ describe('taking', () => {
+ let screenshot;
+ before(() => {
+ browser.navigateTo('/');
+ browser.assert.httpStatus(200);
+ screenshot = browser.getScreenshot();
+ });
+
+ it('captures the page', () =>
+ assert.expect(screenshot.length > 0));
+ });
+});
diff --git a/test/integration/ssl.test.js b/test/integration/ssl.test.js
new file mode 100644
index 0000000..0a71bff
--- /dev/null
+++ b/test/integration/ssl.test.js
@@ -0,0 +1,14 @@
+import {getBrowser} from '../mini-testium-mocha';
+import assert from 'assertive';
+
+describe('ssl/tls', () => {
+ let browser;
+ before(async () => (browser = await getBrowser()));
+
+ it('TLS is supported', () => {
+ browser.navigateTo('https://www.howsmyssl.com/a/check');
+ const raw = browser.getExistingElement('pre').get('text');
+ const sslReport = JSON.parse(raw);
+ assert.match(/^TLS/, sslReport.tls_version);
+ });
+});
diff --git a/test/integration/unicode.test.js b/test/integration/unicode.test.js
new file mode 100644
index 0000000..db2c5f6
--- /dev/null
+++ b/test/integration/unicode.test.js
@@ -0,0 +1,17 @@
+import {getBrowser} from '../mini-testium-mocha';
+import assert from 'assertive';
+
+describe('unicode support', () => {
+ let browser;
+ before(async () => (browser = await getBrowser()));
+
+ before(() => browser.navigateTo('/'));
+
+ it('multibyte unicode can pass through and back from WebDriver', () => {
+ const multibyteText = '日本語 text';
+ const element = browser.getElement('#blank-input');
+ element.type(multibyteText);
+ const result = element.get('value');
+ assert.equal(result, multibyteText);
+ });
+});
diff --git a/test/integration/window.test.js b/test/integration/window.test.js
new file mode 100644
index 0000000..ba81e16
--- /dev/null
+++ b/test/integration/window.test.js
@@ -0,0 +1,48 @@
+import {getBrowser} from '../mini-testium-mocha';
+import assert from 'assertive';
+
+describe('window api', () => {
+ let browser;
+ before(async () => (browser = await getBrowser()));
+
+ describe('frames', () => {
+ before(() => {
+ browser.navigateTo('/windows.html');
+ browser.assert.httpStatus(200);
+ });
+
+ it('can be switched', () => {
+ browser.switchToFrame('cool-frame');
+ const iframeContent = browser.getElement('.in-iframe-only').get('text');
+ browser.switchToDefaultFrame();
+ const primaryContent = browser.getElement('.in-iframe-only');
+ assert.equal('iframe content!', iframeContent);
+ assert.equal(null, primaryContent);
+ });
+
+ it('can be found when nested', () => {
+ browser.switchToFrame('cool-frame');
+ browser.switchToFrame('nested-frame');
+ const element = browser.getElement('#nested-frame-div');
+ assert.truthy('nested frame content', element);
+ });
+ });
+
+ describe('popups', () => {
+ before(() => {
+ browser.navigateTo('/windows.html');
+ browser.assert.httpStatus(200);
+ });
+
+ it('can be opened', () => {
+ browser.click('#open-popup');
+ browser.switchToWindow('popup1');
+ const popupContent = browser.getElement('.popup-only').get('text');
+ browser.closeWindow();
+ browser.switchToDefaultWindow();
+ const primaryContent = browser.getElement('.popup-only');
+ assert.equal('popup content!', popupContent);
+ assert.equal(null, primaryContent);
+ });
+ });
+});
diff --git a/test/mini-testium-mocha.js b/test/mini-testium-mocha.js
new file mode 100644
index 0000000..3f09361
--- /dev/null
+++ b/test/mini-testium-mocha.js
@@ -0,0 +1,18 @@
+// This is a minimal version of `testium-mocha`.
+// We're trying to avoid cyclic dependencies.
+import initTestium from 'testium-core';
+import {once} from 'lodash';
+
+import createDriver from '../';
+
+let browser = null;
+
+async function createBrowser() {
+ const testium = await initTestium().then(createDriver);
+ browser = testium.browser;
+ return browser;
+}
+
+after(() => browser && browser.close());
+
+export const getBrowser = once(createBrowser);
diff --git a/test/mocha.opts b/test/mocha.opts
new file mode 100644
index 0000000..50c2600
--- /dev/null
+++ b/test/mocha.opts
@@ -0,0 +1,3 @@
+--compilers js:babel-core/register
+--recursive
+--timeout 20000