From bef83fbc9a0da47d20c2bf03c970275aadef4078 Mon Sep 17 00:00:00 2001 From: Craig Nishina Date: Mon, 19 Sep 2016 00:05:48 -0700 Subject: [PATCH] feat(elementexplorer): highlight elements - Highlight elements by changing css - Use the rootEl default 'body'. On second attempt, use a default rootEl ''. After the attempt is made, reset the rootEl. closes #3465 --- lib/browser.ts | 60 ++++++++++-------- lib/debugger/clients/explorerScripts.ts | 84 +++++++++++++++++++++++++ lib/debugger/clients/explorerXpath.js | 58 +++++++++++++++++ 3 files changed, 176 insertions(+), 26 deletions(-) create mode 100644 lib/debugger/clients/explorerScripts.ts create mode 100644 lib/debugger/clients/explorerXpath.js diff --git a/lib/browser.ts b/lib/browser.ts index 64f4eb9e4..b9b95134c 100644 --- a/lib/browser.ts +++ b/lib/browser.ts @@ -1,10 +1,10 @@ // Util from NodeJs import * as net from 'net'; import {ActionSequence, Capabilities, Command as WdCommand, FileDetector, Options, promise as wdpromise, Session, TargetLocator, TouchSequence, until, WebDriver, WebElement} from 'selenium-webdriver'; - import * as url from 'url'; import * as util from 'util'; +import {ExplorerScripts} from './debugger/clients/explorerScripts'; import {build$, build$$, ElementArrayFinder, ElementFinder} from './element'; import {ProtractorExpectedConditions} from './expectedConditions'; import {Locator, ProtractorBy} from './locators'; @@ -373,12 +373,17 @@ export class ProtractorBrowser extends Webdriver { * available on the page when finding elements or waiting for stability. * Only compatible with Angular2. */ - useAllAngular2AppRoots() { + useAllAngular2AppRoots(): void { // The empty string is an invalid css selector, so we use it to easily // signal to scripts to not find a root element. this.rootEl = ''; } + /** + * Set the rootEl + */ + setRootEl(rootEl: string): void { this.rootEl = rootEl; } + /** * The same as {@code webdriver.WebDriver.prototype.executeScript}, * but with a customized description for debugging. @@ -991,17 +996,9 @@ export class ProtractorBrowser extends Webdriver { [key: string]: any; } let context: Context = {require: require}; - global.list = (locator: Locator) => { - /* globals browser */ - return global.browser.findElements(locator).then( - (arr: webdriver.WebElement[]) => { - let found: string[] = []; - for (let i = 0; i < arr.length; ++i) { - arr[i].getText().then((text: string) => { found.push(text); }); - } - return found; - }); - }; + (global).list = ExplorerScripts.list; + (global).highlight = ExplorerScripts.highlight; + for (let key in global) { context[key] = global[key]; } @@ -1071,9 +1068,20 @@ export class ProtractorBrowser extends Webdriver { execute_: function(execFn_: Function) { this.execPromiseResult_ = this.execPromiseError_ = undefined; - this.execPromise_ = this.execPromise_.then(execFn_).then( - (result: Object) => { this.execPromiseResult_ = result; }, - (err: Error) => { this.execPromiseError_ = err; }); + (global).tempEl = (global).browser.rootEl; + this.execPromise_ = + this.execPromise_.then(execFn_) + .then((result: Object) => { this.execPromiseResult_ = result; }) + .catch(() => { + (global).browser.rootEl = ''; + execFn_() + .then((result: Object) => { + this.execPromiseResult_ = result; + }) + .catch((err: any) => { this.execPromiseError_ = err; }); + (global).browser.rootEl = (global).tempEl; + }); + // This dummy command is necessary so that the DeferredExecutor.execute // break point can find something to stop at instead of moving on to the @@ -1090,6 +1098,7 @@ export class ProtractorBrowser extends Webdriver { // Run code through vm so that we can maintain a local scope which is // isolated from the rest of the execution. let res = vm_.runInContext(code, sandbox); + if (!webdriver.promise.isPromise(res)) { res = webdriver.promise.fulfilled(res); } @@ -1100,8 +1109,7 @@ export class ProtractorBrowser extends Webdriver { } else { // The '' forces res to be expanded into a string instead of just // '[Object]'. Then we remove the extra space caused by the '' - // using - // substring. + // using substring. return util.format.apply(this, ['', res]).substring(1); } }); @@ -1165,16 +1173,16 @@ export class ProtractorBrowser extends Webdriver { let debuggerClientPath = __dirname + '/debugger/clients/explorer.js'; let onStartFn = () => { logger.info(); - logger.info('------- Element Explorer -------'); - logger.info( - 'Starting WebDriver debugger in a child process. Element ' + - 'Explorer is still beta, please report issues at ' + - 'github.com/angular/protractor'); + logger.info('------- Element Explorer ---------------------------------'); + logger.info('Starting WebDriver debugger in a child process. Element'); + logger.info('Explorer is still beta, please report issues at '); + logger.info('github.com/angular/protractor'); logger.info(); logger.info('Type to see a list of locator strategies.'); - logger.info( - 'Use the `list` helper function to find elements by strategy:'); - logger.info(' e.g., list(by.binding(\'\')) gets all bindings.'); + logger.info('Use the `list` and `highlight` helper function to find'); + logger.info('elements by strategy:') + logger.info(' e.g., list(by.binding(\'\')) gets text of all bindings.'); + logger.info(' e.g., highlight(by.binding(\'\')) highlight html.'); logger.info(); }; this.initDebugger_(debuggerClientPath, onStartFn, opt_debugPort); diff --git a/lib/debugger/clients/explorerScripts.ts b/lib/debugger/clients/explorerScripts.ts new file mode 100644 index 000000000..575f093c3 --- /dev/null +++ b/lib/debugger/clients/explorerScripts.ts @@ -0,0 +1,84 @@ +import {promise, WebElement} from 'selenium-webdriver'; + +import {ProtractorBrowser} from '../../browser'; +import {Locator} from '../../locators'; +let absoluteXPath = require('./explorerXpath').absoluteXPath; + +export class ExplorerScripts { + static list(locator: Locator) { + (global).tempRootEl = (global).browser.rootEl; + return (global) + .browser.findElements(locator) + .then((arr: WebElement[]) => { + let found: string[] = []; + for (let i = 0; i < arr.length; ++i) { + arr[i].getText().then((text: string) => { found.push(text); }); + } + return found; + }) + .catch((err: Error) => { throw err; }); + } + + static highlight(locator: Locator) { + let xPaths: string[] = []; + let cssBorders: string[] = []; + + return (global) + .browser.findElements(locator) + .then((arr: WebElement[]) => { + + for (let i = 0; i < arr.length; ++i) { + ExplorerScripts.getAbsoluteXPath(arr[i]).then( + (result: string) => { xPaths.push(result); }); + (arr[i] as any).getCssValue('border').then((val: string) => { + cssBorders.push(val); + }); + } + if (arr.length == 0) { + return 'No elements found.'; + } + + }) + .then(() => { + + let timesFlashed = 10; + let waitTime = 500; + for (let t = 0; t < timesFlashed; t++) { + let borderValue = '5px dotted rgb(255, 55, 55)'; + if (t % 2 == 1) { + borderValue = '5px dotted rgb(255, 180, 55)'; + } + for (let i = 0; i < xPaths.length; i++) { + (global) + .browser.executeScript( + 'var x = document.evaluate(\'' + xPaths[i] + + '\', document, null, XPathResult.ANY_TYPE, null);' + + 'var xx = x.iterateNext(); xx.style.border = \'' + + borderValue + '\';'); + } + + global.browser.driver.sleep(waitTime); + } + + }) + .then(() => { + + for (let j = 0; j < cssBorders.length; j++) { + (global) + .browser.executeScript( + 'var x = document.evaluate(\'' + xPaths[j] + + '\', document, null, XPathResult.ANY_TYPE, null);' + + 'var xx = x.iterateNext(); xx.style.border = \'' + + cssBorders[j] + '\';'); + } + return 'Highlighting Completed.'; + + }); + }; + + + static getAbsoluteXPath(element: WebElement): promise.Promise { + return (global) + .browser.executeScript(absoluteXPath(arguments[0]), element); + }; +} diff --git a/lib/debugger/clients/explorerXpath.js b/lib/debugger/clients/explorerXpath.js new file mode 100644 index 000000000..c02e8236c --- /dev/null +++ b/lib/debugger/clients/explorerXpath.js @@ -0,0 +1,58 @@ + +// jshint browser: true +// jshint shadow: true +/* global window */ + +exports.absoluteXPath = function(element) { + var comp, comps = []; + var xpath = ''; + var getPos = function(element) { + var position = 1, curNode; + if (element.nodeType == window.Node.ATTRIBUTE_NODE) { + return null; + } + for (curNode = element.previousSibling; curNode; curNode = curNode.previousSibling) { + if (curNode.nodeName == element.nodeName) { + ++position; + } + } + return position; + }; + + if (element instanceof window.Document) { + return '/'; + } + + for (; element && !(element instanceof window.Document); + element = element.nodeType == window.Node.ATTRIBUTE_NODE ? + element.ownerElement : element.parentNode) { + comp = comps[comps.length] = {}; + switch (element.nodeType) { + case window.Node.TEXT_NODE: + comp.name = 'text()'; + break; + case window.Node.ATTRIBUTE_NODE: + comp.name = '@' + element.nodeName; + break; + case window.Node.PROCESSING_INSTRUCTION_NODE: + comp.name = 'processing-instruction()'; + break; + case window.Node.COMMENT_NODE: + comp.name = 'comment()'; + break; + case window.Node.ELEMENT_NODE: + comp.name = element.nodeName; + break; + } + comp.position = getPos(element); + } + + for (var i = comps.length - 1; i >= 0; i--) { + comp = comps[i]; + xpath += '/' + comp.name.toLowerCase(); + if (comp.position !== null) { + xpath += '[' + comp.position + ']'; + } + } + return xpath; +};