diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..ae2ebfe --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..302bd18 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +.DS_Store +.grunt + +lib-cov +*.seed +*.log +*.csv +*.dat +*.out +*.pid +*.gz + +pids +logs +results + +npm-debug.log +sauce_connect.log + +/.nyc_output +/node_modules +/bower_components +/coverage +/dist +/docs diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..464b4d6 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,23 @@ +sudo: required +dist: trusty +addons: + apt: + sources: + - google-chrome + packages: + - google-chrome-stable + +language: node_js +node_js: +- 7.0.0 + +before_script: + - export DBUS_SESSION_BUS_ADDRESS=/dev/null + - export DISPLAY=:99.0 + - sh -e /etc/init.d/xvfb start + - sleep 3 # give xvfb some time to start + +script: npm run coverage + +after_success: + - npm run coveralls diff --git a/README.md b/README.md new file mode 100644 index 0000000..65e890b --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +

wd-elements

+
+

Better elements for selenium-webdriver.

+
+ Build Status + Coverage Status + Standard - JavaScript Style Guide +
+
+ NPM wd-elements package + Dependency Status + devDependency Status +
+
+
+ +## Why + +Extensibility. As of now, **selenium-webdriver** exposes a single class, `WebElement`, yet we know the DOM is constructed of more than 58 different elements (thank you google). Now take into account the explosion of CustomElements (polymer, react, etc...). Our front end is built using components, and our integration tests should as well. + +The goal of **wd-elements** is to provide a sane and consistent environment to author integration tests around the concept of custom elements. + +## Basic Usage + +```js +const webdriver = require('selenium-webdriver') +const WDE = require('wd-elements')(webdriver) + +const driver = new webdriver.Builder().forBrowser('chrome').build(); +const page = WDE.Page.create(driver) + +// for the most part WDElements behave like native selenium WebElements +// the true power comes from primitives and extensibility (see advanced usage) +page.find('#app') + .then((app) => app.find('h1')) + .then((h1) => h1.getText()) +``` + +## License + +The MIT License (MIT) Copyright (c) 2016 Jarid Margolin + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..dc2e258 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,41 @@ +'use strict' + +/* ----------------------------------------------------------------------------- + * dependencies + * -------------------------------------------------------------------------- */ + +// 3rd party +const _ = require('lodash') + +// lib +const Primitives = require('./primitives') +const El = require('./primitives/el') + +/* ----------------------------------------------------------------------------- + * WDElements + * -------------------------------------------------------------------------- */ + +// add all of our base internal primitives +const primitives = new Primitives() + +// config method - lets you set the webdriver module to use - If not specified, +// will utilize module version packaged with WDElement (using mismatched +// versions may have undesired side effects). Setting the driver is recommended +module.exports = function (wd) { + El.configure(wd || require('selenium-webdriver'), primitives) + primitives.load('./primitives') + + // Return proxy of WDElemts that implements a dynamic getter to return + // an interface to interact with primitives + return new Proxy({}, { + get: function (target, name) { + if (_.isUndefined(primitives[name])) { + return primitives.get(name) + } + + return _.isFunction(primitives[name]) + ? primitives[name].bind(primitives) + : primitives[name] + } + }) +} diff --git a/lib/primitives.js b/lib/primitives.js new file mode 100644 index 0000000..27e9c75 --- /dev/null +++ b/lib/primitives.js @@ -0,0 +1,91 @@ +'use strict' + +/* ----------------------------------------------------------------------------- + * dependencies + * -------------------------------------------------------------------------- */ + +// core +const fs = require('fs') +const path = require('path') + +// 3rd party +const _ = require('lodash') +const Promise = require('bluebird') +const callsites = require('callsites') +const requireDir = require('require-directory') + +/* ----------------------------------------------------------------------------- + * Primitives + * -------------------------------------------------------------------------- */ + +module.exports = class Primitives extends Map { + + set (name, Class) { + let depth = 0 + let CurClass = Class + + // we know `El` will is our root element so we can explicitly search for it + while (CurClass.name !== 'El') { + CurClass = Object.getPrototypeOf(CurClass) + depth++ + } + + this.levels = this.levels || [] + this.levels[depth] = this.levels[depth] || [] + this.levels[depth].push(Class) + + return super.set.call(this, name, Class) + } + + load (filePath) { + if (!filePath.startsWith('/')) { + filePath = path.join(this._getCallerDir(), filePath) + } + + if (!filePath.startsWith('/')) { + filePath = path.join(process.cwd(), filePath) + } + + const stat = fs.statSync(filePath) + + return stat.isDirectory() + ? this._loadDir(filePath) + : this._loadFile(filePath) + } + + findByEl (el) { + const Classes = _.flatten(this.levels.reverse()) + const results = Promise.mapSeries(Classes, (Class) => { + return Class.matches(el).then((isMatch) => isMatch ? Class : false) + }) + + return results.then((results) => _.find(results)) + } + + /* --------------------------------------------------------------------------- + * utils + * ------------------------------------------------------------------------ */ + + _getCallerDir () { + const c = callsites() + + let fileName = c[0].getFileName() + for (let i = 1; i < c.length && fileName.includes('lib/primitives.js'); i++) { + fileName = c[i].getFileName() + } + + return path.dirname(fileName) + } + + _loadDir (dirPath) { + _.each(requireDir(module, dirPath), (Class) => { + this.set(Class.name, Class) + }) + } + + _loadFile (filePath) { + const Class = require(filePath) + this.set(Class.name, Class) + } + +} diff --git a/lib/primitives/el.js b/lib/primitives/el.js new file mode 100644 index 0000000..ab2c83c --- /dev/null +++ b/lib/primitives/el.js @@ -0,0 +1,263 @@ +'use strict' + +/* ----------------------------------------------------------------------------- + * dependencies + * -------------------------------------------------------------------------- */ + +// 3rd party +const _ = require('lodash') +const Promise = require('bluebird') + +/* ----------------------------------------------------------------------------- + * El + * -------------------------------------------------------------------------- */ + +module.exports = class El { + + static configure (wd, primitives) { + this.wd = wd + this.primitives = primitives + + this.configured = true + } + + static create (driver, selector) { + return driver.findElement({ css: selector }) + .then((el) => new this(el)) + } + + static matches (el) { + return Promise.resolve(el instanceof this.wd.WebElement) + } + + get children () { + return {} + } + + get properties () { + return ['text'] + } + + // DO NOT OVERWRITE. Intended to be a computer property of children + get definitions () { + if (!this._definitions) { + this._definitions = _.mapValues(this.children, this._appUrlition) + } + + return this._definitions + } + + constructor (el) { + if (!this.constructor.configured) { + throw new Error('WDElements must be configured prior to use') + } + + // _el should not be used directly. Doing so will result in receiving + // back a raw selenium WebElement and break the chain. + this._el = el + + this.wd = this.constructor.wd + this.driver = this._el.getDriver() + this.primitives = this.constructor.primitives + + // alias + this.find = this.findElement + this.findAll = this.findElements + + return this._getWebElementProxy() + } + + /* --------------------------------------------------------------------------- + * data + * ------------------------------------------------------------------------ */ + + data () { + const props = arguments.length ? _.toArray(arguments) : this.properties + + return Promise.props(_.reduce(props, (props, prop) => { + const parts = prop.split(':') + const getter = this[`get${_.capitalize(parts[0])}`] + const getterArgs = _.tail(parts) + + return _.set(props, prop, getter.apply(this, getterArgs)) + }, {})) + } + + /* --------------------------------------------------------------------------- + * find + * ------------------------------------------------------------------------ */ + + findElement (_by, _options) { + const { by, options } = this._normalizeFindArgs(_by, _options) + + return this._el.findElement(this._normalizeBy(by)) + .then((el) => this._createFromEl(el, options)) + } + + findElements (_by, _options) { + const { by, options } = this._normalizeFindArgs(_by, _options) + + return this._el.findElements(this._normalizeBy(by)) + .then((els) => Promise.map(els, (el) => this._createFromEl(el, options))) + } + + findLast (by, options) { + return this.findElements(by, options) + .then((els) => _.last(els)) + } + + findNth (by, n, options) { + return this.findElements(by, options) + .then((els) => els[n]) + } + + /* --------------------------------------------------------------------------- + * wait + * ------------------------------------------------------------------------ */ + + waitUntilEnabled (el) { + return this._waitUntilEl('elementIsEnabled', el) + } + + waitUntilDisabled (el) { + return this._waitUntilEl('elementIsDisabled', el) + } + + waitUntilSelected (el) { + return this._waitUntilEl('elementIsSelected', el) + } + + waitUntilNotSelected (el) { + return this._waitUntilEl('elementIsNotSelected', el) + } + + waitUntilVisible (el) { + return this._waitUntilEl('elementIsVisible', el) + } + + waitUntilNotVisible (el) { + return this._waitUntilEl('elementIsNotVisible', el) + } + + waitUntilStale (el) { + return this._waitUntilEl('stalenessOf', el) + } + + waitUntilTextContains (substr, el) { + return this._waitUntilEl('elementTextContains', el, substr) + } + + waitUntilTextIs (text, el) { + return this._waitUntilEl('elementTextIs', el, text) + } + + waitUntilTextMatches (regex, el) { + return this._waitUntilEl('elementTextMatches', el, regex) + } + + waitUntilLocated (locator) { + if (!locator) { + throw new Error('Locator must be passed') + } else if (_.isString(locator)) { + locator = { css: locator } + } + return this._waitUntil('elementLocated', locator) + } + + _waitUntilEl (name, el, arg) { + el = el || this + + return el instanceof El + ? this._waitUntil(name, el._el, arg) + : this.find(el).then((el) => this._waitUntilEl(name, el, arg)) + } + + _waitUntil (name, arg1, arg2) { + return this.driver.wait(this.wd.until[name](arg1, arg2)) + .then((el) => el instanceof this.wd.WebElement ? this._createFromEl(el) : el) + } + + /* --------------------------------------------------------------------------- + * proxy creation + * ------------------------------------------------------------------------ */ + + _getWebElementProxy () { + return new Proxy(this, { get: this._proxyToWebElement }) + } + + _proxyToWebElement (target, name) { + if (name in target) { + return target[name] + } else if (name in target._el) { + return target._el[name] + } + } + + /* --------------------------------------------------------------------------- + * utils + * ------------------------------------------------------------------------ */ + + _normalizeFindArgs (by, options) { + if (_.isString(by) && by.startsWith('@child.')) { + return this._normalizeFromDefinition(by, options) + } + + return { + by: this._normalizeBy(by), + options: options + } + } + + _normalizeFromDefinition (by, options) { + const definition = this.definitions[by.split('@child.')[1]] + + if (_.isUndefined(definition)) { + throw new Error(`No child found by the name: ${by}`) + } + + return { + by: this._normalizeBy(definition.by), + options: _.assign({}, _.omit(definition, 'by'), options) + } + } + + _normalizeBy (by) { + if (_.isFunction(by) || _.isObject(by)) { + return by + } else if (_.isString(by)) { + return { css: by } + } else { + throw new Error(`Unreconized lookup value`) + } + } + + _appUrlition (raw) { + let definition + + if (_.isObject(raw)) { + definition = raw + } else if (_.isString(raw)) { + definition = { by: raw } + } + + if (!definition) { + throw new Error(`Definition type not recognized`) + } else if (!definition.by) { + throw new Error(`Definition must contain lookup property`) + } + + return definition + } + + _createFromEl (el, options) { + return this._getElClass(el, options) + .then((Class) => new Class(el, options)) + } + + _getElClass (el, options = {}) { + return options.Class + ? Promise.resolve(options.Class) + : this.primitives.findByEl(el) + } + +} diff --git a/lib/primitives/page.js b/lib/primitives/page.js new file mode 100644 index 0000000..c3d8916 --- /dev/null +++ b/lib/primitives/page.js @@ -0,0 +1,109 @@ +'use strict' + +/* ----------------------------------------------------------------------------- + * dependencies + * -------------------------------------------------------------------------- */ + +// 3rd party +const _ = require('lodash') + +// lib +const El = require('./el') + +/* ----------------------------------------------------------------------------- + * El + * -------------------------------------------------------------------------- */ + +module.exports = class Page extends El { + + static create (driver) { + return driver.findElement({ css: 'html' }) + .then((el) => new this(el)) + } + + static matches (el) { + return el.getTagName() + .then((tagName) => tagName === 'html') + } + + /* --------------------------------------------------------------------------- + * getters + * ------------------------------------------------------------------------ */ + + getCurrentUrl () { + return this.driver.getCurrentUrl() + } + + getTitle () { + return this.driver.getTitle() + } + + getSource () { + return this.driver.getPageSource() + } + + /* --------------------------------------------------------------------------- + * actions + * ------------------------------------------------------------------------ */ + + navigateTo (url) { + return this.driver.get.apply(this.driver, arguments) + } + + refresh () { + return this.driver.navigate().refresh() + } + + executeAsyncScript () { + return this.driver.executeAsyncScript.apply(this.driver, arguments) + } + + executeScript () { + return this.driver.executeScript.apply(this.driver, arguments) + } + + close () { + return this.driver.close.apply(this.driver, arguments) + } + + /* --------------------------------------------------------------------------- + * wait + * ------------------------------------------------------------------------ */ + + waitUntilAbleToSwitchToFrame (frame) { + if (_.isString(frame)) { + frame = { css: frame } + } + + return this._waitUntil('ableToSwitchToFrame', frame) + } + + waitUntilAlertIsPresent () { + return this._waitUntil('alertIsPresent') + } + + waitUntilTitleContains (substr) { + return this._waitUntil('titleContains', substr) + } + + waitUntilTitleIs (title) { + return this._waitUntil('titleIs', title) + } + + waitUntilTitleMatches (regex) { + return this._waitUntil('titleMatches', regex) + } + + waitUntilUrlContains (substr) { + return this._waitUntil('urlContains', substr) + } + + waitUntilUrlIs (url) { + return this._waitUntil('urlIs', url) + } + + waitUntilUrlMatches (regex) { + return this._waitUntil('urlMatches', regex) + } + +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..f8ab513 --- /dev/null +++ b/package.json @@ -0,0 +1,40 @@ +{ + "name": "wd-elements", + "description": "Better elements for selenium-webdriver.", + "author": "Jarid Margolin ", + "version": "0.0.1", + "homepage": "https://github.com/jaridmargolin/wd-elements", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/jaridmargolin/wd-elements" + }, + "bugs": { + "url": "https://github.com/jaridmargolin/wd-elements/issues" + }, + "scripts": { + "test": "standard && admiral --require flvr --harmony_async_await", + "inspect-test": "inspect --require flvr --harmony_async_await admiral", + "coverage": "nyc --reporter=lcov --reporter=text npm test", + "coveralls": "cat ./coverage/lcov.info | coveralls" + }, + "main": "lib/index.js", + "dependencies": { + "bluebird": "^3.4.7", + "callsites": "^2.0.0", + "require-directory": "^2.1.1", + "selenium-webdriver": "^3.0.1" + }, + "devDependencies": { + "admiral": "0.0.4", + "admiral-integration-mocha": "0.0.2", + "admiral-target-local": "0.0.2", + "chai": "^3.5.0", + "coveralls": "^2.11.15", + "flvr": "^0.1.3", + "inspect-process": "^0.4.1", + "mocha": "^3.2.0", + "nyc": "^10.0.0", + "standard": "^8.6.0" + } +} diff --git a/test/integration/el.js b/test/integration/el.js new file mode 100644 index 0000000..3c90e22 --- /dev/null +++ b/test/integration/el.js @@ -0,0 +1,366 @@ +/* eslint-env mocha */ +'use strict' + +/* ----------------------------------------------------------------------------- + * dependencies + * -------------------------------------------------------------------------- */ + +// core +const path = require('path') + +// 3rd party +const assert = require('chai').assert +const webdriver = require('selenium-webdriver') + +// lib +const WDElements = require('../../lib/index') + +/* ----------------------------------------------------------------------------- + * reusable + * -------------------------------------------------------------------------- */ + +const WDE = WDElements(webdriver) +const appPath = path.join(__dirname, 'fixtures', 'app.html') +const appUrl = `file://${appPath}` + +/* ----------------------------------------------------------------------------- + * test + * -------------------------------------------------------------------------- */ + +describe('El', function () { + this.timeout(10000) + + /* --------------------------------------------------------------------------- + * wd + * ------------------------------------------------------------------------ */ + + describe('wd', function () { + afterEach(function () { + delete WDE.El._wd + }) + + it('Should use set wd version', function () { + const tempWD = {} + const TempWDE = WDElements(tempWD) + + assert.equal(TempWDE.El.wd, tempWD) + WDElements(webdriver) + }) + + it('Should have access to wd in subclass', function () { + const tempWD = {} + const TempWDE = WDElements(tempWD) + + class CustomEl extends TempWDE.El {} + assert.equal(CustomEl.wd, tempWD) + WDElements(webdriver) + }) + }) + + /* --------------------------------------------------------------------------- + * instantiate + * ------------------------------------------------------------------------ */ + + describe('instantiate', function () { + beforeEach(async function () { + await this.driver.get(appUrl) + }) + + it('Should create instance of El', async function () { + const app = await WDE.El.create(this.driver, '#app') + assert.instanceOf(app, WDE.El) + }) + }) + + /* --------------------------------------------------------------------------- + * data + * ------------------------------------------------------------------------ */ + + describe('data', function () { + beforeEach(async function () { + await this.driver.get(appUrl) + }) + + it('Should be able to specify getter args', async function () { + class CustomParagraph extends WDE.El { + get properties () { return ['attribute:data-test'] } + } + + const paragraph = await CustomParagraph.create(this.driver, 'p') + assert.deepEqual(await paragraph.data(), { 'attribute:data-test': 'val1' }) + }) + + it('Should return all properties', async function () { + class CustomParagraph extends WDE.El { + get properties () { return ['text', 'attribute:data-test'] } + } + + const paragraph = await CustomParagraph.create(this.driver, 'p') + assert.deepEqual(await paragraph.data(), { + 'text': 'Paragraph1', + 'attribute:data-test': 'val1' + }) + }) + + it('Should return only the specified property', async function () { + class CustomParagraph extends WDE.El { + get properties () { return ['text', 'attribute:data-test'] } + } + + const paragraph = await CustomParagraph.create(this.driver, 'p') + assert.deepEqual(await paragraph.data('text'), { + 'text': 'Paragraph1' + }) + }) + }) + + /* --------------------------------------------------------------------------- + * child interface + * ------------------------------------------------------------------------ */ + + describe('child interface', function () { + before(async function () { + await this.driver.get(appUrl) + }) + + it('Should cache access to definitions', async function () { + const app = await WDE.El.create(this.driver, '#app') + assert.equal(app.definitions, app.definitions) + }) + + it('Should get child by string definition', async function () { + class AppEl extends WDE.El { + get children () { + return { 'heading': 'h1' } + } + } + + const app = await AppEl.create(this.driver, '#app') + const heading = await app.find('@child.heading') + + assert.instanceOf(heading, WDE.El) + }) + + it('Should get child by object definition', async function () { + class AppEl extends WDE.El { + get children () { + return { 'heading': { by: 'h1' } } + } + } + + const app = await AppEl.create(this.driver, '#app') + const heading = await app.find('@child.heading') + + assert.instanceOf(heading, WDE.El) + }) + + it('Should return child of specified Class', async function () { + class HeadingEl extends WDE.El {} + class AppEl extends WDE.El { + get children () { + return { 'heading': { by: 'h1', Class: HeadingEl } } + } + } + + const app = await AppEl.create(this.driver, '#app') + const heading = await app.find('@child.heading') + + assert.instanceOf(heading, HeadingEl) + }) + + it('Should return all matching children', async function () { + class AppEl extends WDE.El { + get children () { + return { 'paragraph': 'p' } + } + } + + const app = await AppEl.create(this.driver, '#app') + const paragraphs = await app.findAll('@child.paragraph') + + assert.equal(paragraphs.length, 2) + assert.instanceOf(paragraphs[0], WDE.El) + assert.instanceOf(paragraphs[1], WDE.El) + }) + }) + + /* --------------------------------------------------------------------------- + * primitives interface + * ------------------------------------------------------------------------ */ + + describe('primitives', function () { + before(async function () { + await this.driver.get(appUrl) + }) + + it('Should use the first matching primitive', async function () { + class HeadingEl extends WDE.El { + static matches () { return Promise.resolve(true) } + } + WDE.set('HeadingEl', HeadingEl) + + const app = await WDE.El.create(this.driver, '#app') + const heading = await app.find('h1') + + assert.instanceOf(heading, HeadingEl) + WDE.delete('HeadingEl') + }) + + it('Should load from relative path', async function () { + WDE.load('./fixtures/primitives') + + assert.equal(Object.getPrototypeOf(WDE.HeadingEl), WDE.El) + }) + }) + + /* --------------------------------------------------------------------------- + * find methods + * ------------------------------------------------------------------------ */ + + describe('find', function () { + before(async function () { + await this.driver.get(appUrl) + this.app = await WDE.El.create(this.driver, '#app') + }) + + it('Should find by string selector', async function () { + const heading = await this.app.find('h1') + assert.instanceOf(heading, WDE.El) + }) + + it('Should find by `byHash`', async function () { + const heading = await this.app.find({ css: 'h1' }) + assert.instanceOf(heading, WDE.El) + }) + + it('Should findAll', async function () { + const paragraphs = await this.app.findAll('p') + + assert.equal(paragraphs.length, 2) + assert.instanceOf(paragraphs[0], WDE.El) + assert.instanceOf(paragraphs[1], WDE.El) + }) + + it('Should findLast', async function () { + const paragraphs = await this.app.findAll('p') + const lastParagraph = await this.app.findLast('p') + + assert.equal(await lastParagraph._el.getId(), await paragraphs[1]._el.getId()) + }) + + it('Should findNth', async function () { + const paragraphs = await this.app.findAll('p') + const nthParagraph = await this.app.findNth('p', 1) + + assert.equal(await nthParagraph.getText(), await paragraphs[1].getText()) + }) + }) + + /* --------------------------------------------------------------------------- + * wait + * ------------------------------------------------------------------------ */ + + describe('wait helper', function () { + beforeEach(async function () { + await this.driver.get(appUrl) + this.app = await WDE.El.create(this.driver, '#app') + }) + + it('Should work with no passed element', async function () { + const input = await this.app.find('#enabled') + await input._waitUntilEl('elementIsEnabled') + }) + + it('Should work with passed element', async function () { + const input = await this.app.find('#enabled') + await this.app._waitUntilEl('elementIsEnabled', input) + }) + + it('Should work with passed selector', async function () { + await this.app._waitUntilEl('elementIsEnabled', '#enabled') + }) + + it('Should work with passed query object', async function () { + await this.app._waitUntilEl('elementIsEnabled', { css: '#enabled' }) + }) + }) + + describe('wait methods', function () { + beforeEach(async function () { + await this.driver.get(appUrl) + this.app = await WDE.El.create(this.driver, '#app') + }) + + it('Should waitUntilEnabled', async function () { + const el = await this.app.waitUntilEnabled('#enabled') + assert.instanceOf(el, WDE.El) + }) + + it('Should waitUntilDisabled', async function () { + const el = await this.app.waitUntilDisabled('#disabled') + assert.instanceOf(el, WDE.El) + }) + + it('Should waitUntilSelected', async function () { + const el = await this.app.waitUntilSelected('#selected-option') + assert.instanceOf(el, WDE.El) + }) + + it('Should waitUntilNotSelected', async function () { + const el = await this.app.waitUntilNotSelected('#unselected-option') + assert.instanceOf(el, WDE.El) + }) + + it('Should waitUntilVisible', async function () { + const el = await this.app.waitUntilVisible('#visible') + assert.instanceOf(el, WDE.El) + }) + + it('Should waitUntilNotVisibile', async function () { + const el = await this.app.waitUntilNotVisible('#hidden') + assert.instanceOf(el, WDE.El) + }) + + it('Should waitUntilStale', async function () { + this.driver.executeScript(`var app = document.querySelector('#app'); app.parentNode.removeChild(app);`) + await this.app.waitUntilStale() + }) + + it('Should waitUntilTextContains', async function () { + const el = await this.app.waitUntilTextContains('Tes', 'h1') + assert.instanceOf(el, WDE.El) + }) + + it('Should waitUntilTextIs', async function () { + const el = await this.app.waitUntilTextIs('Test', 'h1') + assert.instanceOf(el, WDE.El) + }) + + it('Should waitUntilTextMatches', async function () { + const el = await this.app.waitUntilTextMatches(/^T/, 'h1') + assert.instanceOf(el, WDE.El) + }) + + it('Should waitUntilLocated', async function () { + const el = await this.app.waitUntilLocated('h1') + assert.instanceOf(el, WDE.El) + }) + }) + + /* --------------------------------------------------------------------------- + * proxy WebElement + * ------------------------------------------------------------------------ */ + + describe('proxy WebElement', function () { + before(async function () { + await this.driver.get(appUrl) + }) + + it('Should contain all instance methods excluding find*', async function () { + const app = await WDE.El.create(this.driver, '#app') + + // assume if getId is working all methods are working + assert.isString(await app.getId()) + }) + }) +}) diff --git a/test/integration/fixtures/app.html b/test/integration/fixtures/app.html new file mode 100644 index 0000000..03156a3 --- /dev/null +++ b/test/integration/fixtures/app.html @@ -0,0 +1,24 @@ + + + + + Test - App + + + +
+

