diff --git a/README.md b/README.md index 65e890b..3b631e0 100644 --- a/README.md +++ b/README.md @@ -24,14 +24,13 @@ The goal of **wd-elements** is to provide a sane and consistent environment to a ```js const webdriver = require('selenium-webdriver') -const WDE = require('wd-elements')(webdriver) +const WDE = require('wd-elements') -const driver = new webdriver.Builder().forBrowser('chrome').build(); -const page = WDE.Page.create(driver) +const driver = WDE(new webdriver.Builder().forBrowser('chrome').build()) // for the most part WDElements behave like native selenium WebElements // the true power comes from primitives and extensibility (see advanced usage) -page.find('#app') +driver.find('#app') .then((app) => app.find('h1')) .then((h1) => h1.getText()) ``` diff --git a/lib/driver.js b/lib/driver.js new file mode 100644 index 0000000..e798fb6 --- /dev/null +++ b/lib/driver.js @@ -0,0 +1,82 @@ +'use strict' + +/* ----------------------------------------------------------------------------- + * dependencies + * -------------------------------------------------------------------------- */ + +// 3rd party +const wd = require('selenium-webdriver') + +// lib +const Finder = require('./finder') + +/* ----------------------------------------------------------------------------- + * private + * -------------------------------------------------------------------------- */ + +const elementWaitMethods = new Set([ + 'elementIsEnabled', + 'elementIsDisabled', + 'elementIsSelected', + 'elementIsNotSelected', + 'elementIsVisible', + 'elementIsNotVisible', + 'stalenessOf', + 'elementTextContains', + 'elementTextIs', + 'elementTextMatches', + 'elementLocated' +]) + +/* ----------------------------------------------------------------------------- + * El + * -------------------------------------------------------------------------- */ + +module.exports = class Driver extends Finder { + + get elementWaitMethods () { + return elementWaitMethods + } + + constructor (driver) { + super(driver) + this._driver = driver + + return this.proxyTo('_driver') + } + + /* --------------------------------------------------------------------------- + * actions + * ------------------------------------------------------------------------ */ + + refresh () { + return this._driver.navigate().refresh() + } + + /* --------------------------------------------------------------------------- + * wait + * ------------------------------------------------------------------------ */ + + wait () { + return this._driver.wait.apply(this._driver, arguments).then((result) => { + return result instanceof wd.WebElement + ? this._createFromEl(result) + : result + }) + } + + waitUntil (name, ...args) { + // if a wd el is passed -> parse it into a raw WebElement + if (args[0] && args[0]._el) { + args[0] = args[0]._el + } + + if (!this.elementWaitMethods.has(name) || args[0] instanceof wd.WebElement) { + return this.wait(wd.until[name](...args)) + } + + return this.find(args[0]) + .then((el) => this.waitUntil(name, ...[el].concat(args.slice(1)))) + } + +} diff --git a/lib/finder.js b/lib/finder.js new file mode 100644 index 0000000..2389812 --- /dev/null +++ b/lib/finder.js @@ -0,0 +1,139 @@ +'use strict' + +/* ----------------------------------------------------------------------------- + * dependencies + * -------------------------------------------------------------------------- */ + +// 3rd party +const _ = require('lodash') +const Promise = require('bluebird') + +/* ----------------------------------------------------------------------------- + * El + * -------------------------------------------------------------------------- */ + +module.exports = class Finder { + + static lookupUsing (primitives) { + this.primitives = primitives + } + + static createUsing (driver) { + this.driver = driver + } + + get children () { + return {} + } + + constructor (root) { + this.root = root + this.primitives = this.constructor.primitives + this.driver = this.constructor.driver + this.definitions = _.mapValues(this.children, this._parseChild) + + // alias + this.find = this.findElement + this.findAll = this.findElements + } + + proxyTo (key) { + return new Proxy(this, { + get: (target, name) => name in target ? target[name] : target[key][name] + }) + } + + /* --------------------------------------------------------------------------- + * find methods + * ------------------------------------------------------------------------ */ + + findElement (_by, _options) { + const { by, options } = this._normalizeFindArgs(_by, _options) + + return this.root.findElement(this._normalizeBy(by)) + .then((el) => this._createFromEl(el, options)) + } + + findElements (_by, _options) { + const { by, options } = this._normalizeFindArgs(_by, _options) + + return this.root.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]) + } + + /* --------------------------------------------------------------------------- + * utils + * ------------------------------------------------------------------------ */ + + _normalizeFindArgs (by, options) { + if (_.isString(by) && by.startsWith('@child.') && this.definitions) { + 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`) + } + } + + _createFromEl (el, options = {}) { + if (options.Class) { + return Promise.resolve(new options.Class(el, options)) + } + + return this.primitives.findByEl(el) + .then((Class) => new Class(el, options)) + } + + _parseChild (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 + } + +} diff --git a/lib/index.js b/lib/index.js index dc2e258..9f5480e 100644 --- a/lib/index.js +++ b/lib/index.js @@ -6,10 +6,12 @@ // 3rd party const _ = require('lodash') +const wd = require('selenium-webdriver') // lib const Primitives = require('./primitives') -const El = require('./primitives/el') +const Finder = require('./finder') +const Driver = require('./driver') /* ----------------------------------------------------------------------------- * WDElements @@ -17,25 +19,33 @@ const El = require('./primitives/el') // add all of our base internal primitives const primitives = new Primitives() +primitives.load('./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] +Finder.lookupUsing(primitives) + +// Return proxy of WDElemts that implements a dynamic getter to return +// an interface to interact with primitives +module.exports = new Proxy(function (_driver) { + const isWrapper = _driver instanceof Driver + const isSupported = _driver instanceof wd.WebDriver || isWrapper + + if (!isSupported) { + throw new Error(`The specified driver is using an unsupported "selenium-webdriver" version.`) + } + + // protect against wrapping driver multiple times + const driver = isWrapper ? _driver : new Driver(_driver) + + Finder.createUsing(driver) + return driver +}, { + 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/el.js b/lib/primitives/el.js index ab2c83c..9278564 100644 --- a/lib/primitives/el.js +++ b/lib/primitives/el.js @@ -7,64 +7,59 @@ // 3rd party const _ = require('lodash') const Promise = require('bluebird') +const wd = require('selenium-webdriver') + +// lib +const Finder = require('../finder') /* ----------------------------------------------------------------------------- - * El + * private * -------------------------------------------------------------------------- */ -module.exports = class El { +const elementWaitAliases = { + 'isEnabled': 'elementIsEnabled', + 'isDisabled': 'elementIsDisabled', + 'isSelected': 'elementIsSelected', + 'isNotSelected': 'elementIsNotSelected', + 'isVisible': 'elementIsVisible', + 'isNotVisible': 'elementIsNotVisible', + 'isStale': 'stalenessOf', + 'textContains': 'elementTextContains', + 'textIs': 'elementTextIs', + 'textMatches': 'elementTextMatches' +} - static configure (wd, primitives) { - this.wd = wd - this.primitives = primitives +/* ----------------------------------------------------------------------------- + * El + * -------------------------------------------------------------------------- */ - this.configured = true - } +module.exports = class El extends Finder { - static create (driver, selector) { - return driver.findElement({ css: selector }) - .then((el) => new this(el)) + static create (selector) { + return this.driver.find(selector, { Class: this }) } static matches (el) { - return Promise.resolve(el instanceof this.wd.WebElement) - } - - get children () { - return {} + return Promise.resolve(el instanceof wd.WebElement) } 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 + get elementWaitAliases () { + return elementWaitAliases } 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. + super(el) 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.proxyTo('_el') + } - return this._getWebElementProxy() + getDriver () { + return this.driver } /* --------------------------------------------------------------------------- @@ -83,181 +78,28 @@ module.exports = class El { }, {})) } - /* --------------------------------------------------------------------------- - * 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) - } + waitUntil (name, ...args) { + const methodName = this.elementWaitAliases[name] || name - 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 + // if a wd el is passed -> parse it into a raw WebElement + if (args[0] && args[0]._el) { + args[0] = args[0]._el } - } - - _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`) + if (!this.driver.elementWaitMethods.has(methodName)) { + throw new Error(`No waitUntil command exists by the name: ${name}`) + } else if (this.elementWaitAliases[name]) { + return this.driver.waitUntil(methodName, ...[this].concat(args)) + } else if (args[0] instanceof wd.WebElement) { + return this.driver.waitUntil(methodName, ...args) } - } - - _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) + return this.find(args[0]) + .then((el) => this.driver.waitUntil(methodName, ...[el].concat(args.slice(1)))) } } diff --git a/lib/primitives/page.js b/lib/primitives/page.js deleted file mode 100644 index c3d8916..0000000 --- a/lib/primitives/page.js +++ /dev/null @@ -1,109 +0,0 @@ -'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 index f8ab513..d1c2939 100644 --- a/package.json +++ b/package.json @@ -27,8 +27,8 @@ }, "devDependencies": { "admiral": "0.0.4", - "admiral-integration-mocha": "0.0.2", - "admiral-target-local": "0.0.2", + "admiral-integration-mocha": "0.1.0", + "admiral-target-local": "0.1.0", "chai": "^3.5.0", "coveralls": "^2.11.15", "flvr": "^0.1.3", diff --git a/test/integration/driver.js b/test/integration/driver.js new file mode 100644 index 0000000..1fab4e4 --- /dev/null +++ b/test/integration/driver.js @@ -0,0 +1,84 @@ +/* eslint-env mocha */ +'use strict' + +/* ----------------------------------------------------------------------------- + * dependencies + * -------------------------------------------------------------------------- */ + +// core +const path = require('path') + +// 3rd party +const assert = require('chai').assert + +// lib +const WDE = require('../../lib/index') + +/* ----------------------------------------------------------------------------- + * reusable + * -------------------------------------------------------------------------- */ + +const appPath = path.join(__dirname, 'fixtures', 'app.html') +const appUrl = `file://${appPath}` + +/* ----------------------------------------------------------------------------- + * test + * -------------------------------------------------------------------------- */ + +describe('Driver', function () { + this.timeout(10000) + + before(function () { + this.returnDriver = (browser) => WDE(browser.driver) + }) + + beforeEach(async function () { + await this.driver.get(appUrl) + }) + + /* --------------------------------------------------------------------------- + * actions + * ------------------------------------------------------------------------ */ + + describe('actions', function () { + it('Should refresh page', async function () { + await this.driver.executeScript('document.title = "changed"') + await this.driver.refresh() + + assert.equal(await this.driver.getTitle(), 'Test - App') + }) + }) + + /* --------------------------------------------------------------------------- + * wait + * ------------------------------------------------------------------------ */ + + describe('wait', function () { + it('Should waitUntilTitleContains', async function () { + await this.driver.waitUntil('titleContains', 'App') + }) + + it('Should resolve when element passed.', async function () { + const input = await this.driver.find('#enabled') + await this.driver.waitUntil('elementIsEnabled', input) + }) + + it('Should resolve when query object passed.', async function () { + await this.driver.waitUntil('elementIsEnabled', { css: '#enabled' }) + }) + + it('Should resolve when selector passed.', async function () { + await this.driver.waitUntil('elementIsEnabled', '#enabled') + }) + }) + + /* --------------------------------------------------------------------------- + * proxy _driver + * ------------------------------------------------------------------------ */ + + describe('proxy _driver', function () { + it('Should getTitle of page', async function () { + assert.equal(await this.driver.getTitle(), 'Test - App') + }) + }) +}) diff --git a/test/integration/el.js b/test/integration/el.js index 3c90e22..a6c4bea 100644 --- a/test/integration/el.js +++ b/test/integration/el.js @@ -10,16 +10,14 @@ const path = require('path') // 3rd party const assert = require('chai').assert -const webdriver = require('selenium-webdriver') // lib -const WDElements = require('../../lib/index') +const WDE = require('../../lib/index') /* ----------------------------------------------------------------------------- * reusable * -------------------------------------------------------------------------- */ -const WDE = WDElements(webdriver) const appPath = path.join(__dirname, 'fixtures', 'app.html') const appUrl = `file://${appPath}` @@ -30,31 +28,12 @@ const appUrl = `file://${appPath}` 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) + before(function () { + this.returnDriver = (browser) => WDE(browser.driver) + }) - class CustomEl extends TempWDE.El {} - assert.equal(CustomEl.wd, tempWD) - WDElements(webdriver) - }) + beforeEach(async function () { + await this.driver.get(appUrl) }) /* --------------------------------------------------------------------------- @@ -62,13 +41,11 @@ describe('El', function () { * ------------------------------------------------------------------------ */ 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') + const app = await WDE.El.create('#app') + assert.instanceOf(app, WDE.El) + assert.equal(app.getDriver(), this.driver) }) }) @@ -77,8 +54,9 @@ describe('El', function () { * ------------------------------------------------------------------------ */ describe('data', function () { - beforeEach(async function () { - await this.driver.get(appUrl) + it('Should return text by default', async function () { + const paragraph = await WDE.El.create('p') + assert.deepEqual(await paragraph.data(), { 'text': 'Paragraph1' }) }) it('Should be able to specify getter args', async function () { @@ -86,7 +64,7 @@ describe('El', function () { get properties () { return ['attribute:data-test'] } } - const paragraph = await CustomParagraph.create(this.driver, 'p') + const paragraph = await CustomParagraph.create('p') assert.deepEqual(await paragraph.data(), { 'attribute:data-test': 'val1' }) }) @@ -95,7 +73,7 @@ describe('El', function () { get properties () { return ['text', 'attribute:data-test'] } } - const paragraph = await CustomParagraph.create(this.driver, 'p') + const paragraph = await CustomParagraph.create('p') assert.deepEqual(await paragraph.data(), { 'text': 'Paragraph1', 'attribute:data-test': 'val1' @@ -107,257 +85,50 @@ describe('El', function () { get properties () { return ['text', 'attribute:data-test'] } } - const paragraph = await CustomParagraph.create(this.driver, 'p') + const paragraph = await CustomParagraph.create('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 () { + describe('wait methods', function () { beforeEach(async function () { - await this.driver.get(appUrl) - this.app = await WDE.El.create(this.driver, '#app') + this.app = await WDE.El.create('#app') }) - it('Should work with no passed element', async function () { + it('Should implement shortcut methods to automatically pass el', async function () { const input = await this.app.find('#enabled') - await input._waitUntilEl('elementIsEnabled') + await input.waitUntil('isEnabled') }) - it('Should work with passed element', async function () { + it('Should proxy normal element methods to driver', async function () { const input = await this.app.find('#enabled') - await this.app._waitUntilEl('elementIsEnabled', input) + await this.app.waitUntil('elementIsEnabled', input) }) - it('Should work with passed selector', async function () { - await this.app._waitUntilEl('elementIsEnabled', '#enabled') + it('Should locally resolve if selector passed for element method', async function () { + await this.app.waitUntil('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) + it('Should throw if method is not supported', function (done) { + Promise.resolve() + .then(() => this.app.waitUntil('isAwesome')) + .catch(() => done()) }) }) /* --------------------------------------------------------------------------- - * proxy WebElement + * proxy _el * ------------------------------------------------------------------------ */ - describe('proxy WebElement', function () { - before(async function () { - await this.driver.get(appUrl) - }) - + describe('proxy _el', function () { it('Should contain all instance methods excluding find*', async function () { - const app = await WDE.El.create(this.driver, '#app') + const app = await WDE.El.create('#app') // assume if getId is working all methods are working assert.isString(await app.getId()) diff --git a/test/integration/finder.js b/test/integration/finder.js new file mode 100644 index 0000000..e1c4240 --- /dev/null +++ b/test/integration/finder.js @@ -0,0 +1,163 @@ +/* eslint-env mocha */ +'use strict' + +/* ----------------------------------------------------------------------------- + * dependencies + * -------------------------------------------------------------------------- */ + +// core +const path = require('path') + +// 3rd party +const assert = require('chai').assert + +// lib +const WDE = require('../../lib/index') +const Finder = require('../../lib/finder') + +/* ----------------------------------------------------------------------------- + * reusable + * -------------------------------------------------------------------------- */ + +const appPath = path.join(__dirname, 'fixtures', 'app.html') +const appUrl = `file://${appPath}` + +/* ----------------------------------------------------------------------------- + * test + * -------------------------------------------------------------------------- */ + +describe('Finder', function () { + this.timeout(10000) + + before(async function () { + await this.driver.get(appUrl) + }) + + /* --------------------------------------------------------------------------- + * child interface + * ------------------------------------------------------------------------ */ + + describe('child interface', function () { + it('Should get child by string definition', async function () { + class MyFinder extends Finder { + get children () { + return { 'heading': 'h1' } + } + } + + const myFinder = new MyFinder(this.driver) + const heading = await myFinder.find('@child.heading') + + assert.instanceOf(heading, WDE.El) + }) + + it('Should get child by object definition', async function () { + class MyFinder extends Finder { + get children () { + return { 'heading': { by: 'h1' } } + } + } + + const myFinder = new MyFinder(this.driver) + const heading = await myFinder.find('@child.heading') + + assert.instanceOf(heading, WDE.El) + }) + + it('Should return child of specified Class', async function () { + class HeadingEl extends WDE.El {} + class MyFinder extends Finder { + get children () { + return { 'heading': { by: 'h1', Class: HeadingEl } } + } + } + + const myFinder = new MyFinder(this.driver) + const heading = await myFinder.find('@child.heading') + + assert.instanceOf(heading, HeadingEl) + }) + + it('Should return all matching children', async function () { + class MyFinder extends Finder { + get children () { + return { 'paragraph': 'p' } + } + } + + const myFinder = new MyFinder(this.driver) + const paragraphs = await myFinder.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 () { + 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 finder = await new Finder(this.driver) + const heading = await finder.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 () { + this.finder = new Finder(this.driver) + }) + + it('Should find by string selector', async function () { + const heading = await this.finder.find('h1') + assert.instanceOf(heading, WDE.El) + }) + + it('Should find by `byHash`', async function () { + const heading = await this.finder.find({ css: 'h1' }) + assert.instanceOf(heading, WDE.El) + }) + + it('Should findAll', async function () { + const paragraphs = await this.finder.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.finder.findAll('p') + const lastParagraph = await this.finder.findLast('p') + + assert.equal(await lastParagraph._el.getId(), await paragraphs[1]._el.getId()) + }) + + it('Should findNth', async function () { + const paragraphs = await this.finder.findAll('p') + const nthParagraph = await this.finder.findNth('p', 1) + + assert.equal(await nthParagraph.getText(), await paragraphs[1].getText()) + }) + }) +}) diff --git a/test/integration/index.js b/test/integration/index.js new file mode 100644 index 0000000..99207aa --- /dev/null +++ b/test/integration/index.js @@ -0,0 +1,40 @@ +/* eslint-env mocha */ +'use strict' + +/* ----------------------------------------------------------------------------- + * dependencies + * -------------------------------------------------------------------------- */ + +// 3rd party +const assert = require('chai').assert + +// lib +const WDE = require('../../lib/index') +const El = require('../../lib/primitives/el') + +/* ----------------------------------------------------------------------------- + * test + * -------------------------------------------------------------------------- */ + +describe('WDE', function () { + this.timeout(10000) + + it('Should throw if driver is not inherited from installed Driver', function () { + assert.throws(function () { WDE({}) }) + }) + + it('Should not wrap the driver more than once', function () { + const driver1 = WDE(this.driver) + const driver2 = WDE(driver1) + + assert.equal(driver1, driver2) + }) + + it('Should proxy primitive Classes', function () { + assert.equal(WDE.El, El) + }) + + it('Should proxy primitive methods', function () { + assert.isFunction(WDE.load) + }) +}) diff --git a/test/integration/page.js b/test/integration/page.js deleted file mode 100644 index 4eee73d..0000000 --- a/test/integration/page.js +++ /dev/null @@ -1,150 +0,0 @@ -/* 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/) - }) - }) -})