diff --git a/lib/browser.ts b/lib/browser.ts index 81107a693..186d592e7 100644 --- a/lib/browser.ts +++ b/lib/browser.ts @@ -1,5 +1,5 @@ import {BPClient} from 'blocking-proxy'; -import {ActionSequence, By, Capabilities, Command as WdCommand, FileDetector, ICommandName, Options, promise as wdpromise, Session, TargetLocator, TouchSequence, until, WebDriver, WebElement} from 'selenium-webdriver'; +import {ActionSequence, By, Capabilities, Command as WdCommand, FileDetector, ICommandName, Options, promise as wdpromise, Session, TargetLocator, TouchSequence, until, WebDriver, WebElement, WebElementPromise} from 'selenium-webdriver'; import * as url from 'url'; import {extend as extendWD, ExtendedWebDriver} from 'webdriver-js-extender'; @@ -34,87 +34,14 @@ for (let foo in require('selenium-webdriver')) { exports[foo] = require('selenium-webdriver')[foo]; } -// Explicitly define webdriver.WebDriver -// TODO: extend WebDriver from selenium-webdriver typings -export class AbstractWebDriver { - actions: () => ActionSequence; - call: - (fn: (...var_args: any[]) => any, opt_scope?: any, - ...var_args: any[]) => wdpromise.Promise; - close: () => void; - controlFlow: () => wdpromise.ControlFlow; - executeScript: (script: string|Function, ...var_args: any[]) => wdpromise.Promise; - executeAsyncScript: (script: string|Function, ...var_args: any[]) => wdpromise.Promise; - getCapabilities: () => wdpromise.Promise; - getCurrentUrl: () => wdpromise.Promise; - getPageSource: () => wdpromise.Promise; - getSession: () => wdpromise.Promise; - getTitle: () => wdpromise.Promise; - getWindowHandle: () => wdpromise.Promise; - getAllWindowHandles: () => wdpromise.Promise; - manage: () => Options; - quit: () => void; - schedule: (command: WdCommand, description: string) => wdpromise.Promise; - setFileDetector: (detector: FileDetector) => void; - sleep: (ms: number) => wdpromise.Promise; - switchTo: () => TargetLocator; - takeScreenshot: () => wdpromise.Promise; - touchActions: () => TouchSequence; - wait: - (condition: wdpromise.Promise|until.Condition|Function, opt_timeout?: number, - opt_message?: string) => wdpromise.Promise; -} -export class AbstractExtendedWebDriver extends AbstractWebDriver { - getNetworkConnection: () => wdpromise.Promise<0|1|2|3|4|5|6|7>; - setNetworkConnection: - (typeOrAirplaneMode: 0|1|2|3|4|5|6|7|boolean, wifi?: boolean, - data?: boolean) => wdpromise.Promise; - toggleAirplaneMode: () => wdpromise.Promise; - toggleWiFi: () => wdpromise.Promise; - toggleData: () => wdpromise.Promise; - toggleLocationServices: () => wdpromise.Promise; - getGeolocation: () => wdpromise.Promise<{latitude: number, longitude: number, altitude: number}>; - setGeolocation: - (latitude?: number, longitude?: number, altitude?: number) => wdpromise.Promise; - getCurrentDeviceActivity: () => wdpromise.Promise; - startDeviceActivity: - (appPackage: string, appActivity: string, appWaitPackage?: string, - appWaitActivity?: string) => wdpromise.Promise; - getAppiumSettings: () => wdpromise.Promise<{[name: string]: any}>; - setAppiumSettings: (settings: {[name: string]: any}) => wdpromise.Promise; - getCurrentContext: () => wdpromise.Promise; - selectContext: (name: string) => wdpromise.Promise; - listContexts: () => wdpromise.Promise; - getScreenOrientation: () => wdpromise.Promise<'LANDSCAPE'|'PORTRAIT'>; - setScreenOrientation: (orientation: string) => wdpromise.Promise; - isDeviceLocked: () => wdpromise.Promise; - lockDevice: (delay?: number) => wdpromise.Promise; - unlockDevice: () => wdpromise.Promise; - installApp: (appPath: string) => wdpromise.Promise; - isAppInstalled: (bundleId: string) => wdpromise.Promise; - removeApp: (appId: string) => wdpromise.Promise; - pullFileFromDevice: (path: string) => wdpromise.Promise; - pullFolderFromDevice: (path: string) => wdpromise.Promise; - pushFileToDevice: (path: string, base64Data: string) => wdpromise.Promise; - uploadFile: (base64Data: string) => wdpromise.Promise; - switchToParentFrame: () => wdpromise.Promise; - fullscreen: () => wdpromise.Promise; - sendAppToBackground: (delay?: number) => wdpromise.Promise; - closeApp: () => wdpromise.Promise; - getAppStrings: (language?: string) => wdpromise.Promise; - launchSession: () => wdpromise.Promise; - resetApp: () => wdpromise.Promise; - hideSoftKeyboard: - (strategy?: 'default'|'tapOutside'|'tapOut'|'swipeDown'|'pressKey'|'press', - key?: string) => wdpromise.Promise; - getDeviceTime: () => wdpromise.Promise; - openDeviceNotifications: () => wdpromise.Promise; - rotationGesture: - (x?: number, y?: number, duration?: number, rotation?: number, - touchCount?: 1|2|3|4|5) => wdpromise.Promise; - shakeDevice: () => wdpromise.Promise; -} +// Explicitly define types for webdriver.WebDriver and ExtendedWebDriver. +// We do this because we use composition over inheritance to implement polymorphism, and therefore +// we don't want to inherit WebDriver's constructor. +export class AbstractWebDriver {} +export interface AbstractWebDriver extends WebDriver {} +export class AbstractExtendedWebDriver extends AbstractWebDriver {} +export interface AbstractExtendedWebDriver extends ExtendedWebDriver {} /** * Mix a function from one object onto another. The function will still be @@ -808,10 +735,10 @@ export class ProtractorBrowser extends AbstractExtendedWebDriver { /** * Waits for Angular to finish rendering before searching for elements. * @see webdriver.WebDriver.findElement - * @returns {!webdriver.promise.Promise} A promise that will be resolved to + * @returns {!webdriver.WebElementPromise} A promise that will be resolved to * the located {@link webdriver.WebElement}. */ - findElement(locator: Locator): WebElement { + findElement(locator: Locator): WebElementPromise { return this.element(locator).getWebElement(); } @@ -821,7 +748,7 @@ export class ProtractorBrowser extends AbstractExtendedWebDriver { * @returns {!webdriver.promise.Promise} A promise that will be resolved to an * array of the located {@link webdriver.WebElement}s. */ - findElements(locator: Locator): wdpromise.Promise { + findElements(locator: Locator): wdpromise.Promise { return this.element.all(locator).getWebElements(); } diff --git a/lib/element.ts b/lib/element.ts index 0d710701b..7b9319b50 100644 --- a/lib/element.ts +++ b/lib/element.ts @@ -2,7 +2,7 @@ import {By, error as wderror, ILocation, ISize, promise as wdpromise, WebDriver, import {ElementHelper, ProtractorBrowser} from './browser'; import {IError} from './exitCodes'; -import {Locator} from './locators'; +import {isProtractorLocator, Locator} from './locators'; import {Logger} from './logger'; let clientSideScripts = require('./clientsidescripts'); @@ -159,7 +159,7 @@ export class ElementArrayFinder extends WebdriverWebElement { // This is the first time we are looking for an element return ptor.waitForAngular('Locator: ' + locator) .then((): wdpromise.Promise => { - if (locator.findElementsOverride) { + if (isProtractorLocator(locator)) { return locator.findElementsOverride(ptor.driver, null, ptor.rootEl); } else { return ptor.driver.findElements(locator); @@ -170,7 +170,7 @@ export class ElementArrayFinder extends WebdriverWebElement { // For each parent web element, find their children and construct // a list of Promise> let childrenPromiseList = parentWebElements.map((parentWebElement: WebElement) => { - return locator.findElementsOverride ? + return isProtractorLocator(locator) ? locator.findElementsOverride(ptor.driver, parentWebElement, ptor.rootEl) : parentWebElement.findElements(locator); }); @@ -905,7 +905,7 @@ export class ElementFinder extends WebdriverWebElement { * browser.driver.findElement(by.css('.parent')); * browser.findElement(by.css('.parent')); * - * @returns {webdriver.WebElement} + * @returns {webdriver.WebElementPromise} */ getWebElement(): WebElementPromise { let id = this.elementArrayFinder_.getWebElements().then((parentWebElements: WebElement[]) => { diff --git a/lib/locators.ts b/lib/locators.ts index 5dbe7d7f3..b29132d82 100644 --- a/lib/locators.ts +++ b/lib/locators.ts @@ -1,9 +1,10 @@ -import {By, promise as wdpromise, WebDriver, WebElement} from 'selenium-webdriver'; +import {By, ByHash, promise as wdpromise, WebDriver, WebElement} from 'selenium-webdriver'; let clientSideScripts = require('./clientsidescripts'); - // Explicitly define webdriver.By. +// We do this because we want to inherit the static methods of webdriver.By, as opposed to +// inheriting from the webdriver.By class itself, which is actually analogous to ProtractorLocator. export class WebdriverBy { className: (className: string) => By = By.className; css: (css: string) => By = By.css; @@ -15,15 +16,21 @@ export class WebdriverBy { tagName: (tagName: string) => By = By.tagName; xpath: (xpath: string) => By = By.xpath; } +export type WebDriverLocator = By | ByHash | Function; // Protractor locator strategy -export interface Locator { - findElementsOverride?: +export interface ProtractorLocator { + findElementsOverride: (driver: WebDriver, using: WebElement, rootSelector: string) => wdpromise.Promise; row?: (index: number) => Locator; column?: (index: string) => Locator; } +export type Locator = ProtractorLocator | WebDriverLocator; + +export function isProtractorLocator(x: Locator): x is ProtractorLocator { + return x && (typeof(x as any).findElementsOverride === 'function'); +} /** * The Protractor Locators. These provide ways of finding elements in @@ -70,7 +77,7 @@ export class ProtractorBy extends WebdriverBy { * element. It should return an array of elements. */ addLocator(name: string, script: Function|string) { - this[name] = (...args: any[]): Locator => { + this[name] = (...args: any[]): ProtractorLocator => { let locatorArguments = args; return { findElementsOverride: (driver: WebDriver, using: WebElement, rootSelector: string): @@ -119,9 +126,9 @@ export class ProtractorBy extends WebdriverBy { * var deprecatedSyntax = element(by.binding('{{person.name}}')); * * @param {string} bindingDescriptor - * @returns {Locator} location strategy + * @returns {ProtractorLocator} location strategy */ - binding(bindingDescriptor: string): Locator { + binding(bindingDescriptor: string): ProtractorLocator { return { findElementsOverride: (driver: WebDriver, using: WebElement, rootSelector: string): wdpromise.Promise => { @@ -151,9 +158,9 @@ export class ProtractorBy extends WebdriverBy { * expect(element(by.exactBinding('phone')).isPresent()).toBe(false); * * @param {string} bindingDescriptor - * @returns {Locator} location strategy + * @returns {ProtractorLocator} location strategy */ - exactBinding(bindingDescriptor: string): Locator { + exactBinding(bindingDescriptor: string): ProtractorLocator { return { findElementsOverride: (driver: WebDriver, using: WebElement, rootSelector: string): wdpromise.Promise => { @@ -179,9 +186,9 @@ export class ProtractorBy extends WebdriverBy { * expect(input.getAttribute('value')).toBe('Foo123'); * * @param {string} model ng-model expression. - * @returns {Locator} location strategy + * @returns {ProtractorLocator} location strategy */ - model(model: string): Locator { + model(model: string): ProtractorLocator { return { findElementsOverride: (driver: WebDriver, using: WebElement, rootSelector: string): wdpromise.Promise => { @@ -204,9 +211,9 @@ export class ProtractorBy extends WebdriverBy { * element(by.buttonText('Save')); * * @param {string} searchText - * @returns {Locator} location strategy + * @returns {ProtractorLocator} location strategy */ - buttonText(searchText: string): Locator { + buttonText(searchText: string): ProtractorLocator { return { findElementsOverride: (driver: WebDriver, using: WebElement, rootSelector: string): wdpromise.Promise => { @@ -229,9 +236,9 @@ export class ProtractorBy extends WebdriverBy { * element(by.partialButtonText('Save')); * * @param {string} searchText - * @returns {Locator} location strategy + * @returns {ProtractorLocator} location strategy */ - partialButtonText(searchText: string): Locator { + partialButtonText(searchText: string): ProtractorLocator { return { findElementsOverride: (driver: WebDriver, using: WebElement, rootSelector: string): wdpromise.Promise => { @@ -245,7 +252,7 @@ export class ProtractorBy extends WebdriverBy { }; // Generate either by.repeater or by.exactRepeater - private byRepeaterInner(exact: boolean, repeatDescriptor: string): Locator { + private byRepeaterInner(exact: boolean, repeatDescriptor: string): ProtractorLocator { let name = 'by.' + (exact ? 'exactR' : 'r') + 'epeater'; return { findElementsOverride: (driver: WebDriver, using: WebElement, rootSelector: string): @@ -256,7 +263,7 @@ export class ProtractorBy extends WebdriverBy { toString: (): string => { return name + '("' + repeatDescriptor + '")'; }, - row: (index: number): Locator => { + row: (index: number): ProtractorLocator => { return { findElementsOverride: (driver: WebDriver, using: WebElement, rootSelector: string): wdpromise.Promise => { @@ -267,7 +274,7 @@ export class ProtractorBy extends WebdriverBy { toString: (): string => { return name + '(' + repeatDescriptor + '").row("' + index + '")"'; }, - column: (binding: string): Locator => { + column: (binding: string): ProtractorLocator => { return { findElementsOverride: (driver: WebDriver, using: WebElement, rootSelector: string): wdpromise.Promise => { @@ -283,7 +290,7 @@ export class ProtractorBy extends WebdriverBy { } }; }, - column: (binding: string): Locator => { + column: (binding: string): ProtractorLocator => { return { findElementsOverride: (driver: WebDriver, using: WebElement, rootSelector: string): wdpromise.Promise => { @@ -294,7 +301,7 @@ export class ProtractorBy extends WebdriverBy { toString: (): string => { return name + '("' + repeatDescriptor + '").column("' + binding + '")'; }, - row: (index: number): Locator => { + row: (index: number): ProtractorLocator => { return { findElementsOverride: (driver: WebDriver, using: WebElement, rootSelector: string): wdpromise.Promise => { @@ -365,9 +372,9 @@ export class ProtractorBy extends WebdriverBy { * var divs = element.all(by.repeater('book in library')); * * @param {string} repeatDescriptor - * @returns {Locator} location strategy + * @returns {ProtractorLocator} location strategy */ - repeater(repeatDescriptor: string): Locator { + repeater(repeatDescriptor: string): ProtractorLocator { return this.byRepeaterInner(false, repeatDescriptor); } @@ -387,9 +394,9 @@ export class ProtractorBy extends WebdriverBy { * expect(element(by.exactRepeater('car in cars')).isPresent()).toBe(true); * * @param {string} repeatDescriptor - * @returns {Locator} location strategy + * @returns {ProtractorLocator} location strategy */ - exactRepeater(repeatDescriptor: string): Locator { + exactRepeater(repeatDescriptor: string): ProtractorLocator { return this.byRepeaterInner(true, repeatDescriptor); } @@ -408,9 +415,9 @@ export class ProtractorBy extends WebdriverBy { * * @param {string} cssSelector css selector * @param {string} searchString text search - * @returns {Locator} location strategy + * @returns {ProtractorLocator} location strategy */ - cssContainingText(cssSelector: string, searchText: string): Locator { + cssContainingText(cssSelector: string, searchText: string): ProtractorLocator { return { findElementsOverride: (driver: WebDriver, using: WebElement, rootSelector: string): wdpromise.Promise => { @@ -441,9 +448,9 @@ export class ProtractorBy extends WebdriverBy { * expect(firstOption.getText()).toEqual('red'); * * @param {string} optionsDescriptor ng-options expression. - * @returns {Locator} location strategy + * @returns {ProtractorLocator} location strategy */ - options(optionsDescriptor: string): Locator { + options(optionsDescriptor: string): ProtractorLocator { return { findElementsOverride: (driver: WebDriver, using: WebElement, rootSelector: string): wdpromise.Promise => {