Test

+

Paragraph1

+

Paragraph2

+ + + +
Visible
+ + +
+ + diff --git a/test/integration/fixtures/primitives/heading.js b/test/integration/fixtures/primitives/heading.js new file mode 100644 index 0000000..1ba1e1c --- /dev/null +++ b/test/integration/fixtures/primitives/heading.js @@ -0,0 +1,14 @@ +'use strict' + +/* ----------------------------------------------------------------------------- + * dependencies + * -------------------------------------------------------------------------- */ + +// 3rd party +const El = require('../../../../lib/primitives/el') + +/* ----------------------------------------------------------------------------- + * El + * -------------------------------------------------------------------------- */ + +module.exports = class HeadingEl extends El {} diff --git a/test/integration/page.js b/test/integration/page.js new file mode 100644 index 0000000..4eee73d --- /dev/null +++ b/test/integration/page.js @@ -0,0 +1,150 @@ +/* eslint-env mocha */ +'use strict' + +/* ----------------------------------------------------------------------------- + * dependencies + * -------------------------------------------------------------------------- */ + +// core +const path = require('path') + +// 3rd party +const assert = require('chai').assert +const webdriver = require('selenium-webdriver') + +// lib +const WDElements = require('../../lib/index') + +/* ----------------------------------------------------------------------------- + * reusable + * -------------------------------------------------------------------------- */ + +const WDE = WDElements(webdriver) +const appPath = path.join(__dirname, 'fixtures', 'app.html') +const appUrl = `file://${appPath}` +const otherPath = path.join(__dirname, 'fixtures', 'other.html') +const otherUrl = `file://${otherPath}` + +/* ----------------------------------------------------------------------------- + * test + * -------------------------------------------------------------------------- */ + +describe('Page', function () { + this.timeout(10000) + + beforeEach(async function () { + await this.driver.get(appUrl) + this.page = await WDE.Page.create(this.driver) + }) + + /* --------------------------------------------------------------------------- + * instantiate + * ------------------------------------------------------------------------ */ + + describe('instantiate', function () { + it('Should create instance of El without specifying selector', async function () { + assert.instanceOf(this.page, WDE.El) + assert.instanceOf(this.page._el, webdriver.WebElement) + }) + }) + + /* --------------------------------------------------------------------------- + * getters + * ------------------------------------------------------------------------ */ + + describe('getters', function () { + it('Should getCurrentUrl of page', async function () { + assert.equal(await this.page.getCurrentUrl(), encodeURI(appUrl)) + }) + + it('Should getTitle of page', async function () { + assert.equal(await this.page.getTitle(), 'Test - App') + }) + + it('Should getSource of page', async function () { + assert.include(await this.page.getSource(), '') + }) + }) + + /* --------------------------------------------------------------------------- + * actions + * ------------------------------------------------------------------------ */ + + describe('actions', function () { + it('Should navigateTo page', async function () { + await this.page.navigateTo(otherUrl) + await this.page.waitUntilStale() + }) + + it('Should refresh page', async function () { + await this.page.refresh() + await this.page.waitUntilStale() + }) + + it('Should executeAsyncScript on page', async function () { + const result = await this.page.executeAsyncScript(function () { + var callback = arguments[0] + setTimeout(function () { callback(1) }, 0) + }) + + assert.equal(result, 1) + }) + + it('Should executeScript on page', async function () { + assert.equal(await this.page.executeScript('return 1'), 1) + }) + + it('Should close page', async function () { + await this.page.close() + + try { + await this.page.waitUntilStale() + } catch (e) { + assert.include(e.message, 'no such session') + // TODO: find way to clean up session after closing the last window + // Could be fixed in Admiral? Could be fixed in selenium? + this.page.driver.session_ = null + } + }) + }) + + /* --------------------------------------------------------------------------- + * wait + * ------------------------------------------------------------------------ */ + + describe('wait methods', function () { + it('Should waitUntilAbleToSwitchToFrame', async function () { + await this.page.waitUntilAbleToSwitchToFrame('iframe') + }) + + it('Should waitUntilAlertIsPresent', async function () { + await this.driver.executeScript(`setTimeout(function () { alert('test') }, 1)`) + const alert = await this.page.waitUntilAlertIsPresent() + await alert.accept() + }) + + it('Should waitUntilTitleContains', async function () { + await this.page.waitUntilTitleContains('App') + }) + + it('Should waitUntilTitleIs', async function () { + await this.page.waitUntilTitleIs('Test - App') + }) + + it('Should waitUntilTitleMatches', async function () { + await this.page.waitUntilTitleMatches(/^T/) + }) + + it('Should waitUntilUrlContains', async function () { + await this.page.waitUntilUrlContains('app.html') + }) + + it('Should waitUntilUrlIs', async function () { + await this.page.waitUntilUrlIs(encodeURI(appUrl)) + }) + + it('Should waitUntilUrlMatches', async function () { + await this.page.waitUntilUrlMatches(/^file/) + }) + }) +})