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