From 66aaa1522af8505ec7aa9dcf97eb5f16d0f8d293 Mon Sep 17 00:00:00 2001 From: Viktor Pergjoka Date: Fri, 20 Oct 2017 12:33:13 +0200 Subject: [PATCH 1/6] feat(element): getText returns text for input fields (#2973) element.getText() returns now also the text for input fields --- lib/element.ts | 45 ++++++++++++++++++++++++++++++++---- spec/basic/elements_spec.js | 23 ++++++++++++++++++ website/test/e2e/api_spec.js | 4 ++-- 3 files changed, 66 insertions(+), 6 deletions(-) diff --git a/lib/element.ts b/lib/element.ts index ee6d3d5af..503f28437 100644 --- a/lib/element.ts +++ b/lib/element.ts @@ -14,9 +14,8 @@ export class WebdriverWebElement {} export interface WebdriverWebElement extends WebElement { [key: string]: any; } let WEB_ELEMENT_FUNCTIONS = [ - 'click', 'sendKeys', 'getTagName', 'getCssValue', 'getAttribute', 'getText', 'getSize', - 'getLocation', 'isEnabled', 'isSelected', 'submit', 'clear', 'isDisplayed', 'getId', - 'takeScreenshot' + 'click', 'sendKeys', 'getTagName', 'getCssValue', 'getAttribute', 'getSize', 'getLocation', + 'isEnabled', 'isSelected', 'submit', 'clear', 'isDisplayed', 'getId', 'takeScreenshot' ]; /** @@ -434,7 +433,7 @@ export class ElementArrayFinder extends WebdriverWebElement { /** * Returns true if there are any elements present that match the finder. * - * @alias element.all(locator).isPresent() + * @alias element.all(locator).isnpm run formatPresent() * * @example * expect($('.item').isPresent()).toBeTruthy(); @@ -447,6 +446,14 @@ export class ElementArrayFinder extends WebdriverWebElement { }); } + getText(): wdpromise.Promise { + return this.asElementFinders_().then((parentWebElements: WebElement[]) => { + let childList = + parentWebElements.map((parentWebElement: WebElement) => parentWebElement.getText()); + return wdpromise.all(childList); + }); + } + /** * Returns the most relevant locator. * @@ -1166,6 +1173,36 @@ export class ElementFinder extends WebdriverWebElement { (element as any).getWebElement ? (element as ElementFinder).getWebElement() : element as WebElement); } + + /** + * Get the visible innerText of this element, including sub-elements, without + * any leading or trailing whitespace. Visible elements are not hidden by CSS. + * Works also for input fields + * + * @view + *
Inner text
+ * + * @example + * var foo = element(by.id('foo')); + * expect(foo.getText()).toEqual('Inner text'); + * + * @returns {!webdriver.promise.Promise.} A promise that will be + * resolved with the element's visible text. + */ + getText(): wdpromise.Promise { + let webElem = this.getWebElement(); + return webElem.getText() + .then((text) => { + if (text) { + return text; + } + return webElem.getAttribute('value'); + }) + .then((value) => { + value = value || ''; + return value; + }); + } } /** diff --git a/spec/basic/elements_spec.js b/spec/basic/elements_spec.js index ed2a3121f..38aeaf876 100644 --- a/spec/basic/elements_spec.js +++ b/spec/basic/elements_spec.js @@ -593,3 +593,26 @@ describe('shortcut css notation', function() { expect(withoutShortcutCount).toEqual(withShortcutCount); }); }); + +describe('should get the text of input fields', function() { + beforeEach(function() { + browser.get('index.html#/form'); + }); + + it('should get the text of an input', function() { + var usernameInput = element(by.model('username')); + var textExpect = 'Jane'; + usernameInput.clear(); + usernameInput.sendKeys(textExpect); + expect(usernameInput.getText()).toEqual(textExpect); + }); + + it('should get the text of a text area', function(){ + var textArea = element(by.model('aboutbox')); + var textExpect = 'Text area'; + textArea.clear(); + textArea.sendKeys(textExpect); + expect(textArea.getText()).toEqual(textExpect); + }); + +}); diff --git a/website/test/e2e/api_spec.js b/website/test/e2e/api_spec.js index 50d803118..2b3d77cf8 100644 --- a/website/test/e2e/api_spec.js +++ b/website/test/e2e/api_spec.js @@ -96,7 +96,7 @@ describe('Api', function() { expect(apiPage.getChildFunctionNames()).toEqual([ 'clone', 'all', 'filter', 'get', 'first', 'last', '$$', 'count', 'isPresent', 'locator', 'then', 'each', 'map', 'reduce', 'evaluate', - 'allowAnimations']); + 'allowAnimations', 'getText']); }); it('should show element functions', function() { @@ -106,7 +106,7 @@ describe('Api', function() { // Then ensure the child functions are shown. expect(apiPage.getChildFunctionNames()).toEqual([ 'clone', 'locator', 'getWebElement', 'all', 'element', '$$', - '$', 'isPresent', 'isElementPresent', 'evaluate', 'allowAnimations', 'equals']); + '$', 'isPresent', 'isElementPresent', 'evaluate', 'allowAnimations', 'equals', 'getText']); }); it('should show browser functions', function() { From 4e526d9c938a16ac09c11022169995a6405a6291 Mon Sep 17 00:00:00 2001 From: viktorpergjoka Date: Thu, 26 Oct 2017 20:58:29 +0200 Subject: [PATCH 2/6] minor fix --- lib/element.ts | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/lib/element.ts b/lib/element.ts index 503f28437..d182a7b0d 100644 --- a/lib/element.ts +++ b/lib/element.ts @@ -1191,17 +1191,12 @@ export class ElementFinder extends WebdriverWebElement { */ getText(): wdpromise.Promise { let webElem = this.getWebElement(); - return webElem.getText() - .then((text) => { - if (text) { - return text; - } - return webElem.getAttribute('value'); - }) - .then((value) => { - value = value || ''; - return value; - }); + return webElem.getText().then((text) => { + if (text) { + return text; + } + return webElem.getAttribute('value').then((value) => value || ''); + }); } } From ab4118b85350b1d634ff87357cc58f12c6419576 Mon Sep 17 00:00:00 2001 From: viktorpergjoka Date: Thu, 26 Oct 2017 22:08:58 +0200 Subject: [PATCH 3/6] return trimmed text. See #3965 --- lib/element.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/element.ts b/lib/element.ts index d182a7b0d..87faef7f9 100644 --- a/lib/element.ts +++ b/lib/element.ts @@ -1193,9 +1193,12 @@ export class ElementFinder extends WebdriverWebElement { let webElem = this.getWebElement(); return webElem.getText().then((text) => { if (text) { - return text; + return text.trim(); } - return webElem.getAttribute('value').then((value) => value || ''); + return webElem.getAttribute('value').then((value) => { + value = value || ''; + return value.trim(); + }); }); } } From 4b5baecfcb83616f684154d22f4b8451ef790c67 Mon Sep 17 00:00:00 2001 From: viktorpergjoka Date: Sat, 28 Oct 2017 23:08:48 +0200 Subject: [PATCH 4/6] revert --- lib/element.ts | 43 ++++-------------------------------- spec/basic/elements_spec.js | 23 ------------------- website/test/e2e/api_spec.js | 4 ++-- 3 files changed, 6 insertions(+), 64 deletions(-) diff --git a/lib/element.ts b/lib/element.ts index 87faef7f9..ee6d3d5af 100644 --- a/lib/element.ts +++ b/lib/element.ts @@ -14,8 +14,9 @@ export class WebdriverWebElement {} export interface WebdriverWebElement extends WebElement { [key: string]: any; } let WEB_ELEMENT_FUNCTIONS = [ - 'click', 'sendKeys', 'getTagName', 'getCssValue', 'getAttribute', 'getSize', 'getLocation', - 'isEnabled', 'isSelected', 'submit', 'clear', 'isDisplayed', 'getId', 'takeScreenshot' + 'click', 'sendKeys', 'getTagName', 'getCssValue', 'getAttribute', 'getText', 'getSize', + 'getLocation', 'isEnabled', 'isSelected', 'submit', 'clear', 'isDisplayed', 'getId', + 'takeScreenshot' ]; /** @@ -433,7 +434,7 @@ export class ElementArrayFinder extends WebdriverWebElement { /** * Returns true if there are any elements present that match the finder. * - * @alias element.all(locator).isnpm run formatPresent() + * @alias element.all(locator).isPresent() * * @example * expect($('.item').isPresent()).toBeTruthy(); @@ -446,14 +447,6 @@ export class ElementArrayFinder extends WebdriverWebElement { }); } - getText(): wdpromise.Promise { - return this.asElementFinders_().then((parentWebElements: WebElement[]) => { - let childList = - parentWebElements.map((parentWebElement: WebElement) => parentWebElement.getText()); - return wdpromise.all(childList); - }); - } - /** * Returns the most relevant locator. * @@ -1173,34 +1166,6 @@ export class ElementFinder extends WebdriverWebElement { (element as any).getWebElement ? (element as ElementFinder).getWebElement() : element as WebElement); } - - /** - * Get the visible innerText of this element, including sub-elements, without - * any leading or trailing whitespace. Visible elements are not hidden by CSS. - * Works also for input fields - * - * @view - *
Inner text
- * - * @example - * var foo = element(by.id('foo')); - * expect(foo.getText()).toEqual('Inner text'); - * - * @returns {!webdriver.promise.Promise.} A promise that will be - * resolved with the element's visible text. - */ - getText(): wdpromise.Promise { - let webElem = this.getWebElement(); - return webElem.getText().then((text) => { - if (text) { - return text.trim(); - } - return webElem.getAttribute('value').then((value) => { - value = value || ''; - return value.trim(); - }); - }); - } } /** diff --git a/spec/basic/elements_spec.js b/spec/basic/elements_spec.js index 38aeaf876..ed2a3121f 100644 --- a/spec/basic/elements_spec.js +++ b/spec/basic/elements_spec.js @@ -593,26 +593,3 @@ describe('shortcut css notation', function() { expect(withoutShortcutCount).toEqual(withShortcutCount); }); }); - -describe('should get the text of input fields', function() { - beforeEach(function() { - browser.get('index.html#/form'); - }); - - it('should get the text of an input', function() { - var usernameInput = element(by.model('username')); - var textExpect = 'Jane'; - usernameInput.clear(); - usernameInput.sendKeys(textExpect); - expect(usernameInput.getText()).toEqual(textExpect); - }); - - it('should get the text of a text area', function(){ - var textArea = element(by.model('aboutbox')); - var textExpect = 'Text area'; - textArea.clear(); - textArea.sendKeys(textExpect); - expect(textArea.getText()).toEqual(textExpect); - }); - -}); diff --git a/website/test/e2e/api_spec.js b/website/test/e2e/api_spec.js index 2b3d77cf8..50d803118 100644 --- a/website/test/e2e/api_spec.js +++ b/website/test/e2e/api_spec.js @@ -96,7 +96,7 @@ describe('Api', function() { expect(apiPage.getChildFunctionNames()).toEqual([ 'clone', 'all', 'filter', 'get', 'first', 'last', '$$', 'count', 'isPresent', 'locator', 'then', 'each', 'map', 'reduce', 'evaluate', - 'allowAnimations', 'getText']); + 'allowAnimations']); }); it('should show element functions', function() { @@ -106,7 +106,7 @@ describe('Api', function() { // Then ensure the child functions are shown. expect(apiPage.getChildFunctionNames()).toEqual([ 'clone', 'locator', 'getWebElement', 'all', 'element', '$$', - '$', 'isPresent', 'isElementPresent', 'evaluate', 'allowAnimations', 'equals', 'getText']); + '$', 'isPresent', 'isElementPresent', 'evaluate', 'allowAnimations', 'equals']); }); it('should show browser functions', function() { From 290250cbc526db5bad9dc2b0df67671df6b5ed66 Mon Sep 17 00:00:00 2001 From: viktorpergjoka Date: Sat, 11 Nov 2017 15:21:50 +0100 Subject: [PATCH 5/6] merged with master --- docs/browser-setup.md | 32 +++++++++++++++++++++++++++++++- docs/debugging.md | 16 +++++++++++++++- docs/timeouts.md | 2 +- docs/tutorial.md | 7 +++++++ lib/clientsidescripts.js | 14 +++++++++++--- lib/locators.ts | 5 +++-- spec/basic/locators_spec.js | 20 ++++++++++++++++++++ 7 files changed, 88 insertions(+), 8 deletions(-) diff --git a/docs/browser-setup.md b/docs/browser-setup.md index 4471099ee..15dafdf8b 100644 --- a/docs/browser-setup.md +++ b/docs/browser-setup.md @@ -54,6 +54,19 @@ capabilities: { }, ``` +Adding Firefox-Specific Options +------------------------------ + +Firefox options are nested in the `moz:firefoxOptions` object. A full list of options is at the [GeckoDriver](https://github.com/mozilla/geckodriver#firefox-capabilities) Github page. For example, to run in safe mode, your configuration would look like this: + +```javascript +capabilities: { + 'browserName': 'firefox', + 'moz:firefoxOptions': { + 'args': ['--safe-mode'] + } +}, +``` Testing Against Multiple Browsers --------------------------------- @@ -118,7 +131,10 @@ browser2.$('.css').click(); Setting up PhantomJS -------------------- -PhantomJS is [no longer officially supported](https://groups.google.com/forum/#!topic/phantomjs/9aI5d-LDuNE). Instead, we recommend either [running Chrome in Xvfb](http://www.tothenew.com/blog/protractor-with-jenkins-and-headless-chrome-xvfb-setup/) or using Chrome's [headless mode](https://developers.google.com/web/updates/2017/04/headless-chrome). +PhantomJS is [no longer officially supported](https://groups.google.com/forum/#!topic/phantomjs/9aI5d-LDuNE). Instead, we recommend to use one of the following alternatives: +1. Chrome with [headless mode](https://developers.google.com/web/updates/2017/04/headless-chrome). Available in Chrome 59+ on Linux/Mac OS X, and in Chrome 60+ on Windows. +2. Firefox with [headless mode](https://developer.mozilla.org/en-US/docs/Mozilla/Command_Line_Options#-headless). Available in Firefox 55+ on Linux, and in Firefox 56+ on Windows/Mac OS X. +3. Chrome with [Xvfb](http://www.tothenew.com/blog/protractor-with-jenkins-and-headless-chrome-xvfb-setup/). Using headless Chrome @@ -138,3 +154,17 @@ capabilities: { } } ``` + +Using headless Firefox +--------------------- +To start Firefox in headless mode, start Firefox with the [`--headless` flag](https://developer.mozilla.org/en-US/docs/Mozilla/Command_Line_Options#-headless). + +```javascript +capabilities: { + browserName: 'firefox', + + 'moz:firefoxOptions': { + args: [ "--headless" ] + } +} +``` diff --git a/docs/debugging.md b/docs/debugging.md index 7a833759d..f0030c591 100644 --- a/docs/debugging.md +++ b/docs/debugging.md @@ -148,6 +148,20 @@ used from the browser's console. // You can also limit the scope of the locator > window.clientSideScripts.findInputs('username', document.getElementById('#myEl')); ``` + +**Debugging with the control flow disabled** + +If you've set the `SELENIUM_PROMISE_MANAGER` config value to false to [disable the control flow](https://github.com/angular/protractor/blob/master/docs/control-flow.md), +the above methods will not work. Instead, you can now use native `debugger` statements to pause your code. However, you +will need to start your tests using Node's `--inspect-brk` option: + +``` +node --inspect-brk node_modules/.bin/protractor +``` + +You will then be able to use the Chrome devtools at chrome://inspect to connect to the tests. + + Setting Up VSCode for Debugging ------------------------------- VS Code has built-in [debugging](https://code.visualstudio.com/docs/editor/debugging) support for the Node.js runtime and can debug JavaScript, TypeScript, and any other language that gets transpiled to JavaScript. @@ -174,7 +188,7 @@ To set up WebStorm for Protractor, do the following: 3. On the Configuration tab set: - **Node Interpreter**: path to node executable - **Working directory**: your project base path - - **JavaScript file**: path to Protractor cli.js file (e.g. *node_modules\protractor\lib\cli.js*) + - **JavaScript file**: path to Protractor cli.js file (e.g. *node_modules\protractor\built\cli.js*) - **Application parameters**: path to your Protractor configuration file (e.g. *protractorConfig.js*) 4. Click OK, place some breakpoints, and start debugging. diff --git a/docs/timeouts.md b/docs/timeouts.md index 252e63da4..5648bb9f6 100644 --- a/docs/timeouts.md +++ b/docs/timeouts.md @@ -64,7 +64,7 @@ Protractor waits for the `angular` variable to be present when loading a new pag ### _How to disable waiting for Angular_ If you need to navigate to a page which does not use Angular, you can turn off waiting for Angular by setting -`browser.waitForAngularEnabled(false). For example: +`browser.waitForAngularEnabled(false)`. For example: ```js browser.waitForAngularEnabled(false); diff --git a/docs/tutorial.md b/docs/tutorial.md index 7a9946fc0..e87758913 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -144,11 +144,18 @@ describe('Protractor Demo App', function() { // Fill this in. expect(latestResult.getText()).toEqual('10'); }); + + it('should read the value from an input', function() { + firstNumber.sendKeys(1); + expect(firstNumber.getAttribute('value')).toEqual('1'); + }); }); ``` Here, we've pulled the navigation out into a `beforeEach` function which is run before every `it` block. We've also stored the ElementFinders for the first and second input in nice variables that can be reused. Fill out the second test using those variables, and run the tests again to ensure they pass. +In the last assertion we read the value from the input field with `firstNumber.getAttribute('value')` and compare it with the value we have set before. + Step 3 - changing the configuration ----------------------------------- diff --git a/lib/clientsidescripts.js b/lib/clientsidescripts.js index dd7fb230e..ad795ec51 100644 --- a/lib/clientsidescripts.js +++ b/lib/clientsidescripts.js @@ -178,7 +178,7 @@ functions.waitForAngular = function(rootSelector, callback) { } catch(e){} if (testability) { - return testability.whenStable(testCallback); + return testability.whenStable(function() { testCallback(); }); } } @@ -676,7 +676,7 @@ functions.findByPartialButtonText = function(searchText, using) { * Find elements by css selector and textual content. * * @param {string} cssSelector The css selector to match. - * @param {string} searchText The exact text to match. + * @param {string} searchText The exact text to match or a serialized regex. * @param {Element} using The scope of the search. * * @return {Array.} An array of matching elements. @@ -684,12 +684,20 @@ functions.findByPartialButtonText = function(searchText, using) { functions.findByCssContainingText = function(cssSelector, searchText, using) { using = using || document; + if (searchText.indexOf('__REGEXP__') === 0) { + var match = searchText.split('__REGEXP__')[1].match(/\/(.*)\/(.*)?/); + searchText = new RegExp(match[1], match[2] || ''); + } var elements = using.querySelectorAll(cssSelector); var matches = []; for (var i = 0; i < elements.length; ++i) { var element = elements[i]; var elementText = element.textContent || element.innerText || ''; - if (elementText.indexOf(searchText) > -1) { + var elementMatches = searchText instanceof RegExp ? + searchText.test(elementText) : + elementText.indexOf(searchText) > -1; + + if (elementMatches) { matches.push(element); } } diff --git a/lib/locators.ts b/lib/locators.ts index ae1f538d2..f6852ea02 100644 --- a/lib/locators.ts +++ b/lib/locators.ts @@ -415,10 +415,11 @@ export class ProtractorBy extends WebdriverBy { * var dog = element(by.cssContainingText('.pet', 'Dog')); * * @param {string} cssSelector css selector - * @param {string} searchString text search + * @param {string|RegExp} searchString text search * @returns {ProtractorLocator} location strategy */ - cssContainingText(cssSelector: string, searchText: string): ProtractorLocator { + cssContainingText(cssSelector: string, searchText: string|RegExp): ProtractorLocator { + searchText = (searchText instanceof RegExp) ? '__REGEXP__' + searchText.toString() : searchText; return { findElementsOverride: (driver: WebDriver, using: WebElement, rootSelector: string): wdpromise.Promise => { diff --git a/spec/basic/locators_spec.js b/spec/basic/locators_spec.js index 6d64bb727..0f370b00b 100644 --- a/spec/basic/locators_spec.js +++ b/spec/basic/locators_spec.js @@ -353,6 +353,26 @@ describe('locators', function() { expect(element(by.cssContainingText('#transformedtext div', 'capitalize')) .getAttribute('id')).toBe('textcapitalize'); }); + + it('should find elements with a regex', function() { + element.all(by.cssContainingText('#transformedtext div', /(upper|lower)case/i)) + .then(function(found) { + expect(found.length).toEqual(2); + expect(found[0].getText()).toBe('UPPERCASE'); + expect(found[1].getText()).toBe('lowercase'); + }); + }); + + it('should find elements with a regex with no flags', function() { + // this test matches the non-transformed text. + // the text is actually transformed with css, + // so you can't match the Node innerText or textContent. + element.all(by.cssContainingText('#transformedtext div', /Uppercase/)) + .then(function(found) { + expect(found.length).toEqual(1); + expect(found[0].getText()).toBe('UPPERCASE'); + }); + }); }); describe('by options', function() { From f1845fe726602bc0744dfe2481f6fde1e9109d1c Mon Sep 17 00:00:00 2001 From: viktorpergjoka Date: Tue, 16 Jan 2018 20:48:15 +0100 Subject: [PATCH 6/6] Added expectedConditions.numberOfWindowsToBe --- lib/expectedConditions.ts | 17 +++++++++++++++++ spec/basic/expected_conditions_spec.js | 11 +++++++++++ 2 files changed, 28 insertions(+) diff --git a/lib/expectedConditions.ts b/lib/expectedConditions.ts index 43d399fb9..64686a194 100644 --- a/lib/expectedConditions.ts +++ b/lib/expectedConditions.ts @@ -434,4 +434,21 @@ export class ProtractorExpectedConditions { return elementFinder.isSelected().then(passBoolean, falseIfMissing); }); } + + /** + * An expectation for checking the number of windows. + * + * @alias ExpectedConditions.numberOfWindowsToBe + * @param {number} expectedNumberOfWindows The number to verify against. + * + * @returns {!function} An expected condition that returns a promise + * representing whether the number of windows is equal to defined. + */ + numberOfWindowsToBe(expectedNumberOfWindows: number): Function { + return () => { + return this.browser.driver.getAllWindowHandles().then((windowHandles) => { + return windowHandles.length === expectedNumberOfWindows; + }); + }; + } } diff --git a/spec/basic/expected_conditions_spec.js b/spec/basic/expected_conditions_spec.js index e91155070..39f6e45e3 100644 --- a/spec/basic/expected_conditions_spec.js +++ b/spec/basic/expected_conditions_spec.js @@ -130,6 +130,17 @@ describe('expected conditions', function() { validityOfTitle, presenceOfInvalidElement).call()).toBe(false); }); + it('should have numberOfWindowsToBe', function() { + browser.executeScript('window.open("http://localhost:8081/ng1/#/form");'); //new tab + expect(EC.numberOfWindowsToBe(2).call()).toEqual(true); + browser.getAllWindowHandles().then(function(handles) { + browser.driver.switchTo().window(handles[1]); + browser.driver.close(); + browser.driver.switchTo().window(handles[0]); + expect(EC.numberOfWindowsToBe(1).call()).toEqual(true); + }); + }); + it('and should shortcircuit', function() { var invalidElem = $('#INVALID');